封面 《創作彼女の恋愛公式》
前言
因为 cc98 老是忘记签到,于是想着写一个自动签到脚本,然后用 crontab 挂一个定期执行,这样就不需要自己每天记得上 98 了,顺便用 golang 写一下当个练手小玩具
分析
首先可以很明确的分析登录和签到两个接口
登录
到登陆页面打开开发者工具,然后输入账号密码,点击登录,可以看到登录接口的请求
可以看到登录接口是 https://openid.cc98.org/connect/token
,然后请求方式是 POST
,请求参数是 username
和 password
,此外还有 Oauth2 涉及到的参数,关于 client_id
和 client_secret
可以使用 cc98 自带的,也可以自己到 https://openid.cc98.org/
去申请自己的应用进行使用
返回的是一个 json
,格式如下
1 2 3 4 5 6 7
| { "access_token": "", "expires_in": 3600, "token_type": "Bearer", "refresh_token": "", "scope": "cc98-api offline_access openid" }
|
主要为使用 access_token
去进行我们后续的操作,否则报 401,后续需要用户身份的接口中需要加上 header
,其中 key 为 Authorization
,值为 Bearer {access_token}
。s 另外需要注意的是请求时候的 content-type
为 application/x-www-form-urlencoded
签到
点击签到按钮,观察后续的请求。因为我的号都进行了测试无法再看,这里直接说一下请求的接口是 https://api.cc98.org/me/signin
,post
方法,无需任何请求参数,但是 content-type
为 application/json
,,返回的是一个字符串。当未签到的时候该值为签到获得的 98 币,如果已经完成签到,那么返回的字符串为 has_signed_in_today
获取签到天数
和 98 小程序比,我们明显的看到缺少了连续签到天数,因此继续看接口,可以看到签到点击后还有一个和签到接口一样的地址为 https://api.cc98.org/me/signin
,但是方法为 get
,无需任何参数,但是 content-type
为 application/json
该接口返回一个 json
,其结果如下
1 2 3 4 5
| { "lastSignInTime": "2023-12-22T00:02:50.453", "lastSignInCount": 2, "hasSignedInToday": true }
|
其中 lastSignInCount
就是连续签到天数,hasSignedInToday
为今天是否签到,lastSignInTime
为最后一次签到时间
代码
总体代码仓库在 github, https://github.com/qxdn/cc98-sign
登录
主要为 http 包使用,模拟登录接口,获取用户的登录返回的信息
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
| package login
import ( "encoding/json" "io/ioutil" "net/http" "net/url" )
type User struct { Username string Password string }
type LoginInfo struct { AccessToken string `json:"access_token"` ExpiresIn string `json:"expires_in"` TokenType string `json:"token_type"` RefreshToken string `json:"refresh_token"` Scope string `json:"scope"` }
func Login(user *User) *LoginInfo {
resp, err := http.PostForm("https://openid.cc98.org/connect/token", url.Values{ "username": {user.Username}, "password": {user.Password}, "grant_type": {"password"}, "scope": {"cc98-api openid offline_access"}, "client_id": {"9a1fd200-8687-44b1-4c20-08d50a96e5cd"}, "client_secret": {"8b53f727-08e2-4509-8857-e34bf92b27f2"}, }) if err != nil { panic(err) } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil {
panic(err) } var info LoginInfo json.Unmarshal(body, &info) return &info }
|
签到和签到结果
此处代码主要为模拟登陆接口和获取签到后的信息
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
| package sign
import ( "encoding/json" "io/ioutil" "net/http" "strconv" )
func SignIn(token string) int { client := &http.Client{} req, err := http.NewRequest("POST", "https://api.cc98.org/me/signin", nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() rawBody, err := ioutil.ReadAll(resp.Body) if err != nil { panic(err) } body := string(rawBody) if body == "has_signed_in_today" { return 0 } coins, _ := strconv.Atoi(body) return coins }
type SignResult struct { LastSignInTime string `json:"lastSignInTime"` LastSignInCount int `json:"lastSignInCount"` HasSignedInToday bool `json:"hasSignedInToday"` }
func GetSignResult(token string) *SignResult { client := &http.Client{} req, err := http.NewRequest("GET", "https://api.cc98.org/me/signin", nil) if err != nil { panic(err) } req.Header.Set("Authorization", "Bearer "+token) resp, err := client.Do(req) if err != nil { panic(err) } defer resp.Body.Close() body, err := ioutil.ReadAll(resp.Body) if err != nil { panic(err) } var result SignResult json.Unmarshal(body, &result) return &result }
|
配置
此处主要为生成默认配置文件和读取配置文件
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
| package config
import ( "encoding/json" "fmt" "io/ioutil" "os" )
type User struct { Username string `json:"username"` Password string `json:"password"` }
type Config struct { Users []User `json:"users"` }
func PathExists(path string) (bool, error) { _, err := os.Stat(path) if err == nil { return true, nil } if os.IsNotExist(err) { return false, nil } return false, err }
func ReadConfig(filepath string) *Config { var configs Config exists, _ := PathExists(filepath) if !exists { configs.Users = []User{{ Username: "用户名", Password: "密码", }} file, _ := json.MarshalIndent(configs, "", " ") ioutil.WriteFile(filepath, file, 0644) fmt.Println("配置文件不存在,已经生成默认配置文件") return nil } jsonFile, err := os.Open(filepath) if err != nil { panic(err) } defer jsonFile.Close() byteValue, _ := ioutil.ReadAll(jsonFile) json.Unmarshal(byteValue, &configs) return &configs }
|
主函数
这里主要为定义一个函数进行自动登陆,采用协程启动多个账户。需要注意的是使用 waitGroup 进行同步,否则主函数执行完,协程还没启动就退出了。
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
| package main
import ( "fmt" "sync"
"github.com/qxdn/cc98sign/pkg/config" "github.com/qxdn/cc98sign/pkg/login" "github.com/qxdn/cc98sign/pkg/sign" )
func AutoSign(user *login.User, done func()) { defer done() info := login.Login(user) coins := sign.SignIn(info.AccessToken) if coins == 0 { fmt.Printf("用户(%s)今日已经签到\n", user.Username) return } result := sign.GetSignResult(info.AccessToken) fmt.Printf("用户(%s)已经连续签到%d天,今日签到获得%d 98币\n", user.Username, result.LastSignInCount, coins) }
func main() { configs := config.ReadConfig("config.json") if configs == nil { return } var waitGroup sync.WaitGroup waitGroup.Add(len(configs.Users)) for _, cuser := range configs.Users { user := &login.User{ Username: cuser.Username, Password: cuser.Password, } go AutoSign(user, waitGroup.Done) } waitGroup.Wait() }
|
效果
后记
后续需要做的是使用 crontab 执行定期执行,和使用 webvpn 来规避 ip 限制