├── docs └── geetest1.png ├── Dockerfile ├── api ├── common │ ├── verification.go │ ├── code.go │ ├── util.go │ ├── cookie.go │ ├── sign_test.go │ ├── rsa.go │ ├── resp.go │ ├── const.go │ ├── sign.go │ └── api.go ├── mihoyo │ ├── api.go │ ├── token_test.go │ ├── auth_test.go │ ├── token.go │ └── auth.go ├── miyoushe │ ├── game_preset_test.go │ ├── api.go │ ├── verification_test.go │ ├── user_test.go │ ├── sign_forum_test.go │ ├── sign_forum.go │ ├── verification.go │ ├── game_test.go │ ├── user.go │ ├── game_preset.go │ ├── home_test.go │ ├── forum_test.go │ ├── home.go │ ├── sign_game_test.go │ ├── game.go │ ├── forum.go │ └── sign_game.go └── ocr │ ├── rr_test.go │ ├── tt_test.go │ ├── rr.go │ └── tt.go ├── cmd ├── main.go ├── root.go ├── service.go ├── config.go ├── util.go ├── notify.go ├── sign.go ├── cron.go └── account.go ├── .gitignore ├── util ├── uuid_test.go ├── qrcode_test.go ├── uuid.go └── qrcode.go ├── .github └── workflows │ ├── golang.yml │ └── release.yml ├── config ├── account_test.go ├── ocr.go ├── account.go ├── config.go └── device.go ├── Makefile ├── job ├── forum_test.go ├── game_test.go ├── qrcode.go ├── auth.go ├── verfication.go ├── forum.go └── game.go ├── go.mod ├── geetest.html ├── README.md ├── .goreleaser.yaml ├── go.sum └── LICENSE /docs/geetest1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/starudream/miyoushe-task/HEAD/docs/geetest1.png -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM starudream/alpine 2 | 3 | WORKDIR / 4 | 5 | COPY miyoushe-task /miyoushe-task 6 | 7 | CMD /miyoushe-task 8 | -------------------------------------------------------------------------------- /api/common/verification.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | type Verification struct { 4 | Challenge string `json:"challenge"` 5 | Validate string `json:"validate"` 6 | } 7 | -------------------------------------------------------------------------------- /cmd/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/starudream/go-lib/core/v2/utils/osutil" 5 | ) 6 | 7 | func main() { 8 | osutil.ExitErr(rootCmd.Execute()) 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # IntelliJ project files 2 | /.idea 3 | /*.iml 4 | 5 | # Compiled files 6 | /bin 7 | /dist 8 | /log 9 | /cover 10 | *.exe 11 | *.log 12 | 13 | # Dev files 14 | miyoushe-task*.yaml 15 | -------------------------------------------------------------------------------- /api/mihoyo/api.go: -------------------------------------------------------------------------------- 1 | package mihoyo 2 | 3 | const ( 4 | AddrHK4E = "https://hk4e-sdk.mihoyo.com" 5 | AddrTakumi = "https://api-takumi.mihoyo.com" 6 | AddrPassport = "https://passport-api.mihoyo.com" 7 | ) 8 | -------------------------------------------------------------------------------- /util/uuid_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/starudream/go-lib/core/v2/utils/testutil" 7 | ) 8 | 9 | func TestUUID(t *testing.T) { 10 | testutil.Log(t, UUID()) 11 | } 12 | -------------------------------------------------------------------------------- /cmd/root.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/starudream/go-lib/cobra/v2" 5 | ) 6 | 7 | var rootCmd = cobra.NewRootCommand(func(c *cobra.Command) { 8 | c.Use = "miyoushe-task" 9 | 10 | cobra.AddConfigFlag(c) 11 | }) 12 | -------------------------------------------------------------------------------- /util/qrcode_test.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/starudream/go-lib/core/v2/utils/testutil" 7 | ) 8 | 9 | func TestQRCode(t *testing.T) { 10 | testutil.Log(t, "\n"+QRCode("hello world")) 11 | } 12 | -------------------------------------------------------------------------------- /util/uuid.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "github.com/google/uuid" 5 | 6 | "github.com/starudream/go-lib/core/v2/utils/osutil" 7 | ) 8 | 9 | func UUID() string { 10 | v, err := uuid.NewRandom() 11 | osutil.PanicErr(err) 12 | return v.String() 13 | } 14 | -------------------------------------------------------------------------------- /api/miyoushe/game_preset_test.go: -------------------------------------------------------------------------------- 1 | package miyoushe 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/starudream/go-lib/core/v2/utils/testutil" 7 | ) 8 | 9 | func TestGamePreset(t *testing.T) { 10 | testutil.Log(t, AllGames) 11 | testutil.Log(t, AllGamesById) 12 | } 13 | -------------------------------------------------------------------------------- /api/miyoushe/api.go: -------------------------------------------------------------------------------- 1 | package miyoushe 2 | 3 | const ( 4 | AddrTakumi = "https://api-takumi.mihoyo.com" 5 | AddrActNap = "https://act-nap-api.mihoyo.com" 6 | AddrTakumiRecord = "https://api-takumi-record.mihoyo.com" 7 | AddrBBS = "https://bbs-api.miyoushe.com" 8 | ) 9 | -------------------------------------------------------------------------------- /util/qrcode.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "github.com/skip2/go-qrcode" 5 | 6 | "github.com/starudream/go-lib/core/v2/utils/osutil" 7 | ) 8 | 9 | func QRCode(content string) string { 10 | code, err := qrcode.New(content, qrcode.Medium) 11 | osutil.PanicErr(err) 12 | return code.ToSmallString(false) 13 | } 14 | -------------------------------------------------------------------------------- /api/common/code.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | const ( 4 | RetCodeQRCodeExpired = -106 // 二维码过期 5 | RetCodeSendPhoneCodeFrequently = -3101 // 发送验证码频率过快 6 | RetCodeGameHasSigned = -5003 // 已签到 7 | RetCodeForumHasSigned = 1008 // 打卡失败或重复打卡 8 | RetCodeForumNeedVerification = 1034 // 需要验证码 9 | ) 10 | -------------------------------------------------------------------------------- /api/common/util.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "math/rand" 5 | ) 6 | 7 | const dicts = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" 8 | 9 | func alphanum(n int) string { 10 | bs := make([]byte, n) 11 | for i := range bs { 12 | bs[i] = dicts[rand.Intn(len(dicts))] 13 | } 14 | return string(bs) 15 | } 16 | -------------------------------------------------------------------------------- /api/ocr/rr_test.go: -------------------------------------------------------------------------------- 1 | package ocr 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/starudream/go-lib/core/v2/utils/testutil" 7 | 8 | "github.com/starudream/miyoushe-task/api/common" 9 | "github.com/starudream/miyoushe-task/config" 10 | ) 11 | 12 | func TestRR(t *testing.T) { 13 | data, err := RR(config.C().RROCRKey, "x", "x", common.RefererAct) 14 | testutil.LogNoErr(t, err, data) 15 | } 16 | -------------------------------------------------------------------------------- /api/ocr/tt_test.go: -------------------------------------------------------------------------------- 1 | package ocr 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/starudream/go-lib/core/v2/utils/testutil" 7 | 8 | "github.com/starudream/miyoushe-task/api/common" 9 | "github.com/starudream/miyoushe-task/config" 10 | ) 11 | 12 | func TestTT(t *testing.T) { 13 | data, err := TT(config.TT().Key, "x", "x", common.RefererAct) 14 | testutil.LogNoErr(t, err, data) 15 | } 16 | 17 | func TestTTResult(t *testing.T) { 18 | data, err := ttResult(config.TT().Key, "x") 19 | testutil.LogNoErr(t, err, data) 20 | } 21 | -------------------------------------------------------------------------------- /api/miyoushe/verification_test.go: -------------------------------------------------------------------------------- 1 | package miyoushe 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/starudream/go-lib/core/v2/utils/testutil" 7 | 8 | "github.com/starudream/miyoushe-task/config" 9 | ) 10 | 11 | func TestCreateVerification(t *testing.T) { 12 | data, err := CreateVerification(config.C().FirstAccount()) 13 | testutil.LogNoErr(t, err, data) 14 | } 15 | 16 | func TestVerifyVerification(t *testing.T) { 17 | data, err := VerifyVerification("", "", config.C().FirstAccount()) 18 | testutil.LogNoErr(t, err, data) 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/golang.yml: -------------------------------------------------------------------------------- 1 | name: Golang 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | name: Build 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | - uses: actions/setup-go@v5 20 | with: 21 | go-version: stable 22 | - uses: goreleaser/goreleaser-action@v6 23 | with: 24 | args: build --clean --snapshot 25 | -------------------------------------------------------------------------------- /config/account_test.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/starudream/go-lib/core/v2/utils/testutil" 7 | ) 8 | 9 | func TestAccount(t *testing.T) { 10 | phone := "123" 11 | 12 | account := Account{Phone: phone, Device: NewDevice()} 13 | testutil.Log(t, account) 14 | 15 | AddAccount(account) 16 | 17 | account, ok := GetAccount(phone) 18 | testutil.Equal(t, true, ok) 19 | testutil.Log(t, account) 20 | 21 | testutil.Nil(t, Save()) 22 | } 23 | 24 | func TestSave(t *testing.T) { 25 | testutil.Nil(t, Save()) 26 | } 27 | -------------------------------------------------------------------------------- /cmd/service.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/starudream/go-lib/core/v2/config" 7 | "github.com/starudream/go-lib/core/v2/utils/osutil" 8 | "github.com/starudream/go-lib/service/v2" 9 | ) 10 | 11 | func init() { 12 | args := []string{"cron"} 13 | if c := config.LoadedFile(); c != "" { 14 | args = append(args, "-c", c) 15 | } 16 | service.AddCommand(rootCmd, service.New("miyoushe-task", serviceCron, service.WithArguments(args...))) 17 | } 18 | 19 | func serviceCron(context.Context) { 20 | osutil.ExitErr(cronRun()) 21 | } 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | PROJECT ?= $(shell basename $(CURDIR)) 2 | MODULE ?= $(shell go list -m) 3 | VERSION ?= $(shell git describe --tags 2>/dev/null || git rev-parse --short HEAD) 4 | 5 | BITTAGS := 6 | LDFLAGS := -s -w 7 | LDFLAGS += -X "github.com/starudream/go-lib/core/v2/config/version.gitVersion=$(VERSION)" 8 | 9 | .PHONY: init 10 | init: 11 | git status -b -s 12 | go mod tidy 13 | 14 | .PHONY: bin 15 | bin: init 16 | CGO_ENABLED=0 go build -tags '$(BITTAGS)' -ldflags '$(LDFLAGS)' -o bin/$(PROJECT) $(MODULE)/cmd 17 | 18 | .PHONY: run 19 | run: bin 20 | DEBUG=true bin/$(PROJECT) $(ARGS) 21 | -------------------------------------------------------------------------------- /cmd/config.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/starudream/go-lib/cobra/v2" 5 | 6 | "github.com/starudream/miyoushe-task/config" 7 | ) 8 | 9 | var ( 10 | configCmd = cobra.NewCommand(func(c *cobra.Command) { 11 | c.Use = "config" 12 | c.Short = "Manage config" 13 | }) 14 | 15 | configSaveCmd = cobra.NewCommand(func(c *cobra.Command) { 16 | c.Use = "save" 17 | c.RunE = func(cmd *cobra.Command, args []string) error { 18 | return config.Save() 19 | } 20 | }) 21 | ) 22 | 23 | func init() { 24 | configCmd.AddCommand(configSaveCmd) 25 | 26 | rootCmd.AddCommand(configCmd) 27 | } 28 | -------------------------------------------------------------------------------- /api/common/cookie.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/starudream/miyoushe-task/config" 7 | ) 8 | 9 | // SToken generate base stoken auth cookies 10 | func SToken(account config.Account) []*http.Cookie { 11 | return []*http.Cookie{ 12 | {Name: "mid", Value: account.Mid}, 13 | {Name: "stoken", Value: account.SToken}, 14 | } 15 | } 16 | 17 | // CToken generate base cookie_token auth cookies 18 | func CToken(account config.Account) []*http.Cookie { 19 | return []*http.Cookie{ 20 | {Name: "account_id", Value: account.Uid}, 21 | {Name: "cookie_token", Value: account.CToken}, 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /job/forum_test.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/starudream/go-lib/core/v2/utils/testutil" 7 | 8 | "github.com/starudream/miyoushe-task/api/common" 9 | "github.com/starudream/miyoushe-task/api/miyoushe" 10 | ) 11 | 12 | func TestSignForumRecord_Success(t *testing.T) { 13 | r := SignForumRecord{ 14 | GameId: common.GameIdDBY, 15 | HasSigned: false, 16 | IsRisky: true, 17 | Points: 50, 18 | PostView: PostView, 19 | PostUpvote: PostUpvote, 20 | PostShare: PostShare, 21 | } 22 | r.GameName = miyoushe.AllGamesById[r.GameId].Name 23 | testutil.Log(t, r, r.Success()) 24 | } 25 | -------------------------------------------------------------------------------- /api/miyoushe/user_test.go: -------------------------------------------------------------------------------- 1 | package miyoushe 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/starudream/go-lib/core/v2/utils/testutil" 7 | 8 | "github.com/starudream/miyoushe-task/config" 9 | ) 10 | 11 | func TestLogin(t *testing.T) { 12 | err := Login(config.C().FirstAccount()) 13 | testutil.LogNoErr(t, err) 14 | } 15 | 16 | func TestGetUser(t *testing.T) { 17 | t.Run("no-auth", func(t *testing.T) { 18 | data, err := GetUser("75596302") 19 | testutil.LogNoErr(t, err, data) 20 | }) 21 | 22 | t.Run("by-auth", func(t *testing.T) { 23 | data, err := GetUser("", config.C().FirstAccount()) 24 | testutil.LogNoErr(t, err, data) 25 | }) 26 | } 27 | -------------------------------------------------------------------------------- /cmd/util.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/starudream/go-lib/core/v2/utils/osutil" 7 | "github.com/starudream/go-lib/core/v2/utils/sliceutil" 8 | 9 | "github.com/starudream/miyoushe-task/config" 10 | ) 11 | 12 | func xGetAccount(args []string, i ...int) config.Account { 13 | if len(i) == 0 { 14 | i = []int{0} 15 | } 16 | phone, _ := sliceutil.GetValue(args, i[0]) 17 | if phone == "" { 18 | osutil.ExitErr(fmt.Errorf("requires account phone")) 19 | } 20 | account, exists := config.GetAccount(phone) 21 | if !exists { 22 | osutil.ExitErr(fmt.Errorf("account %s not exists", phone)) 23 | } 24 | return account 25 | } 26 | -------------------------------------------------------------------------------- /api/mihoyo/token_test.go: -------------------------------------------------------------------------------- 1 | package mihoyo 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/starudream/go-lib/core/v2/utils/testutil" 7 | 8 | "github.com/starudream/miyoushe-task/config" 9 | ) 10 | 11 | func TestGetSTokenByGToken(t *testing.T) { 12 | data, err := GetSTokenByGToken(config.C().FirstAccount()) 13 | testutil.LogNoErr(t, err, data) 14 | } 15 | 16 | func TestGetCTokenBySToken(t *testing.T) { 17 | data, err := GetCTokenBySToken(config.C().FirstAccount()) 18 | testutil.LogNoErr(t, err, data) 19 | } 20 | 21 | func TestGetLTokenBySToken(t *testing.T) { 22 | data, err := GetLTokenBySToken(config.C().FirstAccount()) 23 | testutil.LogNoErr(t, err, data) 24 | } 25 | -------------------------------------------------------------------------------- /api/miyoushe/sign_forum_test.go: -------------------------------------------------------------------------------- 1 | package miyoushe 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/starudream/go-lib/core/v2/utils/testutil" 7 | 8 | "github.com/starudream/miyoushe-task/api/common" 9 | "github.com/starudream/miyoushe-task/config" 10 | ) 11 | 12 | func TestSignForum(t *testing.T) { 13 | data, err := SignForum(common.GameIdSR, config.C().FirstAccount(), nil) 14 | if common.IsRetCode(err, common.RetCodeForumHasSigned) { 15 | t.Skip("bbs has signed") 16 | } 17 | testutil.LogNoErr(t, err, data) 18 | } 19 | 20 | func TestGetSignForum(t *testing.T) { 21 | data, err := GetSignForum(common.GameIdSR, config.C().FirstAccount()) 22 | testutil.LogNoErr(t, err, data) 23 | } 24 | -------------------------------------------------------------------------------- /api/common/sign_test.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/starudream/go-lib/core/v2/utils/testutil" 7 | ) 8 | 9 | func TestDS1(t *testing.T) { 10 | c, s := ds1(1699372800, "123456") 11 | testutil.Equal(t, "salt=QVu5OdwEWxkq9ygpYBgDprR5tI471HWQ&t=1699372800&r=123456", c) 12 | testutil.Equal(t, "1699372800,123456,6e98ea06bfae57644337e04d818301db", s) 13 | } 14 | 15 | func TestDS2(t *testing.T) { 16 | c, s := ds2(1699372800, 100001, "", "role_id=222681079&server=cn_gf01") 17 | testutil.Equal(t, "salt=t0qEgfub6cvueAPgR5m9aQWWVciEer7v&t=1699372800&r=100001&b=&q=role_id=222681079&server=cn_gf01", c) 18 | testutil.Equal(t, "1699372800,100001,f73a2b0996a7439f11f62b53a44d8f7c", s) 19 | } 20 | -------------------------------------------------------------------------------- /config/ocr.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "time" 5 | ) 6 | 7 | type TTOCR struct { 8 | Key string `json:"key" yaml:"key"` 9 | Interval time.Duration `json:"interval" yaml:"interval"` 10 | Timeout time.Duration `json:"timeout" yaml:"timeout"` 11 | // https://www.kancloud.cn/ttorc/ttorc/3119237 12 | ItemId string `json:"item_id" yaml:"item_id"` 13 | } 14 | 15 | func (c Config) TT() TTOCR { 16 | if c.TTOCR.Interval < time.Second { 17 | c.TTOCR.Interval = 3 * time.Second 18 | } 19 | if c.TTOCR.Timeout < 60*time.Second { 20 | c.TTOCR.Timeout = 90 * time.Second 21 | } 22 | if c.TTOCR.ItemId == "" { 23 | c.TTOCR.ItemId = "388" 24 | } 25 | return c.TTOCR 26 | } 27 | 28 | func TT() TTOCR { 29 | return C().TT() 30 | } 31 | -------------------------------------------------------------------------------- /cmd/notify.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | 6 | "github.com/starudream/go-lib/cobra/v2" 7 | "github.com/starudream/go-lib/core/v2/utils/sliceutil" 8 | "github.com/starudream/go-lib/ntfy/v2" 9 | ) 10 | 11 | var ( 12 | notifyCmd = cobra.NewCommand(func(c *cobra.Command) { 13 | c.Use = "notify" 14 | c.Short = "Manage notify" 15 | }) 16 | 17 | notifySendCmd = cobra.NewCommand(func(c *cobra.Command) { 18 | c.Use = "send " 19 | c.Short = "Send notify" 20 | c.RunE = func(cmd *cobra.Command, args []string) error { 21 | msg, _ := sliceutil.GetValue(args, 0, "Hello World") 22 | return ntfy.Notify(context.Background(), msg) 23 | } 24 | }) 25 | ) 26 | 27 | func init() { 28 | notifyCmd.AddCommand(notifySendCmd) 29 | 30 | rootCmd.AddCommand(notifyCmd) 31 | } 32 | -------------------------------------------------------------------------------- /job/game_test.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/starudream/go-lib/core/v2/utils/testutil" 7 | 8 | "github.com/starudream/miyoushe-task/api/common" 9 | "github.com/starudream/miyoushe-task/api/miyoushe" 10 | ) 11 | 12 | func TestSignGameRecords_Success(t *testing.T) { 13 | rs := SignGameRecords{ 14 | { 15 | GameId: common.GameIdYS, 16 | RoleName: "游戏昵称1", 17 | RoleUid: "123456789", 18 | HasSigned: true, 19 | IsRisky: true, 20 | Award: "A*5, B*5", 21 | }, 22 | { 23 | GameId: common.GameIdSR, 24 | RoleName: "游戏昵称2", 25 | RoleUid: "123456789", 26 | HasSigned: true, 27 | IsRisky: false, 28 | Award: "ABC*500", 29 | }, 30 | } 31 | for i := range rs { 32 | rs[i].GameName = miyoushe.AllGamesById[rs[i].GameId].Name 33 | } 34 | testutil.Log(t, rs, rs.Success()) 35 | } 36 | -------------------------------------------------------------------------------- /api/mihoyo/auth_test.go: -------------------------------------------------------------------------------- 1 | package mihoyo 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/starudream/go-lib/core/v2/utils/testutil" 7 | 8 | "github.com/starudream/miyoushe-task/config" 9 | "github.com/starudream/miyoushe-task/util" 10 | ) 11 | 12 | func TestGenQRCode(t *testing.T) { 13 | data, err := GenQRCode(config.C().FirstAccount()) 14 | testutil.LogNoErr(t, err, data, data.Url, util.QRCode(data.Url)) 15 | } 16 | 17 | func TestQueryQRCode(t *testing.T) { 18 | data, err := QueryQRCode("654b81644b7cf30567d6d20c", config.C().FirstAccount()) 19 | testutil.LogNoErr(t, err, data) 20 | } 21 | 22 | func TestSendPhoneCode(t *testing.T) { 23 | account := config.C().FirstAccount() 24 | data, err := SendPhoneCode("", account) 25 | testutil.LogNoErr(t, err, data) 26 | } 27 | 28 | func TestLoginByPhoneCode(t *testing.T) { 29 | account := config.C().FirstAccount() 30 | data, err := LoginByPhoneCode("123456", account) 31 | testutil.LogNoErr(t, err, data) 32 | } 33 | -------------------------------------------------------------------------------- /api/common/rsa.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "crypto/rand" 5 | "crypto/rsa" 6 | "crypto/x509" 7 | "encoding/base64" 8 | "encoding/pem" 9 | 10 | "github.com/starudream/go-lib/core/v2/utils/osutil" 11 | ) 12 | 13 | const publicKeyPem = ` 14 | -----BEGIN PUBLIC KEY----- 15 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDDvekdPMHN3AYhm/vktJT+YJr7 16 | cI5DcsNKqdsx5DZX0gDuWFuIjzdwButrIYPNmRJ1G8ybDIF7oDW2eEpm5sMbL9zs 17 | 9ExXCdvqrn51qELbqj0XxtMTIpaCHFSI50PfPpTFV9Xt/hmyVwokoOXFlAEgCn+Q 18 | CgGs52bFoYMtyi+xEQIDAQAB 19 | -----END PUBLIC KEY----- 20 | ` 21 | 22 | var publicKey *rsa.PublicKey 23 | 24 | func init() { 25 | block, _ := pem.Decode([]byte(publicKeyPem)) 26 | key, err := x509.ParsePKIXPublicKey(block.Bytes) 27 | osutil.PanicErr(err) 28 | publicKey = key.(*rsa.PublicKey) 29 | } 30 | 31 | func RSAEncrypt(content string) string { 32 | bs, err := rsa.EncryptPKCS1v15(rand.Reader, publicKey, []byte(content)) 33 | osutil.PanicErr(err) 34 | return base64.StdEncoding.EncodeToString(bs) 35 | } 36 | -------------------------------------------------------------------------------- /cmd/sign.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/starudream/go-lib/cobra/v2" 5 | 6 | "github.com/starudream/miyoushe-task/job" 7 | ) 8 | 9 | var ( 10 | signCmd = cobra.NewCommand(func(c *cobra.Command) { 11 | c.Use = "sign" 12 | c.Short = "Run sign task" 13 | }) 14 | 15 | signForumCmd = cobra.NewCommand(func(c *cobra.Command) { 16 | c.Use = "forum " 17 | c.Short = "Miyoushe forum task" 18 | c.RunE = func(cmd *cobra.Command, args []string) error { 19 | _, err := job.SignForum(xGetAccount(args)) 20 | return err 21 | } 22 | }) 23 | 24 | signGameCmd = cobra.NewCommand(func(c *cobra.Command) { 25 | c.Use = "game " 26 | c.Short = "Miyoushe game award" 27 | c.RunE = func(cmd *cobra.Command, args []string) error { 28 | _, err := job.SignGame(xGetAccount(args)) 29 | return err 30 | } 31 | }) 32 | ) 33 | 34 | func init() { 35 | signCmd.AddCommand(signForumCmd) 36 | signCmd.AddCommand(signGameCmd) 37 | 38 | rootCmd.AddCommand(signCmd) 39 | } 40 | -------------------------------------------------------------------------------- /api/miyoushe/sign_forum.go: -------------------------------------------------------------------------------- 1 | package miyoushe 2 | 3 | import ( 4 | "github.com/starudream/go-lib/core/v2/gh" 5 | 6 | "github.com/starudream/miyoushe-task/api/common" 7 | "github.com/starudream/miyoushe-task/config" 8 | ) 9 | 10 | type SignForumData struct { 11 | Points int `json:"points"` 12 | } 13 | 14 | func SignForum(gameId string, account config.Account, verification *common.Verification) (*SignForumData, error) { 15 | req := common.R(account.Device, verification).SetCookies(common.SToken(account)).SetBody(gh.M{"gids": gameId}) 16 | return common.Exec[*SignForumData](req, "POST", AddrBBS+"/apihub/app/api/signIn", 2) 17 | } 18 | 19 | type GetSignForumData struct { 20 | IsSigned bool `json:"is_signed"` 21 | } 22 | 23 | func GetSignForum(gameId string, account config.Account) (*GetSignForumData, error) { 24 | req := common.R(account.Device).SetCookies(common.SToken(account)).SetQueryParam("gids", gameId) 25 | return common.Exec[*GetSignForumData](req, "GET", AddrBBS+"/apihub/sapi/querySignInStatus") 26 | } 27 | -------------------------------------------------------------------------------- /api/miyoushe/verification.go: -------------------------------------------------------------------------------- 1 | package miyoushe 2 | 3 | import ( 4 | "github.com/starudream/go-lib/core/v2/gh" 5 | 6 | "github.com/starudream/miyoushe-task/api/common" 7 | "github.com/starudream/miyoushe-task/config" 8 | ) 9 | 10 | func CreateVerification(account config.Account) (*common.AigisData, error) { 11 | req := common.R(account.Device).SetCookies(common.SToken(account)).SetQueryParam("is_high", "true") 12 | return common.Exec[*common.AigisData](req, "GET", AddrBBS+"/misc/api/createVerification") 13 | } 14 | 15 | type VerifyVerificationData struct { 16 | Challenge string `json:"challenge"` 17 | } 18 | 19 | func VerifyVerification(challenge, validate string, account config.Account) (*VerifyVerificationData, error) { 20 | body := gh.M{"geetest_challenge": challenge, "geetest_seccode": validate + "|jordan", "geetest_validate": validate} 21 | req := common.R(account.Device).SetCookies(common.SToken(account)).SetBody(body) 22 | return common.Exec[*VerifyVerificationData](req, "POST", AddrBBS+"/misc/api/verifyVerification") 23 | } 24 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release 2 | 3 | permissions: 4 | contents: write 5 | packages: write 6 | 7 | on: 8 | push: 9 | tags: 10 | - '*' 11 | 12 | jobs: 13 | release: 14 | name: Release 15 | runs-on: ubuntu-latest 16 | steps: 17 | - uses: actions/checkout@v4 18 | with: 19 | fetch-depth: 0 20 | - uses: actions/setup-go@v5 21 | with: 22 | go-version: stable 23 | - uses: docker/setup-qemu-action@v3 24 | with: 25 | platforms: all 26 | - uses: docker/login-action@v3 27 | with: 28 | username: starudream 29 | password: ${{ secrets.DOCKER_TOKEN }} 30 | - uses: docker/login-action@v3 31 | with: 32 | registry: ghcr.io 33 | username: starudream 34 | password: ${{ secrets.GITHUB_TOKEN }} 35 | - uses: goreleaser/goreleaser-action@v6 36 | env: 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | with: 39 | args: release --clean 40 | -------------------------------------------------------------------------------- /api/miyoushe/game_test.go: -------------------------------------------------------------------------------- 1 | package miyoushe 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/starudream/go-lib/core/v2/codec/json" 7 | "github.com/starudream/go-lib/core/v2/utils/testutil" 8 | 9 | "github.com/starudream/miyoushe-task/api/common" 10 | "github.com/starudream/miyoushe-task/config" 11 | ) 12 | 13 | func TestListGame(t *testing.T) { 14 | data, err := ListGame() 15 | testutil.LogNoErr(t, err, data) 16 | testutil.Equal(t, json.MustMarshalString(AllGames), json.MustMarshalString(data.List)) 17 | } 18 | 19 | func TestListGameRole(t *testing.T) { 20 | t.Run("all", func(t *testing.T) { 21 | data, err := ListGameRole("", config.C().FirstAccount()) 22 | testutil.LogNoErr(t, err, data) 23 | }) 24 | 25 | t.Run(common.GameBizSRCN, func(t *testing.T) { 26 | data, err := ListGameRole(common.GameBizSRCN, config.C().FirstAccount()) 27 | testutil.LogNoErr(t, err, data) 28 | }) 29 | } 30 | 31 | func TestListGameCard(t *testing.T) { 32 | data, err := ListGameCard(config.C().FirstAccount()) 33 | testutil.LogNoErr(t, err, data) 34 | } 35 | -------------------------------------------------------------------------------- /api/common/resp.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/starudream/go-lib/resty/v2" 7 | ) 8 | 9 | type BaseResp[T any] struct { 10 | RetCode *int `json:"retcode"` 11 | Message string `json:"message"` 12 | Data T `json:"data"` 13 | } 14 | 15 | func (t *BaseResp[T]) GetRetCode() int { 16 | if t == nil || t.RetCode == nil { 17 | return 999999 18 | } 19 | return *t.RetCode 20 | } 21 | 22 | func (t *BaseResp[T]) IsSuccess() bool { 23 | return t != nil && t.RetCode != nil && *t.RetCode == 0 24 | } 25 | 26 | func (t *BaseResp[T]) String() string { 27 | if t == nil || t.RetCode == nil { 28 | return "" 29 | } 30 | return fmt.Sprintf("retcode: %d, message: %s, data: %v", *t.RetCode, t.Message, t.Data) 31 | } 32 | 33 | func IsRetCode(err error, rc int) bool { 34 | if err == nil { 35 | return false 36 | } 37 | e, ok1 := resty.AsRespErr(err) 38 | if ok1 { 39 | t, ok2 := e.Result().(interface{ GetRetCode() int }) 40 | if ok2 { 41 | return t.GetRetCode() == rc 42 | } 43 | } 44 | return false 45 | } 46 | -------------------------------------------------------------------------------- /api/miyoushe/user.go: -------------------------------------------------------------------------------- 1 | package miyoushe 2 | 3 | import ( 4 | "github.com/starudream/go-lib/core/v2/gh" 5 | "github.com/starudream/go-lib/resty/v2" 6 | 7 | "github.com/starudream/miyoushe-task/api/common" 8 | "github.com/starudream/miyoushe-task/config" 9 | ) 10 | 11 | func Login(account config.Account) error { 12 | req := common.R(account.Device).SetCookies(common.SToken(account)).SetBody(gh.M{"source_id": "", "source_key": "", "source_name": "", "source_type": 0}) 13 | _, err := common.Exec[any](req, "POST", AddrBBS+"/user/api/login") 14 | return err 15 | } 16 | 17 | type GetUserData struct { 18 | UserInfo *UserInfo `json:"user_info"` 19 | } 20 | 21 | type UserInfo struct { 22 | Uid string `json:"uid"` 23 | Nickname string `json:"nickname"` 24 | Introduce string `json:"introduce"` 25 | } 26 | 27 | func GetUser(uid string, account ...config.Account) (*GetUserData, error) { 28 | var req *resty.Request 29 | if len(account) == 0 { 30 | req = common.R().SetQueryParam("uid", uid) 31 | } else { 32 | req = common.R(account[0].Device).SetCookies(common.SToken(account[0])) 33 | } 34 | return common.Exec[*GetUserData](req, "GET", AddrBBS+"/user/api/getUserFullInfo") 35 | } 36 | -------------------------------------------------------------------------------- /api/miyoushe/game_preset.go: -------------------------------------------------------------------------------- 1 | package miyoushe 2 | 3 | import ( 4 | "strconv" 5 | 6 | "github.com/starudream/go-lib/core/v2/codec/json" 7 | ) 8 | 9 | var AllGames = json.MustUnmarshalTo[[]*Game](` 10 | [ 11 | { 12 | "id": 1, 13 | "name": "崩坏3", 14 | "en_name": "bh3", 15 | "op_name": "bh3" 16 | }, 17 | { 18 | "id": 2, 19 | "name": "原神", 20 | "en_name": "ys", 21 | "op_name": "hk4e" 22 | }, 23 | { 24 | "id": 3, 25 | "name": "崩坏学园2", 26 | "en_name": "bh2", 27 | "op_name": "bh2" 28 | }, 29 | { 30 | "id": 4, 31 | "name": "未定事件簿", 32 | "en_name": "wd", 33 | "op_name": "nxx" 34 | }, 35 | { 36 | "id": 5, 37 | "name": "大别野", 38 | "en_name": "dby", 39 | "op_name": "plat" 40 | }, 41 | { 42 | "id": 6, 43 | "name": "崩坏:星穹铁道", 44 | "en_name": "sr", 45 | "op_name": "hkrpg" 46 | }, 47 | { 48 | "id": 8, 49 | "name": "绝区零", 50 | "en_name": "zzz", 51 | "op_name": "nap" 52 | } 53 | ] 54 | `) 55 | 56 | var AllGamesById = func() map[string]*Game { 57 | m := map[string]*Game{} 58 | for i := range AllGames { 59 | m[strconv.Itoa(AllGames[i].Id)] = AllGames[i] 60 | } 61 | return m 62 | }() 63 | -------------------------------------------------------------------------------- /api/miyoushe/home_test.go: -------------------------------------------------------------------------------- 1 | package miyoushe 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/starudream/go-lib/core/v2/utils/testutil" 7 | 8 | "github.com/starudream/miyoushe-task/api/common" 9 | "github.com/starudream/miyoushe-task/config" 10 | ) 11 | 12 | func TestGetHome(t *testing.T) { 13 | t.Run(common.GameIdDBY, func(t *testing.T) { 14 | data, err := GetHome(common.GameIdDBY, config.C().FirstAccount()) 15 | testutil.LogNoErr(t, err, data) 16 | }) 17 | 18 | t.Run(common.GameIdYS, func(t *testing.T) { 19 | data, err := GetHome(common.GameIdYS, config.C().FirstAccount()) 20 | testutil.LogNoErr(t, err, data, data.GetSignActId()) 21 | }) 22 | 23 | t.Run(common.GameIdSR, func(t *testing.T) { 24 | data, err := GetHome(common.GameIdSR, config.C().FirstAccount()) 25 | testutil.LogNoErr(t, err, data, data.GetSignActId()) 26 | }) 27 | 28 | t.Run(common.GameIdBH3, func(t *testing.T) { 29 | data, err := GetHome(common.GameIdBH3, config.C().FirstAccount()) 30 | testutil.LogNoErr(t, err, data, data.GetSignActId()) 31 | }) 32 | } 33 | 34 | func TestGetBusinesses(t *testing.T) { 35 | data, err := GetBusinesses(config.C().FirstAccount()) 36 | testutil.LogNoErr(t, err, data) 37 | } 38 | -------------------------------------------------------------------------------- /api/ocr/rr.go: -------------------------------------------------------------------------------- 1 | package ocr 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/starudream/go-lib/core/v2/gh" 7 | "github.com/starudream/go-lib/resty/v2" 8 | 9 | "github.com/starudream/miyoushe-task/api/common" 10 | ) 11 | 12 | type rrResp struct { 13 | Status int `json:"status"` 14 | Msg string `json:"msg"` 15 | Code int `json:"code,omitempty"` 16 | Time int `json:"time,omitempty"` 17 | Data *common.Verification `json:"data,omitempty"` 18 | } 19 | 20 | func (t *rrResp) IsSuccess() bool { 21 | return t.Status == 0 22 | } 23 | 24 | func (t *rrResp) String() string { 25 | return fmt.Sprintf("status: %d, msg: %s, code: %d", t.Status, t.Msg, t.Code) 26 | } 27 | 28 | func RR(key, gt, challenge, refer string) (*common.Verification, error) { 29 | form := gh.MS{"appkey": key, "gt": gt, "challenge": challenge, "referer": refer} 30 | //goland:noinspection HttpUrlsUsage 31 | res, err := resty.ParseResp[*rrResp, *rrResp]( 32 | resty.R().SetError(&rrResp{}).SetResult(&rrResp{}).SetFormData(form).Post("http://api.rrocr.com/api/recognize.html"), 33 | ) 34 | if err != nil { 35 | return nil, fmt.Errorf("[rrocr] %w", err) 36 | } 37 | return res.Data, nil 38 | } 39 | -------------------------------------------------------------------------------- /api/common/const.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | const ( 4 | // https://github.com/UIGF-org/mihoyo-api-collect/blob/3a9116ea538941cfead749572df1f364cb9f9c8d/other/id.md#%E6%B8%B8%E6%88%8Fid 5 | 6 | GameIdBH3 = "1" // 崩坏3 7 | GameIdYS = "2" // 原神 8 | GameIdBH2 = "3" // 崩坏学园2 9 | GameIdWD = "4" // 未定事件簿 10 | GameIdDBY = "5" // 大别野 11 | GameIdSR = "6" // 崩坏:星穹铁道 12 | GameIdZZZ = "8" // 绝区零 13 | 14 | GameNameBH3 = "bh3" 15 | GameNameYS = "hk4e" 16 | GameNameBH2 = "bh2" 17 | GameNameWD = "nxx" 18 | GameNameDBY = "plat" 19 | GameNameSR = "hkrpg" 20 | GameNameZZZ = "nap" 21 | 22 | // https://github.com/UIGF-org/mihoyo-api-collect/blob/3a9116ea538941cfead749572df1f364cb9f9c8d/other/id.md#%E6%B8%B8%E6%88%8F%E6%A0%87%E8%AF%86%E7%AC%A6 23 | 24 | GameBizBH3CN = GameNameBH3 + "_" + cn 25 | GameBizYSCN = GameNameYS + "_" + cn 26 | GameBizBH2CN = GameNameBH2 + "_" + cn 27 | GameBizWDCN = GameNameWD + "_" + cn 28 | GameBizSRCN = GameNameSR + "_" + cn 29 | GameBizZZZCN = GameNameZZZ + "_" + cn 30 | 31 | cn = "cn" 32 | 33 | // https://github.com/UIGF-org/mihoyo-api-collect/blob/3a9116ea538941cfead749572df1f364cb9f9c8d/other/id.md#%E8%AE%BA%E5%9D%9Bid 34 | 35 | ForumIdSR = "53" 36 | 37 | XRpcSignGame = "x-rpc-signgame" 38 | ) 39 | -------------------------------------------------------------------------------- /api/miyoushe/forum_test.go: -------------------------------------------------------------------------------- 1 | package miyoushe 2 | 3 | import ( 4 | "testing" 5 | 6 | "github.com/starudream/go-lib/core/v2/utils/testutil" 7 | 8 | "github.com/starudream/miyoushe-task/api/common" 9 | "github.com/starudream/miyoushe-task/config" 10 | ) 11 | 12 | func TestListPost(t *testing.T) { 13 | data, err := ListPost(common.ForumIdSR, "", config.C().FirstAccount()) 14 | testutil.LogNoErr(t, err, data, data.LastId) 15 | } 16 | 17 | func TestListFeedPost(t *testing.T) { 18 | data, err := ListFeedPost(common.GameIdDBY, config.C().FirstAccount()) 19 | testutil.LogNoErr(t, err, data, data.LastId) 20 | } 21 | 22 | func TestGetPost(t *testing.T) { 23 | data, err := GetPost("45453046", config.C().FirstAccount()) 24 | testutil.LogNoErr(t, err, data, data.Post.IsUpvote()) 25 | } 26 | 27 | func TestUpvotePost(t *testing.T) { 28 | err := UpvotePost("45453046", false, config.C().FirstAccount()) 29 | testutil.LogNoErr(t, err) 30 | } 31 | 32 | func TestCollectPost(t *testing.T) { 33 | err := CollectPost("45453046", true, config.C().FirstAccount()) 34 | testutil.LogNoErr(t, err) 35 | } 36 | 37 | func TestSharePost(t *testing.T) { 38 | data, err := SharePost("45453046", config.C().FirstAccount()) 39 | testutil.LogNoErr(t, err, data) 40 | } 41 | -------------------------------------------------------------------------------- /api/miyoushe/home.go: -------------------------------------------------------------------------------- 1 | package miyoushe 2 | 3 | import ( 4 | "net/url" 5 | "strings" 6 | 7 | "github.com/starudream/miyoushe-task/api/common" 8 | "github.com/starudream/miyoushe-task/config" 9 | ) 10 | 11 | type Home struct { 12 | Navigator []*HomeNav `json:"navigator"` 13 | Official *HomeOfficial `json:"official"` 14 | } 15 | 16 | type HomeNav struct { 17 | Id int `json:"id"` 18 | Name string `json:"name"` 19 | AppPath string `json:"app_path"` 20 | } 21 | 22 | type HomeOfficial struct { 23 | ForumId int `json:"forum_id"` 24 | } 25 | 26 | func (h *Home) GetSignActId() string { 27 | for _, nav := range h.Navigator { 28 | if strings.Contains(nav.Name, "签到") { 29 | u, err := url.Parse(nav.AppPath) 30 | if err == nil { 31 | return u.Query().Get("act_id") 32 | } 33 | } 34 | } 35 | return "" 36 | } 37 | 38 | func GetHome(gameId string, account config.Account) (*Home, error) { 39 | req := common.R(account.Device).SetQueryParam("gids", gameId) 40 | return common.Exec[*Home](req, "GET", AddrBBS+"/apihub/api/home/new") 41 | } 42 | 43 | type GetBusinessesData struct { 44 | Businesses []string `json:"businesses"` 45 | } 46 | 47 | func GetBusinesses(account config.Account) (*GetBusinessesData, error) { 48 | req := common.R(account.Device).SetCookies(common.SToken(account)).SetQueryParam("uid", account.Uid) 49 | return common.Exec[*GetBusinessesData](req, "GET", AddrBBS+"/user/api/getUserBusinesses") 50 | } 51 | -------------------------------------------------------------------------------- /job/qrcode.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/starudream/go-lib/core/v2/slog" 8 | 9 | "github.com/starudream/miyoushe-task/api/common" 10 | "github.com/starudream/miyoushe-task/api/mihoyo" 11 | "github.com/starudream/miyoushe-task/config" 12 | ) 13 | 14 | func WaitQRCodeConfirmed(ticket string, account config.Account) (config.Account, error) { 15 | ticker := time.NewTicker(3 * time.Second) 16 | 17 | for { 18 | <-ticker.C 19 | res, err := mihoyo.QueryQRCode(ticket, account) 20 | if err != nil { 21 | if common.IsRetCode(err, common.RetCodeQRCodeExpired) { 22 | return account, fmt.Errorf("qrcode expired, please try again") 23 | } 24 | return account, fmt.Errorf("query qrcode error: %w", err) 25 | } 26 | 27 | switch { 28 | case res.Stat.IsInit(): 29 | slog.Info("qrcode not scanned") 30 | continue 31 | case res.Stat.IsScanned(): 32 | slog.Info("qrcode scanned, please confirm login") 33 | continue 34 | case res.Stat.IsConfirmed(): 35 | slog.Info("qrcode confirmed, login success") 36 | default: 37 | return account, fmt.Errorf("unknown qrcode stat: %s", res.Stat) 38 | } 39 | 40 | account.Uid = res.Payload.Uid 41 | account.GToken = res.Payload.Token 42 | 43 | config.UpdateAccount(account.Phone, func(config.Account) config.Account { 44 | return account 45 | }) 46 | err = config.Save() 47 | if err != nil { 48 | return account, fmt.Errorf("save account error: %w", err) 49 | } 50 | 51 | return account, nil 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /api/common/sign.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "crypto/md5" 5 | "encoding/hex" 6 | "fmt" 7 | "math/rand" 8 | "time" 9 | 10 | "github.com/starudream/go-lib/core/v2/codec/json" 11 | "github.com/starudream/go-lib/resty/v2" 12 | ) 13 | 14 | const ( 15 | // AppSaltK2 https://blog.starudream.cn/2023/11/09/miyoushe-salt-2.62.2/ 16 | AppSaltK2 = "QVu5OdwEWxkq9ygpYBgDprR5tI471HWQ" // 2.81.1 17 | // AppSalt6X https://github.com/UIGF-org/mihoyo-api-collect/issues/1 18 | AppSalt6X = "t0qEgfub6cvueAPgR5m9aQWWVciEer7v" 19 | ) 20 | 21 | // DS1 https://github.com/UIGF-org/mihoyo-api-collect/blob/3a9116ea538941cfead749572df1f364cb9f9c8d/other/authentication.md#ds 22 | func DS1() string { 23 | _, s := ds1(time.Now().Unix(), alphanum(6)) 24 | return s 25 | } 26 | 27 | func ds1(t int64, r string) (string, string) { 28 | s := fmt.Sprintf("salt=%s&t=%d&r=%s", AppSaltK2, t, r) 29 | b := md5.Sum([]byte(s)) 30 | return s, fmt.Sprintf("%d,%s,%s", t, r, hex.EncodeToString(b[:])) 31 | } 32 | 33 | // DS2 https://github.com/UIGF-org/mihoyo-api-collect/blob/3a9116ea538941cfead749572df1f364cb9f9c8d/other/authentication.md#ds 34 | func DS2(q *resty.Request) string { 35 | _, s := ds2(time.Now().Unix(), 100000+rand.Intn(100000), json.MustMarshalString(q.Body), q.QueryParam.Encode()) 36 | return s 37 | } 38 | 39 | func ds2(t int64, r int, body, query string) (string, string) { 40 | if r == 100000 { 41 | r += 542367 42 | } 43 | s := fmt.Sprintf("salt=%s&t=%d&r=%d&b=%s&q=%s", AppSalt6X, t, r, body, query) 44 | b := md5.Sum([]byte(s)) 45 | return s, fmt.Sprintf("%d,%d,%s", t, r, hex.EncodeToString(b[:])) 46 | } 47 | -------------------------------------------------------------------------------- /job/auth.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/starudream/miyoushe-task/api/mihoyo" 7 | "github.com/starudream/miyoushe-task/api/miyoushe" 8 | "github.com/starudream/miyoushe-task/config" 9 | ) 10 | 11 | func RefreshSTokenAuto(account config.Account) (config.Account, error) { 12 | _, err := miyoushe.GetUser("", account) 13 | if err != nil { 14 | if account.GToken == "" { 15 | return account, fmt.Errorf("get user error: %w", err) 16 | } 17 | return RefreshSToken(account) 18 | } 19 | return account, nil 20 | } 21 | 22 | func RefreshSToken(account config.Account) (config.Account, error) { 23 | res, err := mihoyo.GetSTokenByGToken(account) 24 | if err != nil { 25 | return account, fmt.Errorf("get stoken error: %w", err) 26 | } 27 | account.Mid = res.UserInfo.Mid 28 | account.SToken = res.Token.Token 29 | 30 | config.UpdateAccount(account.Phone, func(config.Account) config.Account { return account }) 31 | err = config.Save() 32 | if err != nil { 33 | return account, fmt.Errorf("save account error: %w", err) 34 | } 35 | 36 | return account, nil 37 | } 38 | 39 | func RefreshCToken(account config.Account) (config.Account, error) { 40 | res, err := mihoyo.GetCTokenBySToken(account) 41 | if err != nil { 42 | return account, fmt.Errorf("get ctoken error: %w", err) 43 | } 44 | account.CToken = res.CookieToken 45 | 46 | config.UpdateAccount(account.Phone, func(config.Account) config.Account { return account }) 47 | err = config.Save() 48 | if err != nil { 49 | return account, fmt.Errorf("save account error: %w", err) 50 | } 51 | 52 | return account, nil 53 | } 54 | -------------------------------------------------------------------------------- /config/account.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "strings" 5 | 6 | "github.com/kr/pretty" 7 | 8 | "github.com/starudream/go-lib/core/v2/slog" 9 | ) 10 | 11 | type Account struct { 12 | Phone string `json:"phone" yaml:"phone"` 13 | Device Device `json:"device" yaml:"device"` 14 | 15 | Uid string `json:"uid" yaml:"uid"` 16 | GToken string `json:"gtoken" yaml:"gtoken"` 17 | CToken string `json:"ctoken" yaml:"ctoken" table:",ignore"` 18 | 19 | Mid string `json:"mid" yaml:"mid"` 20 | SToken string `json:"stoken" yaml:"stoken" table:",ignore"` 21 | 22 | SignGameIds []string `json:"sign_game_ids" yaml:"sign_game_ids" table:",ignore"` 23 | } 24 | 25 | func AddAccount(account Account) { 26 | _cMu.Lock() 27 | defer _cMu.Unlock() 28 | u := false 29 | for i := range _c.Accounts { 30 | if _c.Accounts[i].Phone == account.Phone { 31 | _c.Accounts[i], u = account, true 32 | } 33 | } 34 | if !u { 35 | _c.Accounts = append(_c.Accounts, account) 36 | } 37 | } 38 | 39 | func UpdateAccount(phone string, cb func(account Account) Account) { 40 | _cMu.Lock() 41 | defer _cMu.Unlock() 42 | for i := range _c.Accounts { 43 | if _c.Accounts[i].Phone == phone { 44 | c := _c.Accounts[i] 45 | nc := cb(c) 46 | slog.Info("update account %s, diff: %s", phone, strings.Join(pretty.Diff(c, nc), ", ")) 47 | _c.Accounts[i] = nc 48 | return 49 | } 50 | } 51 | } 52 | 53 | func GetAccount(phone string) (Account, bool) { 54 | accounts := C().Accounts 55 | for i := range accounts { 56 | if accounts[i].Phone == phone { 57 | return accounts[i], true 58 | } 59 | } 60 | return Account{}, false 61 | } 62 | -------------------------------------------------------------------------------- /job/verfication.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/starudream/go-lib/core/v2/slog" 7 | 8 | "github.com/starudream/miyoushe-task/api/common" 9 | "github.com/starudream/miyoushe-task/api/miyoushe" 10 | "github.com/starudream/miyoushe-task/api/ocr" 11 | "github.com/starudream/miyoushe-task/config" 12 | ) 13 | 14 | func Verify(account config.Account) (verification *common.Verification, _ error) { 15 | res, err := miyoushe.CreateVerification(account) 16 | if err != nil { 17 | return nil, fmt.Errorf("create verification error: %w", err) 18 | } 19 | 20 | verification, err = DM(res.Gt, res.Challenge) 21 | if err != nil { 22 | return nil, err 23 | } 24 | 25 | _, err = miyoushe.VerifyVerification(verification.Challenge, verification.Validate, account) 26 | if err != nil { 27 | return nil, fmt.Errorf("verify verification error: %w", err) 28 | } 29 | 30 | return verification, nil 31 | } 32 | 33 | func DM(gt, challenge string) (*common.Verification, error) { 34 | validate, err := dm(gt, challenge) 35 | if err != nil { 36 | return nil, fmt.Errorf("dm error: %w", err) 37 | } 38 | if validate == nil { 39 | return nil, fmt.Errorf("dm is not configured") 40 | } 41 | slog.Info("verification code has been sent to dm and verify success") 42 | return validate, nil 43 | } 44 | 45 | func dm(gt, challenge string) (*common.Verification, error) { 46 | if key := config.TT().Key; key != "" { 47 | slog.Info("attempt to dm using ttocr, please wait a moment") 48 | return ocr.TT(key, gt, challenge, common.RefererAct) 49 | } 50 | if key := config.C().RROCRKey; key != "" { 51 | slog.Info("attempt to dm using rrocr, please wait a moment") 52 | return ocr.RR(key, gt, challenge, common.RefererAct) 53 | } 54 | return nil, nil 55 | } 56 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/starudream/miyoushe-task 2 | 3 | go 1.24.0 4 | 5 | require ( 6 | github.com/starudream/go-lib/cobra/v2 v2.0.21 7 | github.com/starudream/go-lib/core/v2 v2.1.12 8 | github.com/starudream/go-lib/cron/v2 v2.0.21 9 | github.com/starudream/go-lib/ntfy/v2 v2.0.24 10 | github.com/starudream/go-lib/resty/v2 v2.0.23 11 | github.com/starudream/go-lib/service/v2 v2.0.16 12 | github.com/starudream/go-lib/tablew/v2 v2.0.9 13 | ) 14 | 15 | require ( 16 | github.com/google/uuid v1.6.0 17 | github.com/kr/pretty v0.3.1 18 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e 19 | ) 20 | 21 | require ( 22 | github.com/go-resty/resty/v2 v2.16.5 // indirect 23 | github.com/go-viper/mapstructure/v2 v2.4.0 // indirect 24 | github.com/goccy/go-json v0.10.5 // indirect 25 | github.com/goccy/go-yaml v1.18.0 // indirect 26 | github.com/inconshreveable/mousetrap v1.1.0 // indirect 27 | github.com/kardianos/service v1.2.4 // indirect 28 | github.com/knadh/koanf/maps v0.1.2 // indirect 29 | github.com/knadh/koanf/v2 v2.2.2 // indirect 30 | github.com/kr/text v0.2.0 // indirect 31 | github.com/lmittmann/tint v1.1.2 // indirect 32 | github.com/mattn/go-colorable v0.1.14 // indirect 33 | github.com/mattn/go-isatty v0.0.20 // indirect 34 | github.com/mattn/go-runewidth v0.0.16 // indirect 35 | github.com/mitchellh/copystructure v1.2.0 // indirect 36 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 37 | github.com/rivo/uniseg v0.4.7 // indirect 38 | github.com/robfig/cron/v3 v3.0.1 // indirect 39 | github.com/rogpeppe/go-internal v1.14.1 // indirect 40 | github.com/samber/lo v1.51.0 // indirect 41 | github.com/spf13/cast v1.9.2 // indirect 42 | github.com/spf13/cobra v1.9.1 // indirect 43 | github.com/spf13/pflag v1.0.7 // indirect 44 | golang.org/x/net v0.42.0 // indirect 45 | golang.org/x/sys v0.34.0 // indirect 46 | golang.org/x/text v0.27.0 // indirect 47 | ) 48 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path/filepath" 7 | "sync" 8 | 9 | "github.com/starudream/go-lib/core/v2/codec/yaml" 10 | "github.com/starudream/go-lib/core/v2/config" 11 | "github.com/starudream/go-lib/core/v2/slog" 12 | "github.com/starudream/go-lib/core/v2/utils/osutil" 13 | ) 14 | 15 | type Config struct { 16 | Accounts []Account `json:"accounts" yaml:"accounts"` 17 | Cron Cron `json:"cron" yaml:"cron"` 18 | 19 | // 打码接口 20 | RROCRKey string `json:"rrocr.key" yaml:"rrocr.key"` 21 | TTOCR TTOCR `json:"ttocr" yaml:"ttocr"` 22 | } 23 | 24 | func (c Config) FirstAccount() Account { 25 | if len(c.Accounts) == 0 { 26 | osutil.PanicErr(fmt.Errorf("no account found")) 27 | } 28 | return c.Accounts[0] 29 | } 30 | 31 | type Cron struct { 32 | Spec string `json:"spec" yaml:"spec"` 33 | Startup bool `json:"startup" yaml:"startup"` 34 | } 35 | 36 | var ( 37 | _c = Config{ 38 | Cron: Cron{ 39 | Spec: "5 4 8 * * *", 40 | Startup: false, 41 | }, 42 | } 43 | _cMu = sync.Mutex{} 44 | ) 45 | 46 | func init() { 47 | _ = config.Unmarshal("", &_c) 48 | _ = config.Unmarshal("cron", &_c.Cron) 49 | _ = config.Unmarshal("ttocr", &_c.TTOCR) 50 | config.LoadStruct(_c) 51 | } 52 | 53 | func C() Config { 54 | _cMu.Lock() 55 | defer _cMu.Unlock() 56 | return _c 57 | } 58 | 59 | func Save() error { 60 | config.LoadStruct(_c) 61 | 62 | bs, err := yaml.Marshal(config.Raw()) 63 | if err != nil { 64 | return fmt.Errorf("marshal config error: %w", err) 65 | } 66 | 67 | filename := config.LoadedFile() 68 | if filename == "" { 69 | filename = filepath.Join(osutil.ExeDir(), osutil.ExeName()+".yaml") 70 | slog.Info("config file not found, save to default file", slog.String("file", filename)) 71 | } 72 | 73 | err = os.WriteFile(config.LoadedFile(), bs, 0644) 74 | if err != nil { 75 | return fmt.Errorf("write config file error: %w", err) 76 | } 77 | 78 | slog.Info("save config success", slog.String("file", filename)) 79 | 80 | return nil 81 | } 82 | -------------------------------------------------------------------------------- /cmd/cron.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | "fmt" 7 | 8 | "github.com/starudream/go-lib/cobra/v2" 9 | "github.com/starudream/go-lib/core/v2/slog" 10 | "github.com/starudream/go-lib/cron/v2" 11 | "github.com/starudream/go-lib/ntfy/v2" 12 | "github.com/starudream/go-lib/service/v2" 13 | 14 | "github.com/starudream/miyoushe-task/config" 15 | "github.com/starudream/miyoushe-task/job" 16 | ) 17 | 18 | var cronCmd = cobra.NewCommand(func(c *cobra.Command) { 19 | c.Use = "cron" 20 | c.Short = "Run as cron job" 21 | c.RunE = func(cmd *cobra.Command, args []string) error { 22 | return service.New("miyoushe-task", nil).Run() 23 | } 24 | }) 25 | 26 | func init() { 27 | rootCmd.AddCommand(cronCmd) 28 | } 29 | 30 | func cronRun() error { 31 | if config.C().Cron.Startup { 32 | cronJob() 33 | } 34 | err := cron.AddJob(config.C().Cron.Spec, "miyoushe-cron", cronJob) 35 | if err != nil { 36 | return fmt.Errorf("add cron job error: %w", err) 37 | } 38 | cron.Run() 39 | return nil 40 | } 41 | 42 | func cronJob() { 43 | for i := 0; i < len(config.C().Accounts); i++ { 44 | cronForumAccount(config.C().Accounts[i]) 45 | cronGameAccount(config.C().Accounts[i]) 46 | } 47 | } 48 | 49 | func cronForumAccount(account config.Account) (msg string) { 50 | record, err := job.SignForum(account) 51 | if err != nil { 52 | msg = fmt.Sprintf("%s: %v", record.Name(), err) 53 | slog.Error(msg) 54 | } else { 55 | msg = account.Phone + " " + record.Success() 56 | slog.Info(msg) 57 | } 58 | err = ntfy.Notify(context.Background(), msg) 59 | if err != nil && !errors.Is(err, ntfy.ErrNoConfig) { 60 | slog.Error("cron notify error: %v", err) 61 | } 62 | return 63 | } 64 | 65 | func cronGameAccount(account config.Account) (msg string) { 66 | records, err := job.SignGame(account) 67 | if err != nil { 68 | msg = fmt.Sprintf("%s: %v", records.Name(), err) 69 | slog.Error(msg) 70 | } else { 71 | msg = account.Phone + " " + records.Success() 72 | slog.Info(msg) 73 | } 74 | err = ntfy.Notify(context.Background(), msg) 75 | if err != nil && !errors.Is(err, ntfy.ErrNoConfig) { 76 | slog.Error("cron notify error: %v", err) 77 | } 78 | return 79 | } 80 | -------------------------------------------------------------------------------- /config/device.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "fmt" 5 | 6 | "github.com/starudream/miyoushe-task/util" 7 | ) 8 | 9 | type Device struct { 10 | Id string `json:"id,omitempty" yaml:"id,omitempty"` 11 | // https://github.com/UIGF-org/mihoyo-api-collect/blob/3a9116ea538941cfead749572df1f364cb9f9c8d/other/authentication.md#x-rpc-client_type 12 | Type string `json:"type,omitempty" yaml:"type,omitempty"` 13 | // https://github.com/UIGF-org/mihoyo-api-collect/blob/3a9116ea538941cfead749572df1f364cb9f9c8d/other/authentication.md#x-rpc-device_name 14 | Name string `json:"name,omitempty" yaml:"name,omitempty"` 15 | // https://github.com/UIGF-org/mihoyo-api-collect/blob/3a9116ea538941cfead749572df1f364cb9f9c8d/other/authentication.md#x-rpc-device_model 16 | Model string `json:"model,omitempty" yaml:"model,omitempty"` 17 | // https://github.com/UIGF-org/mihoyo-api-collect/blob/3a9116ea538941cfead749572df1f364cb9f9c8d/other/authentication.md#x-rpc-sys_version 18 | Version string `json:"version,omitempty" yaml:"version,omitempty"` 19 | // https://github.com/UIGF-org/mihoyo-api-collect/blob/3a9116ea538941cfead749572df1f364cb9f9c8d/other/authentication.md#x-rpc-channel 20 | Channel string `json:"channel,omitempty" yaml:"channel,omitempty"` 21 | } 22 | 23 | func (d Device) TableCellString() string { 24 | return fmt.Sprintf("%s (%s)", d.Id, d.Name) 25 | } 26 | 27 | var _d = Device{ 28 | Id: "", 29 | Type: "2", 30 | Name: "Xiaomi 22011211C", 31 | Model: "22011211C", 32 | Version: "13", 33 | Channel: "miyousheluodi", 34 | } 35 | 36 | func NewDevice() Device { 37 | d := _d 38 | d.Id = util.UUID() 39 | return d 40 | } 41 | 42 | // Headers https://github.com/UIGF-org/mihoyo-api-collect/blob/3a9116ea538941cfead749572df1f364cb9f9c8d/other/authentication.md#%E8%AF%B7%E6%B1%82%E5%A4%B4 43 | func (d Device) Headers() map[string]string { 44 | return map[string]string{ 45 | "x-rpc-device_id": def(d.Id, util.UUID()), 46 | "x-rpc-client_type": def(d.Type, _d.Type), 47 | "x-rpc-device_name": def(d.Name, _d.Name), 48 | "x-rpc-device_model": def(d.Model, _d.Model), 49 | "x-rpc-sys_version": def(d.Version, _d.Version), 50 | "x-rpc-channel": def(d.Channel, _d.Channel), 51 | } 52 | } 53 | 54 | func def(v string, def ...string) string { 55 | if v != "" { 56 | return v 57 | } 58 | if len(def) > 0 { 59 | return def[0] 60 | } 61 | return "" 62 | } 63 | -------------------------------------------------------------------------------- /api/miyoushe/sign_game_test.go: -------------------------------------------------------------------------------- 1 | package miyoushe 2 | 3 | import ( 4 | "strings" 5 | "testing" 6 | 7 | "github.com/starudream/go-lib/core/v2/utils/testutil" 8 | 9 | "github.com/starudream/miyoushe-task/api/common" 10 | "github.com/starudream/miyoushe-task/config" 11 | ) 12 | 13 | var gameIdByName = map[string]string{ 14 | common.GameNameBH3: common.GameIdBH3, 15 | common.GameNameYS: common.GameIdYS, 16 | common.GameNameBH2: common.GameIdBH2, 17 | common.GameNameWD: common.GameIdWD, 18 | common.GameNameSR: common.GameIdSR, 19 | common.GameNameZZZ: common.GameIdZZZ, 20 | } 21 | 22 | func GetRole(t *testing.T, gameBiz string) (string, string, string, string) { 23 | gameName := strings.Split(gameBiz, "_")[0] 24 | gameId := gameIdByName[gameName] 25 | 26 | data1, err := GetHome(gameId, config.C().FirstAccount()) 27 | testutil.LogNoErr(t, err, data1) 28 | actId := data1.GetSignActId() 29 | testutil.MustNotEqual(t, "", actId) 30 | 31 | data2, err := ListGameRole(gameBiz, config.C().FirstAccount()) 32 | testutil.LogNoErr(t, err, data2) 33 | testutil.MustNotEqual(t, 0, len(data2.List)) 34 | region, uid := data2.List[0].Region, data2.List[0].GameUid 35 | 36 | return gameName, actId, region, uid 37 | } 38 | 39 | var gameBiz = common.GameBizZZZCN 40 | 41 | func TestSignGame(t *testing.T) { 42 | gameName, actId, region, uid := GetRole(t, gameBiz) 43 | data, err := SignGame(gameName, actId, region, uid, config.C().FirstAccount(), nil) 44 | if common.IsRetCode(err, common.RetCodeGameHasSigned) { 45 | t.Skip("game has signed") 46 | } 47 | testutil.LogNoErr(t, err, data) 48 | testutil.Equal(t, false, data.IsRisky()) 49 | } 50 | 51 | func TestGetSignGame(t *testing.T) { 52 | gameName, actId, region, uid := GetRole(t, gameBiz) 53 | data, err := GetSignGame(gameName, actId, region, uid, config.C().FirstAccount()) 54 | testutil.LogNoErr(t, err, data) 55 | } 56 | 57 | func TestListSignGame(t *testing.T) { 58 | gameName, actId, _, _ := GetRole(t, gameBiz) 59 | data, err := ListSignGame(gameName, actId, config.C().FirstAccount()) 60 | testutil.LogNoErr(t, err, data) 61 | } 62 | 63 | func TestListSignGameAward(t *testing.T) { 64 | gameName, actId, region, uid := GetRole(t, gameBiz) 65 | data, err := ListSignGameAward(gameName, actId, region, uid, config.C().FirstAccount()) 66 | testutil.LogNoErr(t, err, data, len(data), data.Today(), data.Today().ShortString()) 67 | } 68 | -------------------------------------------------------------------------------- /api/mihoyo/token.go: -------------------------------------------------------------------------------- 1 | package mihoyo 2 | 3 | import ( 4 | "fmt" 5 | "strconv" 6 | 7 | "github.com/starudream/go-lib/core/v2/gh" 8 | 9 | "github.com/starudream/miyoushe-task/api/common" 10 | "github.com/starudream/miyoushe-task/config" 11 | ) 12 | 13 | type GetSTokenByGTokenData struct { 14 | Token *TokenInfo `json:"token"` 15 | UserInfo *UserInfo `json:"user_info"` 16 | } 17 | 18 | type TokenInfo struct { 19 | TokenType int `json:"token_type"` 20 | Token string `json:"token"` 21 | } 22 | 23 | type UserInfo struct { 24 | Aid string `json:"aid"` 25 | Mid string `json:"mid"` 26 | } 27 | 28 | // GetSTokenByGToken get stoken v2 by game token 29 | // https://github.com/UIGF-org/mihoyo-api-collect/blob/3a9116ea538941cfead749572df1f364cb9f9c8d/hoyolab/user/token.md#%E9%80%9A%E8%BF%87game-token%E8%8E%B7%E5%8F%96stokenv1 30 | func GetSTokenByGToken(account config.Account) (*GetSTokenByGTokenData, error) { 31 | uid, err := strconv.ParseInt(account.Uid, 10, 64) 32 | if err != nil { 33 | return nil, fmt.Errorf("parse uid error: %w", err) 34 | } 35 | req := common.R(account.Device).SetBody(gh.M{"account_id": uid, "game_token": account.GToken}) 36 | return common.Exec[*GetSTokenByGTokenData](req, "POST", AddrTakumi+"/account/ma-cn-session/app/getTokenByGameToken") 37 | } 38 | 39 | type GetCTokenBySTokenData struct { 40 | Uid string `json:"uid"` 41 | CookieToken string `json:"cookie_token"` 42 | } 43 | 44 | // GetCTokenBySToken get cookie token by stoken v2 45 | // https://github.com/UIGF-org/mihoyo-api-collect/blob/3a9116ea538941cfead749572df1f364cb9f9c8d/hoyolab/user/token.md#%E9%80%9A%E8%BF%87stoken%E8%8E%B7%E5%8F%96cookie-token 46 | func GetCTokenBySToken(account config.Account) (*GetCTokenBySTokenData, error) { 47 | req := common.R(account.Device).SetCookies(common.SToken(account)) 48 | return common.Exec[*GetCTokenBySTokenData](req, "GET", AddrTakumi+"/auth/api/getCookieAccountInfoBySToken") 49 | } 50 | 51 | type GetLTokenBySTokenData struct { 52 | LToken string `json:"ltoken"` 53 | } 54 | 55 | // GetLTokenBySToken get ltoken v1 by stoken v2 56 | // https://github.com/UIGF-org/mihoyo-api-collect/blob/3a9116ea538941cfead749572df1f364cb9f9c8d/hoyolab/user/token.md#%E9%80%9A%E8%BF%87stoken%E8%8E%B7%E5%8F%96ltokenv1 57 | func GetLTokenBySToken(account config.Account) (*GetLTokenBySTokenData, error) { 58 | req := common.R(account.Device).SetCookies(common.SToken(account)) 59 | return common.Exec[*GetLTokenBySTokenData](req, "GET", AddrTakumi+"/account/auth/api/getLTokenBySToken") 60 | } 61 | -------------------------------------------------------------------------------- /api/miyoushe/game.go: -------------------------------------------------------------------------------- 1 | package miyoushe 2 | 3 | import ( 4 | "cmp" 5 | "slices" 6 | 7 | "github.com/starudream/miyoushe-task/api/common" 8 | "github.com/starudream/miyoushe-task/config" 9 | ) 10 | 11 | type ListGameData struct { 12 | List []*Game `json:"list"` 13 | } 14 | 15 | type Game struct { 16 | Id int `json:"id"` 17 | Name string `json:"name"` 18 | EnName string `json:"en_name"` 19 | OpName string `json:"op_name"` 20 | } 21 | 22 | func ListGame() (*ListGameData, error) { 23 | data, err := common.Exec[*ListGameData](common.R(), "GET", AddrBBS+"/apihub/api/getGameList") 24 | if err != nil { 25 | return nil, err 26 | } 27 | slices.SortFunc(data.List, func(a, b *Game) int { return cmp.Compare(a.Id, b.Id) }) 28 | return data, nil 29 | } 30 | 31 | type ListGameRoleData struct { 32 | List []*GameRole `json:"list"` 33 | } 34 | 35 | type GameRole struct { 36 | GameBiz string `json:"game_biz"` 37 | Region string `json:"region"` 38 | GameUid string `json:"game_uid"` 39 | Nickname string `json:"nickname"` 40 | Level int `json:"level"` 41 | IsChosen bool `json:"is_chosen"` 42 | RegionName string `json:"region_name"` 43 | IsOfficial bool `json:"is_official"` 44 | } 45 | 46 | func ListGameRole(gameBiz string, account config.Account) (*ListGameRoleData, error) { 47 | req := common.R(account.Device).SetCookies(common.SToken(account)).SetQueryParam("game_biz", gameBiz) 48 | return common.Exec[*ListGameRoleData](req, "GET", AddrTakumi+"/binding/api/getUserGameRolesByStoken") 49 | } 50 | 51 | type ListGameCardData struct { 52 | List []*GameCard `json:"list"` 53 | } 54 | 55 | type GameCard struct { 56 | HasRole bool `json:"has_role"` 57 | IsPublic bool `json:"is_public"` 58 | GameId int `json:"game_id"` 59 | GameRoleId string `json:"game_role_id"` 60 | Region string `json:"region"` 61 | RegionName string `json:"region_name"` 62 | Level int `json:"level"` 63 | Nickname string `json:"nickname"` 64 | Data []*GameCardItem `json:"data"` 65 | } 66 | 67 | type GameCardItem struct { 68 | Type int `json:"type"` 69 | Name string `json:"name"` 70 | Value string `json:"value"` 71 | } 72 | 73 | func ListGameCard(account config.Account) (*ListGameCardData, error) { 74 | req := common.R(account.Device).SetCookies(common.SToken(account)).SetQueryParam("uid", account.Uid) 75 | return common.Exec[*ListGameCardData](req, "GET", AddrTakumiRecord+"/game_record/card/api/getGameRecordCard") 76 | } 77 | -------------------------------------------------------------------------------- /api/common/api.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | 7 | "github.com/starudream/go-lib/core/v2/codec/json" 8 | "github.com/starudream/go-lib/resty/v2" 9 | 10 | "github.com/starudream/miyoushe-task/config" 11 | ) 12 | 13 | const ( 14 | UserAgent = "Mozilla/5.0 (Linux; Android 13; 22011211C Build/TP1A.220624.014; wv) AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/104.0.5112.97 Mobile Safari/537.36 miHoYoBBS/" + AppVersion 15 | 16 | RefererApp = "https://app.mihoyo.com" 17 | RefererAct = "https://act.mihoyo.com" 18 | 19 | AppVersion = "2.81.1" 20 | AppIdMiyoushe = "bll8iq97cem8" // 米游社 21 | ) 22 | 23 | func R(vs ...any) *resty.Request { 24 | r := resty.R(). 25 | SetHeader("Accept-Encoding", "gzip"). 26 | SetHeader("User-Agent", UserAgent). 27 | SetHeader("Referer", RefererApp). 28 | SetHeader("x-rpc-app_version", AppVersion). 29 | SetHeader("x-rpc-app_id", AppIdMiyoushe). 30 | SetHeader("x-rpc-verify_key", AppIdMiyoushe) 31 | for i := 0; i < len(vs); i++ { 32 | switch v := vs[i].(type) { 33 | case config.Device: 34 | r.SetHeaders(v.Headers()) 35 | case *Verification: 36 | if v == nil || v.Challenge == "" || v.Validate == "" { 37 | continue 38 | } 39 | r.SetHeader("x-rpc-challenge", v.Challenge) 40 | r.SetHeader("x-rpc-validate", v.Validate) 41 | } 42 | } 43 | return r 44 | } 45 | 46 | func Exec[T any](r *resty.Request, method, url string, ds ...int) (t T, _ error) { 47 | if len(ds) == 0 || ds[0] <= 1 { 48 | r.SetHeader("DS", DS1()) 49 | } else if ds[0] == 2 { 50 | r.SetHeader("DS", DS2(r)) 51 | } 52 | res, err := resty.ParseResp[*BaseResp[any], *BaseResp[T]]( 53 | r.SetError(&BaseResp[any]{}).SetResult(&BaseResp[T]{}).Execute(method, url), 54 | ) 55 | if err != nil { 56 | return t, fmt.Errorf("[miyoushe] %w", err) 57 | } 58 | return res.Data, nil 59 | } 60 | 61 | func GetHeaders(err error) http.Header { 62 | if err != nil { 63 | e, ok := resty.AsRespErr(err) 64 | if ok { 65 | return e.Response.Header() 66 | } 67 | } 68 | return http.Header{} 69 | } 70 | 71 | type Aigis struct { 72 | SessionId string `json:"session_id"` 73 | MmtType int `json:"mmt_type"` 74 | Data string `json:"data"` 75 | } 76 | 77 | type AigisData struct { 78 | Challenge string `json:"challenge"` 79 | Gt string `json:"gt"` 80 | NewCaptcha int `json:"new_captcha"` 81 | Success int `json:"success"` 82 | } 83 | 84 | func GetAigisData(err error) (*Aigis, *AigisData) { 85 | s := GetHeaders(err).Get("x-rpc-aigis") 86 | if s != "" { 87 | a, e1 := json.UnmarshalTo[*Aigis](s) 88 | if e1 == nil { 89 | b, e2 := json.UnmarshalTo[*AigisData](a.Data) 90 | if e2 == nil { 91 | return a, b 92 | } 93 | } 94 | } 95 | return nil, nil 96 | } 97 | -------------------------------------------------------------------------------- /geetest.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | GeeTest 5 | 36 | 37 | 38 |
39 | 40 | 41 | 42 |
43 |
44 | 45 |
46 | 47 | 48 | 81 | 82 | 83 | -------------------------------------------------------------------------------- /api/ocr/tt.go: -------------------------------------------------------------------------------- 1 | package ocr 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | 7 | "github.com/starudream/go-lib/core/v2/gh" 8 | "github.com/starudream/go-lib/resty/v2" 9 | 10 | "github.com/starudream/miyoushe-task/api/common" 11 | "github.com/starudream/miyoushe-task/config" 12 | ) 13 | 14 | func TT(key, gt, challenge, refer string) (*common.Verification, error) { 15 | resultId, err := ttRecognize(key, gt, challenge, refer) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | vch := make(chan *common.Verification, 1) 21 | 22 | go func() { 23 | var data *common.Verification 24 | for { 25 | time.Sleep(config.TT().Interval) 26 | data, err = ttResult(key, resultId) 27 | if e, ok1 := resty.AsRespErr(err); ok1 { 28 | if v, ok2 := e.Response.Result().(interface{ GetStatus() int }); ok2 && v.GetStatus() == 2 { 29 | continue 30 | } 31 | } 32 | vch <- data 33 | return 34 | } 35 | }() 36 | 37 | select { 38 | case v := <-vch: 39 | return v, err 40 | case <-time.After(config.TT().Timeout): 41 | return nil, fmt.Errorf("[ttocr] 识别超时") 42 | } 43 | } 44 | 45 | type ttRecognizeResp struct { 46 | Status int `json:"status"` 47 | Msg string `json:"msg"` 48 | ResultId string `json:"resultid,omitempty"` 49 | } 50 | 51 | func (t *ttRecognizeResp) IsSuccess() bool { 52 | return t.Status == 1 53 | } 54 | 55 | func (t *ttRecognizeResp) String() string { 56 | return fmt.Sprintf("status: %d, msg: %s", t.Status, t.Msg) 57 | } 58 | 59 | func ttRecognize(key, gt, challenge, refer string) (string, error) { 60 | form := gh.MS{"appkey": key, "gt": gt, "challenge": challenge, "itemid": config.TT().ItemId, "referer": refer} 61 | res, err := resty.ParseResp[*ttRecognizeResp, *ttRecognizeResp]( 62 | resty.R().SetError(&ttRecognizeResp{}).SetResult(&ttRecognizeResp{}).SetFormData(form).Post("http://api.ttocr.com/api/recognize"), 63 | ) 64 | if err != nil { 65 | return "", fmt.Errorf("[ttocr] %w", err) 66 | } 67 | return res.ResultId, nil 68 | } 69 | 70 | type ttResultResp struct { 71 | Status int `json:"status"` 72 | Msg string `json:"msg"` 73 | Time int `json:"time,omitempty"` 74 | Data *common.Verification `json:"data,omitempty"` 75 | } 76 | 77 | func (t *ttResultResp) GetStatus() int { 78 | if t == nil { 79 | return 0 80 | } 81 | return t.Status 82 | } 83 | 84 | func (t *ttResultResp) IsSuccess() bool { 85 | return t.Status == 1 86 | } 87 | 88 | func (t *ttResultResp) String() string { 89 | return fmt.Sprintf("status: %d, msg: %s", t.Status, t.Msg) 90 | } 91 | 92 | func ttResult(key, resultId string) (*common.Verification, error) { 93 | form := gh.MS{"appkey": key, "resultid": resultId} 94 | res, err := resty.ParseResp[*ttResultResp, *ttResultResp]( 95 | resty.R().SetError(&ttResultResp{}).SetResult(&ttResultResp{}).SetFormData(form).Post("http://api.ttocr.com/api/results"), 96 | ) 97 | if err != nil { 98 | return nil, fmt.Errorf("[ttocr] %w", err) 99 | } 100 | return res.Data, nil 101 | } 102 | -------------------------------------------------------------------------------- /cmd/account.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | 7 | "github.com/starudream/go-lib/cobra/v2" 8 | "github.com/starudream/go-lib/core/v2/slog" 9 | "github.com/starudream/go-lib/core/v2/utils/fmtutil" 10 | "github.com/starudream/go-lib/core/v2/utils/sliceutil" 11 | "github.com/starudream/go-lib/tablew/v2" 12 | 13 | "github.com/starudream/miyoushe-task/api/common" 14 | "github.com/starudream/miyoushe-task/api/mihoyo" 15 | "github.com/starudream/miyoushe-task/config" 16 | "github.com/starudream/miyoushe-task/job" 17 | ) 18 | 19 | var ( 20 | accountCmd = cobra.NewCommand(func(c *cobra.Command) { 21 | c.Use = "account" 22 | c.Short = "Manage accounts" 23 | }) 24 | 25 | accountInitCmd = cobra.NewCommand(func(c *cobra.Command) { 26 | c.Use = "init " 27 | c.Short = "Init account device information" 28 | c.Args = func(cmd *cobra.Command, args []string) error { 29 | phone, _ := sliceutil.GetValue(args, 0) 30 | if phone == "" { 31 | return fmt.Errorf("requires account phone") 32 | } 33 | _, exists := config.GetAccount(phone) 34 | if exists { 35 | return fmt.Errorf("account %s already exists", phone) 36 | } 37 | return nil 38 | } 39 | c.RunE = func(cmd *cobra.Command, args []string) error { 40 | phone, _ := sliceutil.GetValue(args, 0) 41 | config.AddAccount(config.Account{Phone: phone, Device: config.NewDevice()}) 42 | return config.Save() 43 | } 44 | }) 45 | 46 | accountLoginCmd = cobra.NewCommand(func(c *cobra.Command) { 47 | c.Use = "login " 48 | c.Short = "Login account" 49 | c.RunE = func(cmd *cobra.Command, args []string) error { 50 | account := xGetAccount(args) 51 | 52 | _, err := mihoyo.SendPhoneCode("", account) 53 | if err != nil { 54 | if !common.IsRetCode(err, common.RetCodeSendPhoneCodeFrequently) { 55 | return fmt.Errorf("send phone code error: %w", err) 56 | } 57 | 58 | aigis, aigisData := common.GetAigisData(err) 59 | if aigis == nil || aigisData == nil { 60 | return fmt.Errorf("get aigis data empty") 61 | } 62 | 63 | slog.Info("aigis gt: %s, challenge: %s", aigisData.Gt, aigisData.Challenge) 64 | 65 | geetest := base64.StdEncoding.EncodeToString([]byte(fmtutil.Scan("please enter GeeTest json string: "))) 66 | 67 | _, err = mihoyo.SendPhoneCode(fmt.Sprintf("%s;%s", aigis.SessionId, geetest), account) 68 | if err != nil { 69 | return fmt.Errorf("send phone code error: %w", err) 70 | } 71 | } 72 | 73 | code := fmtutil.Scan("please enter the verification code you received (use ctrl+c to exit): ") 74 | if code == "" { 75 | return nil 76 | } 77 | 78 | res, err := mihoyo.LoginByPhoneCode(code, account) 79 | if err != nil { 80 | return fmt.Errorf("login by phone code error: %w", err) 81 | } 82 | 83 | account.Uid = res.UserInfo.Aid 84 | account.Mid = res.UserInfo.Mid 85 | account.SToken = res.Token.Token 86 | 87 | _, err = job.RefreshCToken(account) 88 | if err != nil { 89 | return fmt.Errorf("refresh token error: %w", err) 90 | } 91 | 92 | slog.Info("login success") 93 | return nil 94 | } 95 | }) 96 | 97 | accountListCmd = cobra.NewCommand(func(c *cobra.Command) { 98 | c.Use = "list" 99 | c.Short = "List accounts" 100 | c.Run = func(cmd *cobra.Command, args []string) { 101 | fmt.Println(tablew.Structs(config.C().Accounts)) 102 | } 103 | }) 104 | ) 105 | 106 | func init() { 107 | accountCmd.AddCommand(accountInitCmd) 108 | accountCmd.AddCommand(accountLoginCmd) 109 | accountCmd.AddCommand(accountListCmd) 110 | 111 | rootCmd.AddCommand(accountCmd) 112 | } 113 | -------------------------------------------------------------------------------- /api/mihoyo/auth.go: -------------------------------------------------------------------------------- 1 | package mihoyo 2 | 3 | import ( 4 | "fmt" 5 | "net/url" 6 | 7 | "github.com/starudream/go-lib/core/v2/codec/json" 8 | "github.com/starudream/go-lib/core/v2/gh" 9 | 10 | "github.com/starudream/miyoushe-task/api/common" 11 | "github.com/starudream/miyoushe-task/config" 12 | ) 13 | 14 | type GenQRCodeData struct { 15 | Url string `json:"url"` 16 | Ticket string `json:"ticket"` 17 | } 18 | 19 | // GenQRCode generate to login sr (app_id = 8) to get game token 20 | // https://github.com/UIGF-org/mihoyo-api-collect/blob/3a9116ea538941cfead749572df1f364cb9f9c8d/hoyolab/login/qrcode_hk4e.md#%E7%94%9F%E6%88%90%E4%BA%8C%E7%BB%B4%E7%A0%81 21 | func GenQRCode(account config.Account) (*GenQRCodeData, error) { 22 | req := common.R(account.Device).SetBody(gh.M{"app_id": "8", "device": account.Device.Id}) 23 | data, err := common.Exec[*GenQRCodeData](req, "POST", AddrHK4E+"/hk4e_cn/combo/panda/qrcode/fetch") 24 | if err != nil { 25 | return nil, err 26 | } 27 | u, err := url.Parse(data.Url) 28 | if err != nil { 29 | return nil, fmt.Errorf("parse url error: %w", err) 30 | } 31 | data.Ticket = u.Query().Get("ticket") 32 | return data, nil 33 | } 34 | 35 | type QueryQRCodeData struct { 36 | Stat QRCodeStat `json:"stat"` 37 | Payload *QueryQRCodePayload `json:"payload"` 38 | } 39 | 40 | type QueryQRCodePayload struct { 41 | Proto string `json:"proto"` 42 | Raw string `json:"raw,omitempty"` 43 | Uid string `json:"uid,omitempty"` 44 | Token string `json:"token,omitempty"` 45 | } 46 | 47 | type QRCodeStat string 48 | 49 | const ( 50 | QRCodeStatInit = "Init" 51 | QRCodeStatScanned = "Scanned" 52 | QRCodeStatConfirmed = "Confirmed" 53 | ) 54 | 55 | func (s QRCodeStat) IsInit() bool { 56 | return s == QRCodeStatInit 57 | } 58 | 59 | func (s QRCodeStat) IsScanned() bool { 60 | return s == QRCodeStatScanned 61 | } 62 | 63 | func (s QRCodeStat) IsConfirmed() bool { 64 | return s == QRCodeStatConfirmed 65 | } 66 | 67 | func QueryQRCode(ticket string, account config.Account) (*QueryQRCodeData, error) { 68 | req := common.R(account.Device).SetBody(gh.M{"app_id": "8", "device": account.Device.Id, "ticket": ticket}) 69 | data, err := common.Exec[*QueryQRCodeData](req, "POST", AddrHK4E+"/hk4e_cn/combo/panda/qrcode/query") 70 | if err != nil || !data.Stat.IsConfirmed() { 71 | return data, err 72 | } 73 | payload, err := json.UnmarshalTo[*QueryQRCodePayload](data.Payload.Raw) 74 | if err != nil { 75 | return nil, fmt.Errorf("unmarshal payload error: %w", err) 76 | } 77 | data.Payload.Uid = payload.Uid 78 | data.Payload.Token = payload.Token // game token 79 | return data, nil 80 | } 81 | 82 | const phoneAreaCodeCN = "+86" 83 | 84 | type SendPhoneCodeData struct { 85 | SentNew bool `json:"sent_new"` 86 | Countdown int `json:"countdown"` 87 | ActionType string `json:"action_type"` 88 | } 89 | 90 | func SendPhoneCode(aigis string, account config.Account) (*SendPhoneCodeData, error) { 91 | req := common.R(account.Device).SetBody(gh.M{ 92 | "area_code": common.RSAEncrypt(phoneAreaCodeCN), 93 | "mobile": common.RSAEncrypt(account.Phone), 94 | }) 95 | if aigis != "" { 96 | req.SetHeader("x-rpc-aigis", aigis) 97 | } 98 | return common.Exec[*SendPhoneCodeData](req, "POST", AddrPassport+"/account/ma-cn-verifier/verifier/createLoginCaptcha") 99 | } 100 | 101 | type LoginByPhoneCodeData struct { 102 | Token *TokenInfo `json:"token"` 103 | UserInfo *UserInfo `json:"user_info"` 104 | LoginTicket string `json:"login_ticket"` 105 | } 106 | 107 | const actionTypeLoginByMobileCaptcha = "login_by_mobile_captcha" 108 | 109 | func LoginByPhoneCode(code string, account config.Account) (*LoginByPhoneCodeData, error) { 110 | req := common.R(account.Device).SetBody(gh.M{ 111 | "area_code": common.RSAEncrypt(phoneAreaCodeCN), 112 | "mobile": common.RSAEncrypt(account.Phone), 113 | "action_type": actionTypeLoginByMobileCaptcha, 114 | "captcha": code, 115 | }) 116 | return common.Exec[*LoginByPhoneCodeData](req, "POST", AddrPassport+"/account/ma-cn-passport/app/loginByMobileCaptcha") 117 | } 118 | -------------------------------------------------------------------------------- /api/miyoushe/forum.go: -------------------------------------------------------------------------------- 1 | package miyoushe 2 | 3 | import ( 4 | "github.com/starudream/go-lib/core/v2/gh" 5 | 6 | "github.com/starudream/miyoushe-task/api/common" 7 | "github.com/starudream/miyoushe-task/config" 8 | ) 9 | 10 | type ListPostData struct { 11 | IsLast bool `json:"is_last"` 12 | IsOrigin bool `json:"is_origin"` 13 | LastId string `json:"last_id"` 14 | List []*PostData `json:"list"` 15 | } 16 | 17 | type PostData struct { 18 | Post *PostInfo `json:"post"` 19 | Stat *PostStat `json:"stat"` 20 | User *PostUser `json:"user"` 21 | SelfOperation *PostSelfOperation `json:"self_operation"` 22 | } 23 | 24 | func (p *PostData) IsUpvote() bool { 25 | return p != nil && p.SelfOperation != nil && p.SelfOperation.Attitude == 1 26 | } 27 | 28 | func (p *PostData) IsCollected() bool { 29 | return p != nil && p.SelfOperation != nil && p.SelfOperation.IsCollected 30 | } 31 | 32 | type PostInfo struct { 33 | PostId string `json:"post_id"` 34 | Subject string `json:"subject"` 35 | Content string `json:"content"` 36 | MaxFloor int `json:"max_floor"` 37 | ReplyTime string `json:"reply_time"` 38 | CreatedAt int `json:"created_at"` 39 | UpdatedAt int `json:"updated_at"` 40 | } 41 | 42 | type PostStat struct { 43 | BookmarkNum int `json:"bookmark_num"` 44 | ForwardNum int `json:"forward_num"` 45 | LikeNum int `json:"like_num"` 46 | ReplyNum int `json:"reply_num"` 47 | ViewNum int `json:"view_num"` 48 | } 49 | 50 | type PostUser struct { 51 | Uid string `json:"uid"` 52 | Nickname string `json:"nickname"` 53 | } 54 | 55 | type PostSelfOperation struct { 56 | Attitude int `json:"attitude"` 57 | IsCollected bool `json:"is_collected"` 58 | UpvoteType int `json:"upvote_type"` 59 | } 60 | 61 | func ListPost(forumId, lastId string, account config.Account) (*ListPostData, error) { 62 | query := gh.MS{"forum_id": forumId, "is_good": "false", "is_hot": "false", "sort_type": "1", "last_id": lastId, "page_size": "10"} 63 | req := common.R(account.Device).SetCookies(common.SToken(account)).SetQueryParams(query) 64 | return common.Exec[*ListPostData](req, "GET", AddrTakumi+"/post/api/getForumPostList") 65 | } 66 | 67 | func ListFeedPost(gameId string, account config.Account) (*ListPostData, error) { 68 | query := gh.MS{"gids": gameId, "fresh_action": "1", "is_first_initialize": "true"} 69 | req := common.R(account.Device).SetCookies(common.SToken(account)).SetQueryParams(query) 70 | return common.Exec[*ListPostData](req, "GET", AddrBBS+"/post/api/feeds/posts") 71 | } 72 | 73 | type GetPostData struct { 74 | Post *PostData `json:"post"` 75 | } 76 | 77 | func GetPost(postId string, account config.Account) (*GetPostData, error) { 78 | req := common.R(account.Device).SetCookies(common.SToken(account)).SetQueryParam("post_id", postId) 79 | return common.Exec[*GetPostData](req, "GET", AddrBBS+"/post/api/getPostFull") 80 | } 81 | 82 | func UpvotePost(postId string, cancel bool, account config.Account) error { 83 | req := common.R(account.Device).SetCookies(common.SToken(account)).SetBody(gh.M{"post_id": postId, "upvote_type": gh.Ternary(cancel, "0", "1"), "is_cancel": cancel}) 84 | _, err := common.Exec[any](req, "POST", AddrBBS+"/post/api/post/upvote") 85 | return err 86 | } 87 | 88 | func CollectPost(postId string, cancel bool, account config.Account) error { 89 | req := common.R(account.Device).SetCookies(common.SToken(account)).SetBody(gh.M{"post_id": postId, "is_cancel": cancel}) 90 | _, err := common.Exec[any](req, "POST", AddrBBS+"/post/api/collectPost") 91 | return err 92 | } 93 | 94 | type SharePostData struct { 95 | Title string `json:"title"` 96 | Content string `json:"content"` 97 | Icon string `json:"icon"` 98 | Url string `json:"url"` 99 | } 100 | 101 | func SharePost(postId string, account config.Account) (*SharePostData, error) { 102 | req := common.R(account.Device).SetCookies(common.SToken(account)).SetQueryParam("entity_type", "1").SetQueryParam("entity_id", postId) 103 | return common.Exec[*SharePostData](req, "GET", AddrBBS+"/apihub/api/getShareConf") 104 | } 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Miyoushe-Task (_No longer maintained_) 2 | 3 | ![golang](https://img.shields.io/github/actions/workflow/status/starudream/miyoushe-task/golang.yml?style=for-the-badge&logo=github&label=golang) 4 | ![release](https://img.shields.io/github/v/release/starudream/miyoushe-task?style=for-the-badge) 5 | ![license](https://img.shields.io/github/license/starudream/miyoushe-task?style=for-the-badge) 6 | 7 | ## Config 8 | 9 | - `global` [doc](https://github.com/starudream/go-lib/blob/v2/README.md) - [example](https://github.com/starudream/go-lib/blob/v2/app.example.yaml) 10 | 11 | 以下参数无需手动增加,可通过下方 [Account](#account) 初始化并扫码登录自动获取 12 | 13 | ```yaml 14 | accounts: 15 | - phone: "手机号码,仅用作唯一标识,暂无实际作用" 16 | device: 17 | id: "设备标识,uuid,登录后建议不要修改" 18 | type: "手机类型,默认 2 为安卓" 19 | name: "手机型号,默认 Xiaomi 22011211C" 20 | model: "手机型号,默认 22011211C" 21 | version: "手机安卓版本,默认 13" 22 | channel: "渠道,默认 miyousheluodi" 23 | uid: "米游社 uid" 24 | gtoken: "game token,废弃" 25 | ctoken: "cookie token" 26 | mid: "米哈游 uid" 27 | stoken: "stoken v2" 28 | sign_game_ids: [ "游戏签到的游戏 id 列表,为空时签到所有游戏角色" ] 29 | 30 | cron: 31 | spec: "签到奖励执行时间,默认 5 4 8 * * * 即每天 08:04:05" 32 | startup: "是否启动时执行一次,默认 false" 33 | 34 | # 打码平台配置 35 | rrocr: 36 | key: "from rrocr.com" 37 | ttocr: 38 | key: "from ttocr.com" 39 | interval: 3s 40 | timeout: 90s 41 | item_id: 388 42 | ``` 43 | 44 | ## Usage 45 | 46 | ``` 47 | > miyoushe-task -h 48 | Usage: 49 | miyoushe-task [command] 50 | 51 | Available Commands: 52 | account Manage accounts 53 | config Manage config 54 | cron Run as cron job 55 | notify Manage notify 56 | sign Run sign task 57 | 58 | Flags: 59 | -c, --config string path to config file 60 | -h, --help help for miyoushe-task 61 | -v, --version version for miyoushe-task 62 | 63 | Use "miyoushe-task [command] --help" for more information about a command. 64 | ``` 65 | 66 | ### Account 67 | 68 | ```shell 69 | # list accounts 70 | miyoushe-task account list 71 | # init account device information 72 | miyoushe-task account init 73 | # login account by send phone code to get token 74 | miyoushe-task account login 75 | ``` 76 | 77 | 如果登录时出现验证码, 下载项目中 [geetest.html](./geetest.html) 文件,本地打开文件后输入 `gt` 和 `challenge`,复制极验结果。 78 | 79 | ```text 80 | aigis gt: abc, challenge: xyz 81 | please enter GeeTest json string: {"geetest_challenge":"123","geetest_validate":"456","geetest_seccode":"789|jordan"} 82 | ``` 83 | 84 | ![geetest](./docs/geetest1.png) 85 | 86 | ### SignForum `米游社每日任务` 87 | 88 | ```shell 89 | miyoushe-task sign forum 90 | ``` 91 | 92 | ### SignGame `米游社游戏签到` 93 | 94 | ```shell 95 | miyoushe-task sign game 96 | ``` 97 | 98 | ### Cron 99 | 100 | ```shell 101 | miyoushe-task cron 102 | ``` 103 | 104 | ### Service 105 | 106 | ```shell 107 | # register as system service 108 | miyoushe-task service --user --config miyoushe-task.yaml install 109 | miyoushe-task service start 110 | miyoushe-task service status 111 | ``` 112 | 113 | ## Docker 114 | 115 | ```shell 116 | mkdir miyoushe && touch miyoushe/app.yaml 117 | docker run -it --rm -v $(pwd)/miyoushe:/miyoushe -e DEBUG=true starudream/miyoushe-task /miyoushe-task -c /miyoushe/app.yaml account init 118 | docker run -it --rm -v $(pwd)/miyoushe:/miyoushe -e DEBUG=true starudream/miyoushe-task /miyoushe-task -c /miyoushe/app.yaml account login 119 | docker run -it --rm -v $(pwd)/miyoushe:/miyoushe -e DEBUG=true starudream/miyoushe-task /miyoushe-task -c /miyoushe/app.yaml sign game 120 | ``` 121 | 122 | ## Docker Compose 123 | 124 | ```yaml 125 | version: "3" 126 | services: 127 | miyoushe: 128 | image: starudream/miyoushe-task 129 | container_name: miyoushe 130 | restart: always 131 | command: /miyoushe-task -c /miyoushe/app.yaml cron 132 | volumes: 133 | - "./miyoushe/:/miyoushe" 134 | environment: 135 | DEBUG: "true" 136 | app.log.console.level: "info" 137 | app.log.file.enabled: "true" 138 | app.log.file.level: "debug" 139 | app.log.file.filename: "/miyoushe/app.log" 140 | app.cron.spec: "5 4 8 * * *" 141 | app.rrocr.key: "foo" 142 | ``` 143 | 144 | ## Thanks 145 | 146 | - [mihoyo-api-collect](https://github.com/UIGF-org/mihoyo-api-collect) 147 | - [miyoushe 2.62.2 salt](https://blog.starudream.cn/2023/11/09/miyoushe-salt-2.62.2/) 148 | 149 | ## [License](./LICENSE) 150 | -------------------------------------------------------------------------------- /.goreleaser.yaml: -------------------------------------------------------------------------------- 1 | version: 2 2 | 3 | env: 4 | - CGO_ENABLED=0 5 | - GO111MODULE=on 6 | 7 | snapshot: 8 | version_template: "{{ .Version }}-next" 9 | 10 | report_sizes: true 11 | 12 | builds: 13 | - main: ./cmd 14 | goos: 15 | - linux 16 | - darwin 17 | - windows 18 | goarch: 19 | - amd64 20 | - arm64 21 | ldflags: 22 | - -s -w 23 | - -X "github.com/starudream/go-lib/core/v2/config/version.gitVersion=v{{ .Version }}" 24 | 25 | archives: 26 | - formats: 27 | - tar.gz 28 | name_template: "{{ .ProjectName }}_v{{ .Version }}_{{ .Os }}_{{ .Arch }}" 29 | format_overrides: 30 | - goos: windows 31 | formats: 32 | - zip 33 | 34 | dockers: 35 | - goos: linux 36 | goarch: amd64 37 | dockerfile: Dockerfile 38 | use: buildx 39 | image_templates: 40 | - "starudream/{{ .ProjectName }}:latest-amd64" 41 | - "starudream/{{ .ProjectName }}:{{ .Tag }}-amd64" 42 | - "ghcr.io/starudream/{{ .ProjectName }}:latest-amd64" 43 | - "ghcr.io/starudream/{{ .ProjectName }}:{{ .Tag }}-amd64" 44 | build_flag_templates: 45 | - "--pull" 46 | - "--platform=linux/amd64" 47 | - "--label=org.opencontainers.image.created={{ .Date }}" 48 | - "--label=org.opencontainers.image.title={{ .ProjectName }}" 49 | - "--label=org.opencontainers.image.revision={{ .FullCommit }}" 50 | - "--label=org.opencontainers.image.version={{ .Version }}" 51 | - "--label=org.opencontainers.image.source={{ .GitURL }}" 52 | - "--label=org.opencontainers.image.url={{ .GitURL }}" 53 | - "--label=org.opencontainers.image.licenses=Apache-2.0" 54 | - goos: linux 55 | goarch: arm64 56 | dockerfile: Dockerfile 57 | use: buildx 58 | image_templates: 59 | - "starudream/{{ .ProjectName }}:latest-arm64" 60 | - "starudream/{{ .ProjectName }}:{{ .Tag }}-arm64" 61 | - "ghcr.io/starudream/{{ .ProjectName }}:latest-arm64" 62 | - "ghcr.io/starudream/{{ .ProjectName }}:{{ .Tag }}-arm64" 63 | build_flag_templates: 64 | - "--pull" 65 | - "--platform=linux/arm64" 66 | - "--label=org.opencontainers.image.created={{ .Date }}" 67 | - "--label=org.opencontainers.image.title={{ .ProjectName }}" 68 | - "--label=org.opencontainers.image.revision={{ .FullCommit }}" 69 | - "--label=org.opencontainers.image.version={{ .Version }}" 70 | - "--label=org.opencontainers.image.source={{ .GitURL }}" 71 | - "--label=org.opencontainers.image.url={{ .GitURL }}" 72 | - "--label=org.opencontainers.image.licenses=Apache-2.0" 73 | 74 | docker_manifests: 75 | - name_template: "starudream/{{ .ProjectName }}:latest" 76 | image_templates: 77 | - "starudream/{{ .ProjectName }}:latest-amd64" 78 | - "starudream/{{ .ProjectName }}:latest-arm64" 79 | - name_template: "starudream/{{ .ProjectName }}:{{ .Tag }}" 80 | image_templates: 81 | - "starudream/{{ .ProjectName }}:{{ .Tag }}-amd64" 82 | - "starudream/{{ .ProjectName }}:{{ .Tag }}-arm64" 83 | - name_template: "ghcr.io/starudream/{{ .ProjectName }}:latest" 84 | image_templates: 85 | - "ghcr.io/starudream/{{ .ProjectName }}:latest-amd64" 86 | - "ghcr.io/starudream/{{ .ProjectName }}:latest-arm64" 87 | - name_template: "ghcr.io/starudream/{{ .ProjectName }}:{{ .Tag }}" 88 | image_templates: 89 | - "ghcr.io/starudream/{{ .ProjectName }}:{{ .Tag }}-amd64" 90 | - "ghcr.io/starudream/{{ .ProjectName }}:{{ .Tag }}-arm64" 91 | 92 | checksum: 93 | name_template: "checksums.txt" 94 | 95 | release: 96 | target_commitish: "{{ .Commit }}" 97 | 98 | changelog: 99 | sort: asc 100 | use: github 101 | groups: 102 | - title: Features 103 | regexp: "^.*feat[(\\w)]*:+.*$" 104 | order: 10 105 | - title: Bug Fixes 106 | regexp: "^.*fix[(\\w)]*:+.*$" 107 | order: 20 108 | - title: Performance Improvements 109 | regexp: "^.*perf[(\\w)]*:+.*$" 110 | order: 30 111 | - title: Styles 112 | regexp: "^.*style[(\\w)]*:+.*$" 113 | order: 50 114 | - title: Miscellaneous Chores 115 | regexp: "^.*chore[(\\w)]*:+.*$" 116 | order: 60 117 | - title: Documentation 118 | regexp: "^.*docs[(\\w)]*:+.*$" 119 | order: 80 120 | - title: Dependencies 121 | regexp: "^.*deps[(\\w)]*:+.*$" 122 | order: 85 123 | - title: Build System 124 | regexp: "^.*build[(\\w)]*:+.*$" 125 | order: 90 126 | - title: Continuous Integration 127 | regexp: "^.*ci[(\\w)]*:+.*$" 128 | order: 95 129 | - title: Others 130 | order: 99 131 | -------------------------------------------------------------------------------- /job/forum.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "time" 7 | 8 | "github.com/starudream/go-lib/core/v2/slog" 9 | 10 | "github.com/starudream/miyoushe-task/api/common" 11 | "github.com/starudream/miyoushe-task/api/miyoushe" 12 | "github.com/starudream/miyoushe-task/config" 13 | ) 14 | 15 | const ( 16 | VerifyRetry = 10 17 | 18 | PostView = 3 19 | PostUpvote = 10 20 | PostShare = 1 21 | PostLoop = 10 22 | ) 23 | 24 | type SignForumRecord struct { 25 | GameId string 26 | GameName string 27 | HasSigned bool 28 | IsRisky bool 29 | IsSuccess bool 30 | Verify int 31 | Points int 32 | 33 | PostView int 34 | PostUpvote int 35 | PostShare int 36 | LoopCount int 37 | } 38 | 39 | func (r SignForumRecord) Name() string { 40 | return "米游社每日任务" 41 | } 42 | 43 | func (r SignForumRecord) Success() string { 44 | vs := []string{r.Name() + "完成"} 45 | vs = append(vs, fmt.Sprintf("在版区【%s】", r.GameName)) 46 | if r.HasSigned { 47 | vs = append(vs, " 已打卡") 48 | } else if r.Points > 0 { 49 | vs = append(vs, fmt.Sprintf(" 打卡获得%d米游币(%d)", r.Points, r.Verify)) 50 | } else { 51 | vs = append(vs, fmt.Sprintf(" 打卡失败(%d)", r.Verify)) 52 | } 53 | vs = append(vs, 54 | fmt.Sprintf(" 浏览%d/%d个帖子", r.PostView, PostView), 55 | fmt.Sprintf(" 点赞%d/%d个帖子", r.PostUpvote, PostUpvote), 56 | fmt.Sprintf(" 分享%d/%d个帖子", r.PostShare, PostShare), 57 | ) 58 | return strings.Join(vs, "\n") 59 | } 60 | 61 | func SignForum(account config.Account) (record SignForumRecord, err error) { 62 | account, err = RefreshSTokenAuto(account) 63 | if err != nil { 64 | return 65 | } 66 | 67 | businesses, err := miyoushe.GetBusinesses(account) 68 | if err != nil { 69 | err = fmt.Errorf("get businesses error: %w", err) 70 | return 71 | } 72 | 73 | if len(businesses.Businesses) == 0 { 74 | err = fmt.Errorf("no channel subscription, please use phone to login and subscribe channel") 75 | return 76 | } 77 | 78 | defer func() { 79 | slog.Info("sign forum record: %+v", record) 80 | if err != nil { 81 | slog.Error("sign forum error: %v", err) 82 | } 83 | }() 84 | 85 | gameId := businesses.Businesses[0] 86 | game := miyoushe.AllGamesById[gameId] 87 | 88 | record.GameId = gameId 89 | record.GameName = game.Name 90 | 91 | today, err := miyoushe.GetSignForum(gameId, account) 92 | if err != nil { 93 | err = fmt.Errorf("get sign forum error: %w", err) 94 | return 95 | } 96 | 97 | var ( 98 | verification *common.Verification 99 | signForumData *miyoushe.SignForumData 100 | ) 101 | 102 | if today.IsSigned { 103 | record.HasSigned = true 104 | goto post 105 | } 106 | 107 | sign: 108 | 109 | signForumData, err = miyoushe.SignForum(gameId, account, verification) 110 | if err != nil { 111 | if common.IsRetCode(err, common.RetCodeForumHasSigned) { 112 | record.HasSigned = true 113 | } else if common.IsRetCode(err, common.RetCodeForumNeedVerification) { 114 | record.IsRisky = true 115 | verify: 116 | record.Verify++ 117 | verification, err = Verify(account) 118 | if err == nil { 119 | goto sign 120 | } else { 121 | slog.Error("verify error: %v", err) 122 | if record.Verify < VerifyRetry { 123 | slog.Info("retry verify, count: %d", record.Verify) 124 | goto verify 125 | } 126 | } 127 | } else { 128 | err = fmt.Errorf("sign forum error: %w", err) 129 | return 130 | } 131 | } else { 132 | record.IsSuccess = true 133 | } 134 | 135 | if signForumData != nil { 136 | record.Points = signForumData.Points 137 | } 138 | 139 | post: 140 | 141 | record.LoopCount++ 142 | 143 | posts, err := miyoushe.ListFeedPost(gameId, account) 144 | if err != nil { 145 | err = fmt.Errorf("list feed post error: %w", err) 146 | return 147 | } 148 | 149 | for i := 0; i < len(posts.List); i++ { 150 | p := posts.List[i] 151 | pid := p.Post.PostId 152 | if record.PostView < PostView { 153 | _, e := miyoushe.GetPost(pid, account) 154 | if e != nil { 155 | slog.Error("get post error: %v", e) 156 | continue 157 | } 158 | record.PostView++ 159 | time.Sleep(100 * time.Millisecond) 160 | } 161 | if record.PostUpvote < PostUpvote && !p.IsUpvote() { 162 | e := miyoushe.UpvotePost(pid, false, account) 163 | if e != nil { 164 | slog.Error("upvote post error: %v", e) 165 | continue 166 | } 167 | slog.Debug("upvote post: %s (%s) %s", p.Post.Subject, pid, p.User.Nickname) 168 | record.PostUpvote++ 169 | time.Sleep(100 * time.Millisecond) 170 | } 171 | if record.PostShare < PostShare { 172 | _, e := miyoushe.SharePost(pid, account) 173 | if e != nil { 174 | slog.Error("share post error: %v", e) 175 | continue 176 | } 177 | record.PostShare++ 178 | time.Sleep(100 * time.Millisecond) 179 | } 180 | time.Sleep(500 * time.Millisecond) 181 | } 182 | 183 | if record.LoopCount < PostLoop && (record.PostView < PostView || record.PostUpvote < PostUpvote || record.PostShare < PostShare) { 184 | goto post 185 | } 186 | 187 | return 188 | } 189 | -------------------------------------------------------------------------------- /job/game.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "cmp" 5 | "fmt" 6 | "slices" 7 | "strings" 8 | 9 | "github.com/starudream/go-lib/core/v2/slog" 10 | 11 | "github.com/starudream/miyoushe-task/api/common" 12 | "github.com/starudream/miyoushe-task/api/miyoushe" 13 | "github.com/starudream/miyoushe-task/config" 14 | ) 15 | 16 | var SignGameIdByBiz = map[string]string{ 17 | common.GameBizBH3CN: common.GameIdBH3, 18 | common.GameBizYSCN: common.GameIdYS, 19 | common.GameBizBH2CN: common.GameIdBH2, 20 | common.GameBizWDCN: common.GameIdWD, 21 | common.GameBizSRCN: common.GameIdSR, 22 | common.GameBizZZZCN: common.GameIdZZZ, 23 | } 24 | 25 | type SignGameRecord struct { 26 | GameId string 27 | GameName string 28 | RoleName string 29 | RoleUid string 30 | HasSigned bool 31 | IsRisky bool 32 | IsSuccess bool 33 | Verify int 34 | Award string 35 | } 36 | 37 | type SignGameRecords []SignGameRecord 38 | 39 | func (rs SignGameRecords) Name() string { 40 | return "米游社游戏签到" 41 | } 42 | 43 | func (rs SignGameRecords) Success() string { 44 | vs := []string{rs.Name() + "完成"} 45 | for i := 0; i < len(rs); i++ { 46 | if rs[i].HasSigned || rs[i].IsSuccess { 47 | vs = append(vs, fmt.Sprintf("在游戏【%s】角色【%s】获得 %s(%d)", rs[i].GameName, rs[i].RoleName, rs[i].Award, rs[i].Verify)) 48 | } 49 | } 50 | return strings.Join(vs, "\n") 51 | } 52 | 53 | func SignGame(account config.Account) (_ SignGameRecords, err error) { 54 | account, err = RefreshSTokenAuto(account) 55 | if err != nil { 56 | return nil, err 57 | } 58 | account, err = RefreshCToken(account) 59 | if err != nil { 60 | return nil, err 61 | } 62 | roles, err := miyoushe.ListGameRole("", account) 63 | if err != nil { 64 | return nil, fmt.Errorf("list game role error: %w", err) 65 | } 66 | return SignGameRoles(roles.List, account) 67 | } 68 | 69 | func SignGameRoles(roles []*miyoushe.GameRole, account config.Account) (SignGameRecords, error) { 70 | var records []SignGameRecord 71 | for _, role := range roles { 72 | record, err := SignGameRole(role, account) 73 | slog.Info("sign game record: %+v", record) 74 | if err != nil { 75 | return nil, err 76 | } 77 | records = append(records, record) 78 | } 79 | slices.SortFunc(records, func(a, b SignGameRecord) int { 80 | if a.GameId == b.GameId { 81 | return cmp.Compare(a.RoleUid, b.RoleUid) 82 | } 83 | return cmp.Compare(a.GameId, b.GameId) 84 | }) 85 | return records, nil 86 | } 87 | 88 | func SignGameRole(role *miyoushe.GameRole, account config.Account) (record SignGameRecord, err error) { 89 | record.RoleName = role.Nickname 90 | record.RoleUid = role.GameUid 91 | 92 | gameId := SignGameIdByBiz[role.GameBiz] 93 | if gameId == "" { 94 | slog.Warn("game biz %s not supported", role.GameBiz) 95 | return 96 | } 97 | 98 | if len(account.SignGameIds) > 0 && !slices.Contains(account.SignGameIds, gameId) { 99 | slog.Warn("game id %s not in sign game ids", gameId) 100 | return 101 | } 102 | 103 | game := miyoushe.AllGamesById[gameId] 104 | 105 | record.GameId = gameId 106 | record.GameName = game.Name 107 | 108 | gameName := strings.Split(role.GameBiz, "_")[0] 109 | 110 | home, err := miyoushe.GetHome(gameId, account) 111 | if err != nil { 112 | err = fmt.Errorf("get home error: %w", err) 113 | return 114 | } 115 | 116 | actId := home.GetSignActId() 117 | if actId == "" { 118 | err = fmt.Errorf("get sign act id error: %w", err) 119 | return 120 | } 121 | 122 | today, err := miyoushe.GetSignGame(gameName, actId, role.Region, role.GameUid, account) 123 | if err != nil { 124 | err = fmt.Errorf("get sign game error: %w", err) 125 | return 126 | } 127 | 128 | var ( 129 | verification *common.Verification 130 | signGameData *miyoushe.SignGameData 131 | ) 132 | 133 | if today.IsSign { 134 | record.HasSigned = true 135 | goto award 136 | } 137 | 138 | sign: 139 | 140 | signGameData, err = miyoushe.SignGame(gameName, actId, role.Region, role.GameUid, account, verification) 141 | if err != nil { 142 | if common.IsRetCode(err, common.RetCodeGameHasSigned) { 143 | record.HasSigned = true 144 | } else { 145 | err = fmt.Errorf("sign game error: %w", err) 146 | return 147 | } 148 | } else if signGameData.IsRisky() { 149 | record.IsRisky = true 150 | if signGameData.Gt == "" || signGameData.Challenge == "" { 151 | err = fmt.Errorf("sign game is risky, but gt or challenge is empty") 152 | return 153 | } 154 | record.Verify++ 155 | verification, err = DM(signGameData.Gt, signGameData.Challenge) 156 | if err != nil { 157 | slog.Error("dm error: %v", err) 158 | if record.Verify >= VerifyRetry { 159 | return 160 | } 161 | slog.Info("retry sign, count: %d", record.Verify) 162 | } 163 | goto sign 164 | } else { 165 | record.IsSuccess = true 166 | } 167 | 168 | award: 169 | 170 | award, err := miyoushe.ListSignGameAward(gameName, actId, role.Region, role.GameUid, account) 171 | if err != nil { 172 | err = fmt.Errorf("list sign game award error: %w", err) 173 | return 174 | } 175 | record.Award = award.Today().ShortString() 176 | return 177 | } 178 | -------------------------------------------------------------------------------- /api/miyoushe/sign_game.go: -------------------------------------------------------------------------------- 1 | package miyoushe 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | "time" 7 | 8 | "github.com/starudream/go-lib/core/v2/gh" 9 | 10 | "github.com/starudream/miyoushe-task/api/common" 11 | "github.com/starudream/miyoushe-task/config" 12 | ) 13 | 14 | var signGameAddrByName = map[string]string{ 15 | common.GameNameZZZ: AddrActNap, 16 | } 17 | 18 | func signGameAddr(gameName string) string { 19 | addr, ok := signGameAddrByName[gameName] 20 | if ok { 21 | return addr 22 | } 23 | return AddrTakumi 24 | } 25 | 26 | var signGameHeaderByName = map[string]string{ 27 | common.GameNameZZZ: "zzz", 28 | } 29 | 30 | func signGameHeader(gameName string) string { 31 | header, ok := signGameHeaderByName[gameName] 32 | if ok { 33 | return header 34 | } 35 | return gameName 36 | } 37 | 38 | type SignGameData struct { 39 | Code string `json:"code"` 40 | Success int `json:"success"` 41 | IsRisk bool `json:"is_risk"` 42 | RiskCode int `json:"risk_code"` 43 | Gt string `json:"gt"` 44 | Challenge string `json:"challenge"` 45 | } 46 | 47 | func (t *SignGameData) IsRisky() bool { 48 | return t.IsRisk 49 | } 50 | 51 | func SignGame(gameName, actId, region, uid string, account config.Account, validate *common.Verification) (*SignGameData, error) { 52 | body := gh.MS{"lang": "zh-cn", "act_id": actId, "region": region, "uid": uid} 53 | req := common.R(account.Device, validate).SetHeader(common.XRpcSignGame, signGameHeader(gameName)).SetCookies(common.SToken(account)).SetCookies(common.CToken(account)).SetBody(body) 54 | return common.Exec[*SignGameData](req, "POST", signGameAddr(gameName)+"/event/luna/sign") 55 | } 56 | 57 | type GetSignGameData struct { 58 | TotalSignDay int `json:"total_sign_day"` 59 | Today string `json:"today"` 60 | IsSign bool `json:"is_sign"` 61 | IsSub bool `json:"is_sub"` 62 | Region string `json:"region"` 63 | SignCntMissed int `json:"sign_cnt_missed"` 64 | ShortSignDay int `json:"short_sign_day"` 65 | } 66 | 67 | func GetSignGame(gameName, actId, region, uid string, account config.Account) (*GetSignGameData, error) { 68 | query := gh.MS{"lang": "zh-cn", "act_id": actId, "region": region, "uid": uid} 69 | req := common.R(account.Device).SetHeader(common.XRpcSignGame, signGameHeader(gameName)).SetCookies(common.SToken(account)).SetCookies(common.CToken(account)).SetQueryParams(query) 70 | return common.Exec[*GetSignGameData](req, "GET", signGameAddr(gameName)+"/event/luna/info") 71 | } 72 | 73 | type ListSignGameData struct { 74 | Month int `json:"month"` 75 | Biz string `json:"biz"` 76 | Resign bool `json:"resign"` 77 | Awards []*SignGameAward `json:"awards"` 78 | ExtraAward *SignGameExtraAward `json:"short_extra_award"` 79 | } 80 | 81 | type SignGameAward struct { 82 | Name string `json:"name"` 83 | Cnt int `json:"cnt"` 84 | CreatedAt string `json:"created_at,omitempty"` 85 | } 86 | 87 | type SignGameExtraAward struct { 88 | HasExtraAward bool `json:"has_extra_award"` 89 | StartTime string `json:"start_time"` 90 | EndTime string `json:"end_time"` 91 | List []any `json:"list"` 92 | StartTimestamp string `json:"start_timestamp"` 93 | EndTimestamp string `json:"end_timestamp"` 94 | } 95 | 96 | func ListSignGame(gameName, actId string, account config.Account) (*ListSignGameData, error) { 97 | query := gh.MS{"lang": "zh-cn", "act_id": actId} 98 | req := common.R(account.Device).SetHeader(common.XRpcSignGame, signGameHeader(gameName)).SetCookies(common.SToken(account)).SetQueryParams(query) 99 | return common.Exec[*ListSignGameData](req, "GET", signGameAddr(gameName)+"/event/luna/home") 100 | } 101 | 102 | type ListSignGameAwardData struct { 103 | Total int `json:"total"` 104 | List SignGameAwards `json:"list"` 105 | } 106 | 107 | type SignGameAwards []*SignGameAward 108 | 109 | func (v1 SignGameAwards) Today() (v2 SignGameAwards) { 110 | today := time.Now().Format(time.DateOnly) 111 | for i := range v1 { 112 | if strings.HasPrefix(v1[i].CreatedAt, today) { 113 | v2 = append(v2, v1[i]) 114 | } 115 | } 116 | return 117 | } 118 | 119 | func (v1 SignGameAwards) ShortString() string { 120 | v2 := make([]string, len(v1)) 121 | for i, v := range v1 { 122 | v2[i] = v.Name + "*" + strconv.Itoa(v.Cnt) 123 | } 124 | return strings.Join(v2, ", ") 125 | } 126 | 127 | func ListSignGameAward(gameName, actId, region, uid string, account config.Account) (list SignGameAwards, _ error) { 128 | for page, total := 1, -1; ; page++ { 129 | query := gh.MS{"lang": "zh-cn", "act_id": actId, "region": region, "uid": uid, "current_page": strconv.Itoa(page), "page_size": "10"} 130 | req := common.R(account.Device).SetHeader(common.XRpcSignGame, signGameHeader(gameName)).SetCookies(common.SToken(account)).SetCookies(common.CToken(account)).SetQueryParams(query) 131 | data, err := common.Exec[*ListSignGameAwardData](req, "GET", signGameAddr(gameName)+"/event/luna/award", 2) 132 | if err != nil { 133 | return nil, err 134 | } 135 | list = append(list, data.List...) 136 | if page == 1 && total == -1 { 137 | total = data.Total 138 | } 139 | total -= len(data.List) 140 | if total <= 0 { 141 | return 142 | } 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= 2 | github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= 3 | github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= 4 | github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= 5 | github.com/go-resty/resty/v2 v2.16.5 h1:hBKqmWrr7uRc3euHVqmh1HTHcKn99Smr7o5spptdhTM= 6 | github.com/go-resty/resty/v2 v2.16.5/go.mod h1:hkJtXbA2iKHzJheXYvQ8snQES5ZLGKMwQ07xAwp/fiA= 7 | github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= 8 | github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= 9 | github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= 10 | github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= 11 | github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= 12 | github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= 13 | github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= 14 | github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 15 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 16 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 17 | github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= 18 | github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= 19 | github.com/kardianos/service v1.2.4 h1:XNlGtZOYNx2u91urOdg/Kfmc+gfmuIo1Dd3rEi2OgBk= 20 | github.com/kardianos/service v1.2.4/go.mod h1:E4V9ufUuY82F7Ztlu1eN9VXWIQxg8NoLQlmFe0MtrXc= 21 | github.com/knadh/koanf/maps v0.1.2 h1:RBfmAW5CnZT+PJ1CVc1QSJKf4Xu9kxfQgYVQSu8hpbo= 22 | github.com/knadh/koanf/maps v0.1.2/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= 23 | github.com/knadh/koanf/v2 v2.2.2 h1:ghbduIkpFui3L587wavneC9e3WIliCgiCgdxYO/wd7A= 24 | github.com/knadh/koanf/v2 v2.2.2/go.mod h1:abWQc0cBXLSF/PSOMCB/SK+T13NXDsPvOksbpi5e/9Q= 25 | github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= 26 | github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= 27 | github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= 28 | github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= 29 | github.com/lmittmann/tint v1.1.2 h1:2CQzrL6rslrsyjqLDwD11bZ5OpLBPU+g3G/r5LSfS8w= 30 | github.com/lmittmann/tint v1.1.2/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE= 31 | github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= 32 | github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= 33 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 34 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 35 | github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= 36 | github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= 37 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 38 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 39 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 40 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 41 | github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= 42 | github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= 43 | github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= 44 | github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= 45 | github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= 46 | github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= 47 | github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 48 | github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= 49 | github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= 50 | github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 51 | github.com/samber/lo v1.51.0 h1:kysRYLbHy/MB7kQZf5DSN50JHmMsNEdeY24VzJFu7wI= 52 | github.com/samber/lo v1.51.0/go.mod h1:4+MXEGsJzbKGaUEQFKBq2xtfuznW9oz/WrgyzMzRoM0= 53 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= 54 | github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= 55 | github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE= 56 | github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= 57 | github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= 58 | github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= 59 | github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 60 | github.com/spf13/pflag v1.0.7 h1:vN6T9TfwStFPFM5XzjsvmzZkLuaLX+HS+0SeFLRgU6M= 61 | github.com/spf13/pflag v1.0.7/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= 62 | github.com/starudream/go-lib/cobra/v2 v2.0.21 h1:1ARo8O2+zPXq+kl9wWnAhG1ajmpdzpcoA/DkaObYUiQ= 63 | github.com/starudream/go-lib/cobra/v2 v2.0.21/go.mod h1:qzeme6uF+5/v+uAofX6+I7MXlen6ypIi7DMofBFJhmw= 64 | github.com/starudream/go-lib/core/v2 v2.1.12 h1:E1uzJdr0j4SrTpOPT24vq8DMg/lf0XybCiS76R8KnHc= 65 | github.com/starudream/go-lib/core/v2 v2.1.12/go.mod h1:956S/9Yngu7/m0bCeIpZ29hNpCCoS8rqqIoGKg1vpWA= 66 | github.com/starudream/go-lib/cron/v2 v2.0.21 h1:pXSrkSHGU0mY1VVAXN6D8dfz/CiPaCE0Bdi7F7n4vVA= 67 | github.com/starudream/go-lib/cron/v2 v2.0.21/go.mod h1:xQP23oCPkRfMxj1CmpFzu9pVFDJfHxl+X2I6bQ8HXWc= 68 | github.com/starudream/go-lib/ntfy/v2 v2.0.24 h1:lnJ3474dziK+MvRX6dzbdyQABymwdrqQq+fTknl8Dlw= 69 | github.com/starudream/go-lib/ntfy/v2 v2.0.24/go.mod h1:XQy/V6f5iQjWnHJP1F7fhSFjlKfe7EenZvnZ4Ag/FBE= 70 | github.com/starudream/go-lib/resty/v2 v2.0.23 h1:CZVheixtfJ3xhFjzoqipIkYXDGvnGLdt45SXMyxtnMc= 71 | github.com/starudream/go-lib/resty/v2 v2.0.23/go.mod h1:jVQ8Qqr5Yz21bF7IC5ooIm8t27Jw6petsijMxr7wIJQ= 72 | github.com/starudream/go-lib/service/v2 v2.0.16 h1:D4HmK0TG2YCZEPjiD8qvLcG08I0eOgQtGXw8ZMTVcy4= 73 | github.com/starudream/go-lib/service/v2 v2.0.16/go.mod h1:FfNaF8Zv10ak+s6Gijj8TgbKXUPgn7urfmE2haxHwiY= 74 | github.com/starudream/go-lib/tablew/v2 v2.0.9 h1:/1osA6vcJLLa7ZryMAMGSmSeD3sY6L+SU+5kQzJv9WA= 75 | github.com/starudream/go-lib/tablew/v2 v2.0.9/go.mod h1:Jd49DDnO+CIvgLOYIM+Bw+yvX7L5/eDQSlgxBsdkMJE= 76 | golang.org/x/net v0.42.0 h1:jzkYrhi3YQWD6MLBJcsklgQsoAcw89EcZbJw8Z614hs= 77 | golang.org/x/net v0.42.0/go.mod h1:FF1RA5d3u7nAYA4z2TkclSCKh68eSXtiFwcWQpPXdt8= 78 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 79 | golang.org/x/sys v0.34.0 h1:H5Y5sJ2L2JRdyv7ROF1he/lPdvFsd0mJHFw2ThKHxLA= 80 | golang.org/x/sys v0.34.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 81 | golang.org/x/text v0.27.0 h1:4fGWRpyh641NLlecmyl4LOe6yDdfaYNrGb2zdfo4JV4= 82 | golang.org/x/text v0.27.0/go.mod h1:1D28KMCvyooCX9hBiosv5Tz/+YLxj0j7XhWjpSUF7CU= 83 | golang.org/x/time v0.6.0 h1:eTDhh4ZXt5Qf0augr54TN6suAUudPcawVZeIAPU7D4U= 84 | golang.org/x/time v0.6.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM= 85 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 86 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 87 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | --------------------------------------------------------------------------------