├── .gitignore ├── LICENSE ├── README.md ├── cas ├── auth.go ├── auth_test.go └── des.go ├── cet ├── cet4.go ├── cet6.go ├── query.go └── readme.md ├── chaoxing ├── chaoxing.go ├── course │ ├── chapter │ │ ├── chapter.go │ │ ├── chapter_brief.go │ │ └── chapter_list.go │ ├── course.go │ ├── course_brief.go │ ├── course_list.go │ ├── data.go │ ├── exam │ │ ├── exam.go │ │ ├── exam_brief.go │ │ └── exam_list.go │ └── work │ │ ├── work.go │ │ ├── work_brief.go │ │ └── work_list.go ├── cx_test.go ├── data.go ├── login.go ├── request │ └── request.go └── utils │ ├── utils.go │ └── utils_test.go ├── client ├── client.go ├── err.go ├── lowNetworkClient.go └── request.go ├── examples ├── ddl │ ├── go.mod │ ├── go.sum │ └── main.go ├── healthcheckin │ ├── go.mod │ ├── go.sum │ └── main.go └── sign │ ├── go.mod │ └── go.sum ├── go.mod ├── go.sum ├── hduhelp ├── data.go ├── time.go └── time_test.go ├── internal ├── ocr │ ├── ocr.go │ ├── ocr_test.go │ ├── readme.md │ ├── testdata │ │ ├── 1.jfif │ │ ├── 2.jfif │ │ ├── 3.jfif │ │ ├── 4.jfif │ │ ├── 5.jfif │ │ ├── 6.jfif │ │ └── 7.jfif │ ├── yunma_ocr.go │ └── yunmaocrtype_string.go └── utils │ └── convert │ ├── converT_test.go │ └── tostring.go ├── phy ├── captcha_ocr.go ├── experiment_sche.go ├── experiment_sche_test.go ├── login.go ├── login_test.go └── readme.md ├── skl ├── api.go ├── data.go ├── err.go ├── file.go ├── file_test.go ├── login.go ├── login_test.go ├── method.go ├── pos │ └── pos.go ├── schema │ ├── schema.go │ ├── schema_test.go │ └── weeks.go ├── skl.go ├── ticket.go ├── ticket_test.go └── user_test.go ├── sso ├── auth.go ├── auth_test.go └── ecb.go └── zjooc ├── api.go ├── course_test.go ├── data.go ├── login.go ├── login_test.go ├── pages.go └── zjooc.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Binaries for programs and plugins 2 | *.exe 3 | *.exe~ 4 | *.dll 5 | *.so 6 | *.dylib 7 | 8 | # Test binary, built with `go test -c` 9 | *.test 10 | 11 | # Output of the go coverage tool, specifically when used with LiteIDE 12 | *.out 13 | 14 | # Dependency directories (remove the comment below to include it) 15 | # vendor/ 16 | .idea/ 17 | .run 18 | 19 | # Temp generated files 20 | *.svl 21 | 22 | # env 23 | .env 24 | .env.development 25 | .env.production 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 柏喵Sakura 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | hduLib 3 |

4 | 5 |
6 | 7 | # hdu 8 | 9 | ![Version](https://img.shields.io/badge/version-0.1.2-blue.svg) 10 | ![Go Version](https://img.shields.io/badge/golang-1.19-blue.svg) 11 | [![Go Reference](https://pkg.go.dev/badge/github.com/hduLib/hdu.svg)](https://pkg.go.dev/github.com/hduLib/hdu) 12 | [![Go Report Card](https://goreportcard.com/badge/github.com/hduLib/hdu)](https://goreportcard.com/report/github.com/hduLib/hdu) 13 | 14 | 15 | ✨这个库实现了与杭电学生经常使用的网站之间的交互,使用这个库可以非常轻松地调取需要的各种信息,自动化地进行各种交互✨ 16 | 17 |
18 | 19 | ## 声明 20 | 21 | 本仓库所有代码仅作学习使用,不得用于其他任何途径。 22 | 23 | ## 功能 24 | 25 | - [x] 健康打卡(skl) 26 | - [x] sso单点登录 27 | - [x] cas统一认证(即将弃用) 28 | - [ ] web vpn 29 | - [x] 请假 30 | - [x] 课表(skl api) 31 | - [x] 省教育平台zjooc(目前仅读取课程) 32 | - [x] 超星学习通(网页端v2 api) 33 | - [ ] 中国大学mooc 34 | - [ ] 智慧树 35 | - [x] 物理实验平台 36 | - [ ] CET 37 | - [ ] 智慧课堂(内网站点) 38 | 39 | ## 社区 40 | 41 | - [hdu-lis](https://github.com/MarleneJiang/hdu-lis) 用js写的hduLib 42 | - [课表ddl卡片](https://github.com/MarleneJiang/hdu-scriptable) 基于scriptable的ios小组件,用于显示ddl和课表 43 | - [健康打卡](https://github.com/HDU-HealthCheckin/HealthCheckin-Release) 44 | 45 | ## 感谢 46 | 47 | 本项目参考了很多大佬的成果,在此一并列出并感谢 48 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /cas/auth.go: -------------------------------------------------------------------------------- 1 | package cas 2 | 3 | import ( 4 | "bytes" 5 | _ "embed" 6 | "errors" 7 | "fmt" 8 | "github.com/hduLib/hdu/client" 9 | "io" 10 | "net/http" 11 | "net/url" 12 | "regexp" 13 | "strconv" 14 | ) 15 | 16 | var ltRegexp = regexp.MustCompile("") 17 | var executionRegexp = regexp.MustCompile("") 18 | 19 | func GenLoginReq(URL, user, passwd string) (*http.Request, error) { 20 | var lt, execution []byte 21 | 22 | //获取lt和execution 23 | req, err := http.NewRequest(http.MethodGet, URL, nil) 24 | req.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.81 Safari/537.36") 25 | resp, err := client.Do(req) 26 | if err != nil { 27 | return nil, err 28 | } 29 | if resp.StatusCode != 200 { 30 | reason, err := io.ReadAll(resp.Body) 31 | if err != nil { 32 | return nil, fmt.Errorf("fail to read body: %v", err) 33 | } 34 | return nil, fmt.Errorf("fail to get lt and excution: %s", string(reason)) 35 | } 36 | body, err := io.ReadAll(resp.Body) 37 | if err != nil { 38 | return nil, err 39 | } 40 | tmp := ltRegexp.FindSubmatch(body) 41 | if len(tmp) != 2 { 42 | return nil, errors.New("提取lt错误") 43 | } 44 | lt = tmp[1] 45 | tmp = executionRegexp.FindSubmatch(body) 46 | if len(tmp) != 2 { 47 | return nil, errors.New("提取execution错误") 48 | } 49 | execution = tmp[1] 50 | 51 | //获取rsa 52 | rsa := getRsa(user + passwd + string(lt)) 53 | 54 | postData := url.Values{} 55 | postData.Add("rsa", rsa) 56 | postData.Add("ul", strconv.Itoa(len(user))) 57 | postData.Add("pl", strconv.Itoa(len(passwd))) 58 | postData.Add("lt", string(lt)) 59 | postData.Add("execution", string(execution)) 60 | postData.Add("_eventId", "submit") 61 | req, err = http.NewRequest(http.MethodPost, URL, bytes.NewBufferString(postData.Encode())) 62 | if err != nil { 63 | return nil, err 64 | } 65 | req.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/104.0.5112.81 Safari/537.36") 66 | req.Header.Add("Referer", URL) 67 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 68 | for _, c := range resp.Cookies() { 69 | req.AddCookie(c) 70 | } 71 | if err != nil { 72 | return nil, err 73 | } 74 | return req, nil 75 | } 76 | -------------------------------------------------------------------------------- /cas/auth_test.go: -------------------------------------------------------------------------------- 1 | package cas 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestGenLoginReq(t *testing.T) { 8 | req, err := GenLoginReq("https://api.hduhelp.com/login/direct/cas?clientID=healthcheckin&redirect=https://healthcheckin.hduhelp.com/#/auth", "21111111", "666666") 9 | if err != nil { 10 | t.Error(err) 11 | } 12 | t.Log(req) 13 | } 14 | -------------------------------------------------------------------------------- /cas/des.go: -------------------------------------------------------------------------------- 1 | package cas 2 | 3 | import ( 4 | "strconv" 5 | "strings" 6 | ) 7 | 8 | func getRsa(data string) string { 9 | return strEnc(data, "1", "2", "3") 10 | } 11 | 12 | func strEnc(data, firstKey, secondKey, thirdKey string) string { 13 | 14 | leng := len(data) 15 | var encData string 16 | var firstKeyBt, secondKeyBt, thirdKeyBt [][64]int 17 | var firstLength, secondLength, thirdLength int 18 | if firstKey != "" { 19 | firstKeyBt = getKeyBytes(firstKey) 20 | firstLength = len(firstKeyBt) 21 | } 22 | if secondKey != "" { 23 | secondKeyBt = getKeyBytes(secondKey) 24 | secondLength = len(secondKeyBt) 25 | } 26 | if thirdKey != "" { 27 | thirdKeyBt = getKeyBytes(thirdKey) 28 | thirdLength = len(thirdKeyBt) 29 | } 30 | 31 | if leng > 0 { 32 | if leng < 4 { 33 | bt := strToBt(data) 34 | var encByte [64]int 35 | if firstKey != "" && secondKey != "" && thirdKey != "" { 36 | 37 | tempBt := bt 38 | for x := 0; x < firstLength; x++ { 39 | tempBt = enc(tempBt, firstKeyBt[x]) 40 | } 41 | for y := 0; y < secondLength; y++ { 42 | tempBt = enc(tempBt, secondKeyBt[y]) 43 | } 44 | for z := 0; z < thirdLength; z++ { 45 | tempBt = enc(tempBt, thirdKeyBt[z]) 46 | } 47 | encByte = tempBt 48 | } else { 49 | if firstKey != "" && secondKey != "" { 50 | tempBt := bt 51 | for x := 0; x < firstLength; x++ { 52 | tempBt = enc(tempBt, firstKeyBt[x]) 53 | } 54 | for y := 0; y < secondLength; y++ { 55 | tempBt = enc(tempBt, secondKeyBt[y]) 56 | } 57 | encByte = tempBt 58 | } else { 59 | if firstKey != "" { 60 | 61 | tempBt := bt 62 | for x := 0; x < firstLength; x++ { 63 | tempBt = enc(tempBt, firstKeyBt[x]) 64 | } 65 | encByte = tempBt 66 | } 67 | } 68 | } 69 | encData = bt64ToHex(encByte) 70 | } else { 71 | iterator := leng / 4 72 | remainder := leng % 4 73 | for i := 0; i < iterator; i++ { 74 | tempData := data[i*4+0 : i*4+4] 75 | tempByte := strToBt(tempData) 76 | var encByte [64]int 77 | if firstKey != "" && secondKey != "" && thirdKey != "" { 78 | 79 | tempBt := tempByte 80 | for x := 0; x < firstLength; x++ { 81 | tempBt = enc(tempBt, firstKeyBt[x]) 82 | } 83 | for y := 0; y < secondLength; y++ { 84 | tempBt = enc(tempBt, secondKeyBt[y]) 85 | } 86 | for z := 0; z < thirdLength; z++ { 87 | tempBt = enc(tempBt, thirdKeyBt[z]) 88 | } 89 | encByte = tempBt 90 | } else { 91 | if firstKey != "" && secondKey != "" { 92 | 93 | tempBt := tempByte 94 | for x := 0; x < firstLength; x++ { 95 | tempBt = enc(tempBt, firstKeyBt[x]) 96 | } 97 | for y := 0; y < secondLength; y++ { 98 | tempBt = enc(tempBt, secondKeyBt[y]) 99 | } 100 | encByte = tempBt 101 | } else { 102 | if firstKey != "" { 103 | 104 | tempBt := tempByte 105 | for x := 0; x < firstLength; x++ { 106 | tempBt = enc(tempBt, firstKeyBt[x]) 107 | } 108 | encByte = tempBt 109 | } 110 | } 111 | } 112 | encData += bt64ToHex(encByte) 113 | } 114 | if remainder > 0 { 115 | remainderData := data[iterator*4+0 : leng] 116 | tempByte := strToBt(remainderData) 117 | var encByte [64]int 118 | if firstKey != "" && secondKey != "" && thirdKey != "" { 119 | 120 | tempBt := tempByte 121 | for x := 0; x < firstLength; x++ { 122 | tempBt = enc(tempBt, firstKeyBt[x]) 123 | } 124 | for y := 0; y < secondLength; y++ { 125 | tempBt = enc(tempBt, secondKeyBt[y]) 126 | } 127 | for z := 0; z < thirdLength; z++ { 128 | tempBt = enc(tempBt, thirdKeyBt[z]) 129 | } 130 | encByte = tempBt 131 | } else { 132 | if firstKey != "" && secondKey != "" { 133 | 134 | tempBt := tempByte 135 | for x := 0; x < firstLength; x++ { 136 | tempBt = enc(tempBt, firstKeyBt[x]) 137 | } 138 | for y := 0; y < secondLength; y++ { 139 | tempBt = enc(tempBt, secondKeyBt[y]) 140 | } 141 | encByte = tempBt 142 | } else { 143 | if firstKey != "" { 144 | 145 | tempBt := tempByte 146 | for x := 0; x < firstLength; x++ { 147 | tempBt = enc(tempBt, firstKeyBt[x]) 148 | } 149 | encByte = tempBt 150 | } 151 | } 152 | } 153 | encData += bt64ToHex(encByte) 154 | } 155 | } 156 | } 157 | return encData 158 | } 159 | 160 | func getKeyBytes(key string) [][64]int { 161 | var keyBytes [][64]int 162 | leng := len(key) 163 | iterator := leng / 4 164 | remainder := leng % 4 165 | i := 0 166 | for i = 0; i < iterator; i++ { 167 | keyBytes = append(keyBytes, strToBt(key[i*4+0:i*4+4])) 168 | } 169 | if remainder > 0 { 170 | keyBytes = append(keyBytes, strToBt(key[i*4+0:leng])) 171 | } 172 | return keyBytes 173 | } 174 | 175 | func strToBt(str string) [64]int { 176 | leng := len(str) 177 | var bt [64]int 178 | if leng < 4 { 179 | for i := 0; i < leng; i++ { 180 | k := int(str[i]) 181 | for j := 0; j < 16; j++ { 182 | pow := 1 183 | for m := 15; m > j; m-- { 184 | pow *= 2 185 | } 186 | bt[16*i+j] = (k / pow) % 2 187 | } 188 | } 189 | for p := leng; p < 4; p++ { 190 | k := 0 191 | for q := 0; q < 16; q++ { 192 | pow := 1 193 | for m := 15; m > q; m-- { 194 | pow *= 2 195 | } 196 | bt[16*p+q] = (k / pow) % 2 197 | } 198 | 199 | } 200 | } else { 201 | for i := 0; i < 4; i++ { 202 | k := int(str[i]) 203 | for j := 0; j < 16; j++ { 204 | pow := 1 205 | for m := 15; m > j; m-- { 206 | pow *= 2 207 | } 208 | bt[16*i+j] = (k / pow) % 2 209 | } 210 | } 211 | } 212 | return bt 213 | } 214 | 215 | func bt4ToHex(binary string) string { 216 | var hex string 217 | switch { 218 | case strings.EqualFold(binary, "0000"): 219 | hex = "0" 220 | case strings.EqualFold(binary, "0001"): 221 | hex = "1" 222 | case strings.EqualFold(binary, "0010"): 223 | hex = "2" 224 | case strings.EqualFold(binary, "0011"): 225 | hex = "3" 226 | case strings.EqualFold(binary, "0100"): 227 | hex = "4" 228 | case strings.EqualFold(binary, "0101"): 229 | hex = "5" 230 | case strings.EqualFold(binary, "0110"): 231 | hex = "6" 232 | case strings.EqualFold(binary, "0111"): 233 | hex = "7" 234 | case strings.EqualFold(binary, "1000"): 235 | hex = "8" 236 | case strings.EqualFold(binary, "1001"): 237 | hex = "9" 238 | case strings.EqualFold(binary, "1010"): 239 | hex = "A" 240 | case strings.EqualFold(binary, "1011"): 241 | hex = "B" 242 | case strings.EqualFold(binary, "1100"): 243 | hex = "C" 244 | case strings.EqualFold(binary, "1101"): 245 | hex = "D" 246 | case strings.EqualFold(binary, "1110"): 247 | hex = "E" 248 | case strings.EqualFold(binary, "1111"): 249 | hex = "F" 250 | } 251 | return hex 252 | } 253 | 254 | func bt64ToHex(byteData [64]int) string { 255 | var hex string 256 | for i := 0; i < 16; i++ { 257 | var bt string 258 | for j := 0; j < 4; j++ { 259 | bt += strconv.Itoa(byteData[i*4+j]) 260 | } 261 | hex += bt4ToHex(bt) 262 | } 263 | return hex 264 | } 265 | 266 | func enc(dataByte, keyByte [64]int) [64]int { 267 | keys := generateKeys(keyByte) 268 | ipByte := initPermute(dataByte) 269 | var ipLeft [32]int 270 | var ipRight [32]int 271 | var tempLeft [32]int 272 | for k := 0; k < 32; k++ { 273 | ipLeft[k] = ipByte[k] 274 | ipRight[k] = ipByte[32+k] 275 | } 276 | for i := 0; i < 16; i++ { 277 | for j := 0; j < 32; j++ { 278 | tempLeft[j] = ipLeft[j] 279 | ipLeft[j] = ipRight[j] 280 | } 281 | var key [48]int 282 | for m := 0; m < 48; m++ { 283 | key[m] = keys[i][m] 284 | } 285 | tempRight := xor32(pPermute(sBoxPermute(xor48(expandPermute(ipRight), key))), tempLeft) 286 | for n := 0; n < 32; n++ { 287 | ipRight[n] = tempRight[n] 288 | } 289 | 290 | } 291 | 292 | var finalData [64]int 293 | for i := 0; i < 32; i++ { 294 | finalData[i] = ipRight[i] 295 | finalData[32+i] = ipLeft[i] 296 | } 297 | return finallyPermute(finalData) 298 | } 299 | 300 | func initPermute(originalData [64]int) [64]int { 301 | var ipByte [64]int 302 | for i, m, n := 0, 1, 0; i < 4; i, m, n = i+1, m+2, n+2 { 303 | for j, k := 7, 0; j >= 0; j, k = j-1, k+1 { 304 | ipByte[i*8+k] = originalData[j*8+m] 305 | ipByte[i*8+k+32] = originalData[j*8+n] 306 | } 307 | } 308 | return ipByte 309 | } 310 | 311 | func expandPermute(rightData [32]int) [48]int { 312 | var epByte [48]int 313 | for i := 0; i < 8; i++ { 314 | if i == 0 { 315 | epByte[i*6+0] = rightData[31] 316 | } else { 317 | epByte[i*6+0] = rightData[i*4-1] 318 | } 319 | epByte[i*6+1] = rightData[i*4+0] 320 | epByte[i*6+2] = rightData[i*4+1] 321 | epByte[i*6+3] = rightData[i*4+2] 322 | epByte[i*6+4] = rightData[i*4+3] 323 | if i == 7 { 324 | epByte[i*6+5] = rightData[0] 325 | } else { 326 | epByte[i*6+5] = rightData[i*4+4] 327 | } 328 | } 329 | return epByte 330 | } 331 | 332 | func xor32(byteOne, byteTwo [32]int) [32]int { 333 | 334 | var xorByte [32]int 335 | for i := 0; i < 32; i++ { 336 | xorByte[i] = byteOne[i] ^ byteTwo[i] 337 | } 338 | return xorByte 339 | } 340 | 341 | func xor48(byteOne, byteTwo [48]int) [48]int { 342 | 343 | var xorByte [48]int 344 | for i := 0; i < len(byteOne); i++ { 345 | xorByte[i] = byteOne[i] ^ byteTwo[i] 346 | } 347 | return xorByte 348 | } 349 | 350 | func sBoxPermute(expandByte [48]int) [32]int { 351 | 352 | // var sBoxByte = new Array(32); 353 | var sBoxByte [32]int 354 | var binary string 355 | s1 := [4][16]int{{14, 4, 13, 1, 2, 15, 11, 8, 3, 10, 6, 12, 5, 9, 0, 7}, {0, 15, 7, 4, 14, 2, 13, 1, 10, 6, 12, 11, 9, 5, 3, 8}, 356 | {4, 1, 14, 8, 13, 6, 2, 11, 15, 12, 9, 7, 3, 10, 5, 0}, {15, 12, 8, 2, 4, 9, 1, 7, 5, 11, 3, 14, 10, 0, 6, 13}} 357 | 358 | /* Table - s2 */ 359 | s2 := [4][16]int{{15, 1, 8, 14, 6, 11, 3, 4, 9, 7, 2, 13, 12, 0, 5, 10}, {3, 13, 4, 7, 15, 2, 8, 14, 12, 0, 1, 10, 6, 9, 11, 5}, 360 | {0, 14, 7, 11, 10, 4, 13, 1, 5, 8, 12, 6, 9, 3, 2, 15}, {13, 8, 10, 1, 3, 15, 4, 2, 11, 6, 7, 12, 0, 5, 14, 9}} 361 | 362 | /* Table - s3 */ 363 | s3 := [4][16]int{{10, 0, 9, 14, 6, 3, 15, 5, 1, 13, 12, 7, 11, 4, 2, 8}, {13, 7, 0, 9, 3, 4, 6, 10, 2, 8, 5, 14, 12, 11, 15, 1}, 364 | {13, 6, 4, 9, 8, 15, 3, 0, 11, 1, 2, 12, 5, 10, 14, 7}, {1, 10, 13, 0, 6, 9, 8, 7, 4, 15, 14, 3, 11, 5, 2, 12}} 365 | /* Table - s4 */ 366 | s4 := [4][16]int{{7, 13, 14, 3, 0, 6, 9, 10, 1, 2, 8, 5, 11, 12, 4, 15}, {13, 8, 11, 5, 6, 15, 0, 3, 4, 7, 2, 12, 1, 10, 14, 9}, 367 | {10, 6, 9, 0, 12, 11, 7, 13, 15, 1, 3, 14, 5, 2, 8, 4}, {3, 15, 0, 6, 10, 1, 13, 8, 9, 4, 5, 11, 12, 7, 2, 14}} 368 | 369 | /* Table - s5 */ 370 | s5 := [4][16]int{{2, 12, 4, 1, 7, 10, 11, 6, 8, 5, 3, 15, 13, 0, 14, 9}, {14, 11, 2, 12, 4, 7, 13, 1, 5, 0, 15, 10, 3, 9, 8, 6}, 371 | {4, 2, 1, 11, 10, 13, 7, 8, 15, 9, 12, 5, 6, 3, 0, 14}, {11, 8, 12, 7, 1, 14, 2, 13, 6, 15, 0, 9, 10, 4, 5, 3}} 372 | 373 | /* Table - s6 */ 374 | s6 := [4][16]int{{12, 1, 10, 15, 9, 2, 6, 8, 0, 13, 3, 4, 14, 7, 5, 11}, {10, 15, 4, 2, 7, 12, 9, 5, 6, 1, 13, 14, 0, 11, 3, 8}, 375 | {9, 14, 15, 5, 2, 8, 12, 3, 7, 0, 4, 10, 1, 13, 11, 6}, {4, 3, 2, 12, 9, 5, 15, 10, 11, 14, 1, 7, 6, 0, 8, 13}} 376 | 377 | /* Table - s7 */ 378 | s7 := [4][16]int{{4, 11, 2, 14, 15, 0, 8, 13, 3, 12, 9, 7, 5, 10, 6, 1}, {13, 0, 11, 7, 4, 9, 1, 10, 14, 3, 5, 12, 2, 15, 8, 6}, 379 | {1, 4, 11, 13, 12, 3, 7, 14, 10, 15, 6, 8, 0, 5, 9, 2}, {6, 11, 13, 8, 1, 4, 10, 7, 9, 5, 0, 15, 14, 2, 3, 12}} 380 | 381 | /* Table - s8 */ 382 | s8 := [4][16]int{{13, 2, 8, 4, 6, 15, 11, 1, 10, 9, 3, 14, 5, 0, 12, 7}, {1, 15, 13, 8, 10, 3, 7, 4, 12, 5, 6, 11, 0, 14, 9, 2}, 383 | {7, 11, 4, 1, 9, 12, 14, 2, 0, 6, 10, 13, 15, 3, 5, 8}, {2, 1, 14, 7, 4, 10, 8, 13, 15, 12, 9, 0, 3, 5, 6, 11}} 384 | 385 | for m := 0; m < 8; m++ { 386 | i, j := 0, 0 387 | i = expandByte[m*6+0]*2 + expandByte[m*6+5] 388 | j = expandByte[m*6+1]*2*2*2 + expandByte[m*6+2]*2*2 + expandByte[m*6+3]*2 + expandByte[m*6+4] 389 | switch m { 390 | case 0: 391 | binary = getBoxBinary(s1[i][j]) 392 | break 393 | case 1: 394 | binary = getBoxBinary(s2[i][j]) 395 | break 396 | case 2: 397 | binary = getBoxBinary(s3[i][j]) 398 | break 399 | case 3: 400 | binary = getBoxBinary(s4[i][j]) 401 | break 402 | case 4: 403 | binary = getBoxBinary(s5[i][j]) 404 | break 405 | case 5: 406 | binary = getBoxBinary(s6[i][j]) 407 | break 408 | case 6: 409 | binary = getBoxBinary(s7[i][j]) 410 | break 411 | case 7: 412 | binary = getBoxBinary(s8[i][j]) 413 | break 414 | } 415 | sBoxByte[m*4+0], _ = strconv.Atoi(binary[0:1]) 416 | sBoxByte[m*4+1], _ = strconv.Atoi(binary[1:2]) 417 | sBoxByte[m*4+2], _ = strconv.Atoi(binary[2:3]) 418 | sBoxByte[m*4+3], _ = strconv.Atoi(binary[3:4]) 419 | } 420 | return sBoxByte 421 | } 422 | func pPermute(sBoxByte [32]int) [32]int { 423 | var pBoxPermute [32]int 424 | pBoxPermute[0] = sBoxByte[15] 425 | pBoxPermute[1] = sBoxByte[6] 426 | pBoxPermute[2] = sBoxByte[19] 427 | pBoxPermute[3] = sBoxByte[20] 428 | pBoxPermute[4] = sBoxByte[28] 429 | pBoxPermute[5] = sBoxByte[11] 430 | pBoxPermute[6] = sBoxByte[27] 431 | pBoxPermute[7] = sBoxByte[16] 432 | pBoxPermute[8] = sBoxByte[0] 433 | pBoxPermute[9] = sBoxByte[14] 434 | pBoxPermute[10] = sBoxByte[22] 435 | pBoxPermute[11] = sBoxByte[25] 436 | pBoxPermute[12] = sBoxByte[4] 437 | pBoxPermute[13] = sBoxByte[17] 438 | pBoxPermute[14] = sBoxByte[30] 439 | pBoxPermute[15] = sBoxByte[9] 440 | pBoxPermute[16] = sBoxByte[1] 441 | pBoxPermute[17] = sBoxByte[7] 442 | pBoxPermute[18] = sBoxByte[23] 443 | pBoxPermute[19] = sBoxByte[13] 444 | pBoxPermute[20] = sBoxByte[31] 445 | pBoxPermute[21] = sBoxByte[26] 446 | pBoxPermute[22] = sBoxByte[2] 447 | pBoxPermute[23] = sBoxByte[8] 448 | pBoxPermute[24] = sBoxByte[18] 449 | pBoxPermute[25] = sBoxByte[12] 450 | pBoxPermute[26] = sBoxByte[29] 451 | pBoxPermute[27] = sBoxByte[5] 452 | pBoxPermute[28] = sBoxByte[21] 453 | pBoxPermute[29] = sBoxByte[10] 454 | pBoxPermute[30] = sBoxByte[3] 455 | pBoxPermute[31] = sBoxByte[24] 456 | return pBoxPermute 457 | } 458 | 459 | func finallyPermute(endByte [64]int) [64]int { 460 | var fpByte [64]int 461 | fpByte[0] = endByte[39] 462 | fpByte[1] = endByte[7] 463 | fpByte[2] = endByte[47] 464 | fpByte[3] = endByte[15] 465 | fpByte[4] = endByte[55] 466 | fpByte[5] = endByte[23] 467 | fpByte[6] = endByte[63] 468 | fpByte[7] = endByte[31] 469 | fpByte[8] = endByte[38] 470 | fpByte[9] = endByte[6] 471 | fpByte[10] = endByte[46] 472 | fpByte[11] = endByte[14] 473 | fpByte[12] = endByte[54] 474 | fpByte[13] = endByte[22] 475 | fpByte[14] = endByte[62] 476 | fpByte[15] = endByte[30] 477 | fpByte[16] = endByte[37] 478 | fpByte[17] = endByte[5] 479 | fpByte[18] = endByte[45] 480 | fpByte[19] = endByte[13] 481 | fpByte[20] = endByte[53] 482 | fpByte[21] = endByte[21] 483 | fpByte[22] = endByte[61] 484 | fpByte[23] = endByte[29] 485 | fpByte[24] = endByte[36] 486 | fpByte[25] = endByte[4] 487 | fpByte[26] = endByte[44] 488 | fpByte[27] = endByte[12] 489 | fpByte[28] = endByte[52] 490 | fpByte[29] = endByte[20] 491 | fpByte[30] = endByte[60] 492 | fpByte[31] = endByte[28] 493 | fpByte[32] = endByte[35] 494 | fpByte[33] = endByte[3] 495 | fpByte[34] = endByte[43] 496 | fpByte[35] = endByte[11] 497 | fpByte[36] = endByte[51] 498 | fpByte[37] = endByte[19] 499 | fpByte[38] = endByte[59] 500 | fpByte[39] = endByte[27] 501 | fpByte[40] = endByte[34] 502 | fpByte[41] = endByte[2] 503 | fpByte[42] = endByte[42] 504 | fpByte[43] = endByte[10] 505 | fpByte[44] = endByte[50] 506 | fpByte[45] = endByte[18] 507 | fpByte[46] = endByte[58] 508 | fpByte[47] = endByte[26] 509 | fpByte[48] = endByte[33] 510 | fpByte[49] = endByte[1] 511 | fpByte[50] = endByte[41] 512 | fpByte[51] = endByte[9] 513 | fpByte[52] = endByte[49] 514 | fpByte[53] = endByte[17] 515 | fpByte[54] = endByte[57] 516 | fpByte[55] = endByte[25] 517 | fpByte[56] = endByte[32] 518 | fpByte[57] = endByte[0] 519 | fpByte[58] = endByte[40] 520 | fpByte[59] = endByte[8] 521 | fpByte[60] = endByte[48] 522 | fpByte[61] = endByte[16] 523 | fpByte[62] = endByte[56] 524 | fpByte[63] = endByte[24] 525 | return fpByte 526 | } 527 | 528 | func getBoxBinary(i int) string { 529 | var binary string 530 | switch i { 531 | case 0: 532 | binary = "0000" 533 | case 1: 534 | binary = "0001" 535 | case 2: 536 | binary = "0010" 537 | case 3: 538 | binary = "0011" 539 | case 4: 540 | binary = "0100" 541 | case 5: 542 | binary = "0101" 543 | case 6: 544 | binary = "0110" 545 | case 7: 546 | binary = "0111" 547 | case 8: 548 | binary = "1000" 549 | case 9: 550 | binary = "1001" 551 | case 10: 552 | binary = "1010" 553 | case 11: 554 | binary = "1011" 555 | case 12: 556 | binary = "1100" 557 | case 13: 558 | binary = "1101" 559 | case 14: 560 | binary = "1110" 561 | case 15: 562 | binary = "1111" 563 | } 564 | return binary 565 | } 566 | 567 | func generateKeys(keyByte [64]int) [16][48]int { 568 | var key [56]int 569 | var keys [16][48]int 570 | 571 | var loop = []int{1, 1, 2, 2, 2, 2, 2, 2, 1, 2, 2, 2, 2, 2, 2, 1} 572 | 573 | for i := 0; i < 7; i++ { 574 | for j, k := 0, 7; j < 8; j, k = j+1, k-1 { 575 | key[i*8+j] = keyByte[8*k+i] 576 | } 577 | } 578 | 579 | for i := 0; i < 16; i++ { 580 | var tempLeft, tempRight int 581 | for j := 0; j < loop[i]; j++ { 582 | tempLeft = key[0] 583 | tempRight = key[28] 584 | for k := 0; k < 27; k++ { 585 | key[k] = key[k+1] 586 | key[28+k] = key[29+k] 587 | } 588 | key[27] = tempLeft 589 | key[55] = tempRight 590 | } 591 | // var tempKey = new Array(48); 592 | var tempKey [48]int 593 | tempKey[0] = key[13] 594 | tempKey[1] = key[16] 595 | tempKey[2] = key[10] 596 | tempKey[3] = key[23] 597 | tempKey[4] = key[0] 598 | tempKey[5] = key[4] 599 | tempKey[6] = key[2] 600 | tempKey[7] = key[27] 601 | tempKey[8] = key[14] 602 | tempKey[9] = key[5] 603 | tempKey[10] = key[20] 604 | tempKey[11] = key[9] 605 | tempKey[12] = key[22] 606 | tempKey[13] = key[18] 607 | tempKey[14] = key[11] 608 | tempKey[15] = key[3] 609 | tempKey[16] = key[25] 610 | tempKey[17] = key[7] 611 | tempKey[18] = key[15] 612 | tempKey[19] = key[6] 613 | tempKey[20] = key[26] 614 | tempKey[21] = key[19] 615 | tempKey[22] = key[12] 616 | tempKey[23] = key[1] 617 | tempKey[24] = key[40] 618 | tempKey[25] = key[51] 619 | tempKey[26] = key[30] 620 | tempKey[27] = key[36] 621 | tempKey[28] = key[46] 622 | tempKey[29] = key[54] 623 | tempKey[30] = key[29] 624 | tempKey[31] = key[39] 625 | tempKey[32] = key[50] 626 | tempKey[33] = key[44] 627 | tempKey[34] = key[32] 628 | tempKey[35] = key[47] 629 | tempKey[36] = key[43] 630 | tempKey[37] = key[48] 631 | tempKey[38] = key[38] 632 | tempKey[39] = key[55] 633 | tempKey[40] = key[33] 634 | tempKey[41] = key[52] 635 | tempKey[42] = key[45] 636 | tempKey[43] = key[41] 637 | tempKey[44] = key[49] 638 | tempKey[45] = key[35] 639 | tempKey[46] = key[28] 640 | tempKey[47] = key[31] 641 | 642 | switch i { 643 | case 0: 644 | for m := 0; m < 48; m++ { 645 | keys[0][m] = tempKey[m] 646 | } 647 | case 1: 648 | for m := 0; m < 48; m++ { 649 | keys[1][m] = tempKey[m] 650 | } 651 | case 2: 652 | for m := 0; m < 48; m++ { 653 | keys[2][m] = tempKey[m] 654 | } 655 | case 3: 656 | for m := 0; m < 48; m++ { 657 | keys[3][m] = tempKey[m] 658 | } 659 | case 4: 660 | for m := 0; m < 48; m++ { 661 | keys[4][m] = tempKey[m] 662 | } 663 | case 5: 664 | for m := 0; m < 48; m++ { 665 | keys[5][m] = tempKey[m] 666 | } 667 | case 6: 668 | for m := 0; m < 48; m++ { 669 | keys[6][m] = tempKey[m] 670 | } 671 | case 7: 672 | for m := 0; m < 48; m++ { 673 | keys[7][m] = tempKey[m] 674 | } 675 | case 8: 676 | for m := 0; m < 48; m++ { 677 | keys[8][m] = tempKey[m] 678 | } 679 | case 9: 680 | for m := 0; m < 48; m++ { 681 | keys[9][m] = tempKey[m] 682 | } 683 | case 10: 684 | for m := 0; m < 48; m++ { 685 | keys[10][m] = tempKey[m] 686 | } 687 | case 11: 688 | for m := 0; m < 48; m++ { 689 | keys[11][m] = tempKey[m] 690 | } 691 | case 12: 692 | for m := 0; m < 48; m++ { 693 | keys[12][m] = tempKey[m] 694 | } 695 | case 13: 696 | for m := 0; m < 48; m++ { 697 | keys[13][m] = tempKey[m] 698 | } 699 | case 14: 700 | for m := 0; m < 48; m++ { 701 | keys[14][m] = tempKey[m] 702 | } 703 | case 15: 704 | for m := 0; m < 48; m++ { 705 | keys[15][m] = tempKey[m] 706 | } 707 | } 708 | } 709 | return keys 710 | } 711 | -------------------------------------------------------------------------------- /cet/cet4.go: -------------------------------------------------------------------------------- 1 | package cet 2 | -------------------------------------------------------------------------------- /cet/cet6.go: -------------------------------------------------------------------------------- 1 | package cet 2 | -------------------------------------------------------------------------------- /cet/query.go: -------------------------------------------------------------------------------- 1 | package cet 2 | 3 | // 成绩查询入口: 4 | -------------------------------------------------------------------------------- /cet/readme.md: -------------------------------------------------------------------------------- 1 | # CET 全国大学英语四、六级考试 2 | 3 | ## 接口 4 | 5 | - [ ] 报名 6 | - [ ] 考试安排查询 7 | - [ ] 考试成绩查询 8 | -------------------------------------------------------------------------------- /chaoxing/chaoxing.go: -------------------------------------------------------------------------------- 1 | package chaoxing 2 | 3 | import ( 4 | "fmt" 5 | "github.com/hduLib/hdu/chaoxing/course" 6 | "github.com/hduLib/hdu/chaoxing/course/work" 7 | "github.com/hduLib/hdu/chaoxing/request" 8 | "github.com/hduLib/hdu/client" 9 | "io" 10 | "net/http" 11 | ) 12 | 13 | type Cx struct { 14 | req *request.Request 15 | } 16 | 17 | func newUser(ck []*http.Cookie) *Cx { 18 | return &Cx{req: &request.Request{Cookies: ck}} 19 | } 20 | 21 | func (cx *Cx) CourseList() (*course.List, error) { 22 | resp, err := cx.req.Get(courseListURL()) 23 | if err != nil { 24 | return nil, err 25 | } 26 | list, err := course.NewCourseList(resp, cx.req) 27 | if err != nil { 28 | return nil, fmt.Errorf("fail to parse courselist:%v", err) 29 | } 30 | return list, nil 31 | } 32 | 33 | func todoListURL() string { 34 | return fmt.Sprintf("https://home-yd.chaoxing.com/proxy/gopage?cxanalyzetag=hp&type=mywork") 35 | } 36 | 37 | // WorkList 是手机api的作业列表 38 | func (cx *Cx) WorkList() (*work.List, error) { 39 | req, err := cx.req.NewGet(todoListURL()) 40 | if err != nil { 41 | return nil, err 42 | } 43 | req.Header.Set("User-Agent", "Mozilla/5.0 (iPhone; CPU iPhone OS 16_3_1 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Mobile/15E148 (schild:0adc7adb7f466d4df73716e19c87efaa) (device:iPhone14,5) Language/zh-Hans com.ssreader.ChaoXingStudy/ChaoXingStudy_3_6.0.6_ios_phone_202304130930_102 (@Kalimdor)_9407410973156787895") 44 | resp, err := client.Do(req) 45 | if err != nil { 46 | return nil, err 47 | } 48 | defer resp.Body.Close() 49 | body, _ := io.ReadAll(resp.Body) 50 | if resp.StatusCode != http.StatusOK { 51 | return nil, &client.ErrNotOk{ 52 | StatusCode: resp.StatusCode, 53 | Body: string(body), 54 | } 55 | } 56 | return work.NewListPhoneAPI(body, cx.req) 57 | } 58 | -------------------------------------------------------------------------------- /chaoxing/course/chapter/chapter.go: -------------------------------------------------------------------------------- 1 | package chapter 2 | 3 | type Chapter struct { 4 | } 5 | -------------------------------------------------------------------------------- /chaoxing/course/chapter/chapter_brief.go: -------------------------------------------------------------------------------- 1 | package chapter 2 | 3 | type Brief struct { 4 | } 5 | -------------------------------------------------------------------------------- /chaoxing/course/chapter/chapter_list.go: -------------------------------------------------------------------------------- 1 | package chapter 2 | 3 | import ( 4 | "bytes" 5 | "github.com/PuerkitoBio/goquery" 6 | "github.com/hduLib/hdu/chaoxing/request" 7 | ) 8 | 9 | type List struct { 10 | Chapters []Brief 11 | } 12 | 13 | func NewList(resp []byte, req *request.Request) (*List, error) { 14 | doc, err := goquery.NewDocumentFromReader(bytes.NewReader(resp)) 15 | if err != nil { 16 | return nil, err 17 | } 18 | chapters := doc.Find(".chapter_item") 19 | chapters.Each(func(_ int, selection *goquery.Selection) { 20 | //todo: parse chapter 21 | }) 22 | return nil, nil 23 | } 24 | -------------------------------------------------------------------------------- /chaoxing/course/course.go: -------------------------------------------------------------------------------- 1 | package course 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/PuerkitoBio/goquery" 7 | "github.com/hduLib/hdu/chaoxing/course/chapter" 8 | "github.com/hduLib/hdu/chaoxing/course/exam" 9 | "github.com/hduLib/hdu/chaoxing/course/work" 10 | "github.com/hduLib/hdu/chaoxing/request" 11 | "github.com/hduLib/hdu/chaoxing/utils" 12 | ) 13 | 14 | type Course struct { 15 | ClazzId string 16 | CourseId string 17 | CoverURL string 18 | Title string 19 | Duration string 20 | TeacherName string 21 | CourseNum string 22 | cpi string 23 | cfid string 24 | bbsid string 25 | heardUt string 26 | fid string 27 | opEnc string 28 | enc string 29 | oldEnc string 30 | workEnc string 31 | examEnc string 32 | v string 33 | t string 34 | courseEvaluateUrl string 35 | req *request.Request 36 | } 37 | 38 | func NewCourse(resp []byte, cb *Brief) (*Course, error) { 39 | doc, err := goquery.NewDocumentFromReader(bytes.NewReader(resp)) 40 | if err != nil { 41 | return nil, fmt.Errorf("fail to parse resp:%v", err) 42 | } 43 | return &Course{ 44 | ClazzId: cb.ClazzId, 45 | CourseId: cb.CourseId, 46 | CoverURL: cb.CoverURL, 47 | Title: cb.Title, 48 | Duration: cb.Duration, 49 | TeacherName: cb.TeacherName, 50 | CourseNum: cb.CourseNum, 51 | cpi: utils.GetValueAttrBySelector(doc, "#cpi"), 52 | cfid: utils.GetValueAttrBySelector(doc, "#cfid"), 53 | bbsid: utils.GetValueAttrBySelector(doc, "#bbsid"), 54 | heardUt: utils.GetValueAttrBySelector(doc, "#heardUt"), 55 | fid: utils.GetValueAttrBySelector(doc, "#fid"), 56 | opEnc: utils.GetValueAttrBySelector(doc, "#openc"), 57 | enc: utils.GetValueAttrBySelector(doc, "#enc"), 58 | oldEnc: utils.GetValueAttrBySelector(doc, "#oldenc"), 59 | workEnc: utils.GetValueAttrBySelector(doc, "#workEnc"), 60 | examEnc: utils.GetValueAttrBySelector(doc, "#examEnc"), 61 | v: utils.GetValueAttrBySelector(doc, "#v"), 62 | t: utils.GetValueAttrBySelector(doc, "#t"), 63 | courseEvaluateUrl: utils.GetValueAttrBySelector(doc, "#courseEvaluateUrl"), 64 | req: cb.req, 65 | }, nil 66 | } 67 | 68 | func (c *Course) WorkList() (*work.List, error) { 69 | resp, err := c.req.Get(c.workListURL()) 70 | if err != nil { 71 | return nil, err 72 | } 73 | return work.NewList(resp, c.req) 74 | } 75 | 76 | func (c *Course) ExamList() (*exam.List, error) { 77 | resp, err := c.req.Get(c.examListURL()) 78 | if err != nil { 79 | return nil, err 80 | } 81 | return exam.NewList(resp, c.req, c.cpi, c.ClazzId) 82 | } 83 | 84 | func (c *Course) ChapterList() (*chapter.List, error) { 85 | resp, err := c.req.Get(c.chapterListURL()) 86 | if err != nil { 87 | return nil, err 88 | } 89 | return chapter.NewList(resp, c.req) 90 | } 91 | -------------------------------------------------------------------------------- /chaoxing/course/course_brief.go: -------------------------------------------------------------------------------- 1 | package course 2 | 3 | import ( 4 | "github.com/hduLib/hdu/chaoxing/request" 5 | ) 6 | 7 | type Brief struct { 8 | ClazzId string 9 | CourseId string 10 | CoverURL string 11 | Title string 12 | // Duration 意义不大,不同老师填的不同,不推荐使用 13 | Duration string 14 | TeacherName string 15 | // CourseNum 可能是上课地点可能是课程号,感觉全看教师自己填了什么,不推荐使用 16 | CourseNum string 17 | url string 18 | // point to cx for getting further information 19 | req *request.Request 20 | //本来应该有一个名为cpi的字段,但是仅仅出现在url内并且没有摸清他的意义,暂时不予理会 21 | } 22 | 23 | // Detail returns detailed Course for further request 24 | func (br *Brief) Detail() (*Course, error) { 25 | resp, err := br.req.Get(br.url) 26 | if err != nil { 27 | return nil, err 28 | } 29 | return NewCourse(resp, br) 30 | } 31 | -------------------------------------------------------------------------------- /chaoxing/course/course_list.go: -------------------------------------------------------------------------------- 1 | package course 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/PuerkitoBio/goquery" 7 | "github.com/hduLib/hdu/chaoxing/request" 8 | "github.com/hduLib/hdu/chaoxing/utils" 9 | "log" 10 | "strings" 11 | ) 12 | 13 | type List struct { 14 | Courses []Brief 15 | } 16 | 17 | // todo 18 | func (l *List) FindByName(name string) *Brief { 19 | var res *Brief 20 | l.Each(func(course *Brief) bool { 21 | if course.Title == name { 22 | res = course 23 | return false 24 | } 25 | return true 26 | }) 27 | return res 28 | } 29 | 30 | func (l *List) Each(f func(course *Brief) bool) { 31 | for i := range l.Courses { 32 | if !f(&l.Courses[i]) { 33 | return 34 | } 35 | } 36 | } 37 | 38 | func NewCourseList(resp []byte, req *request.Request) (*List, error) { 39 | var list List 40 | doc, err := goquery.NewDocumentFromReader(bytes.NewReader(resp)) 41 | if err != nil { 42 | return nil, err 43 | } 44 | doc.Find(".learnCourse").Each(func(_ int, selection *goquery.Selection) { 45 | url, exist := selection.Find("a.color1").Attr("href") 46 | if !exist { 47 | log.Println("course url not existed") 48 | } 49 | imgUrl, exist := selection.Find("img").Attr("src") 50 | if !exist { 51 | log.Println("cover url not existed") 52 | } 53 | title := strings.TrimSpace(selection.Find("span").Contents().Text()) 54 | info := selection.Find(".color3") 55 | teacher := info.Contents().Text() 56 | info = info.Next() 57 | var dur string 58 | fmt.Sscanf(info.Contents().Text(), "开课时间:%s", &dur) 59 | var CourseNum string 60 | fmt.Sscanf(info.Next().Contents().Text(), "班级:%s", &CourseNum) 61 | list.Courses = append(list.Courses, Brief{ 62 | ClazzId: utils.GetValueAttrBySelector(selection, ".clazzId"), 63 | CourseId: utils.GetValueAttrBySelector(selection, ".courseId"), 64 | CoverURL: imgUrl, 65 | url: url, 66 | Title: title, 67 | Duration: dur, 68 | TeacherName: teacher, 69 | CourseNum: CourseNum, 70 | req: req, 71 | }) 72 | }) 73 | return &list, nil 74 | } 75 | -------------------------------------------------------------------------------- /chaoxing/course/data.go: -------------------------------------------------------------------------------- 1 | package course 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | func (c *Course) workListURL() string { 9 | return fmt.Sprintf("https://mooc1.chaoxing.com/mooc2/work/list?courseId=%s&classId=%s&cpi=%s&ut=%s&enc=%s", c.CourseId, c.ClazzId, c.cpi, c.heardUt, c.workEnc) 10 | } 11 | 12 | func (c *Course) examListURL() string { 13 | return fmt.Sprintf("https://mooc1.chaoxing.com/exam-ans/mooc2/exam/exam-list?courseid=%s&clazzid=%s&cpi=%s&ut=%s&t=%s&enc=%s&openc=%s", c.CourseId, c.ClazzId, c.cpi, c.heardUt, c.t, c.enc, c.opEnc) 14 | } 15 | 16 | func (c *Course) chapterListURL() string { 17 | return fmt.Sprintf("https://mooc2-ans.chaoxing.com/mooc2-ans/mycourse/studentcourse?courseid=%s&clazzid=%s&cpi=%s&ut=%s&t=%d", c.CourseId, c.ClazzId, c.cpi, c.heardUt, time.Now().UnixMilli()) 18 | } 19 | -------------------------------------------------------------------------------- /chaoxing/course/exam/exam.go: -------------------------------------------------------------------------------- 1 | package exam 2 | 3 | type Exam struct { 4 | } 5 | 6 | func New() { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /chaoxing/course/exam/exam_brief.go: -------------------------------------------------------------------------------- 1 | package exam 2 | 3 | import ( 4 | "github.com/hduLib/hdu/chaoxing/request" 5 | "time" 6 | ) 7 | 8 | const ( 9 | Undo = "待做" 10 | Finished = "已完成" 11 | ) 12 | 13 | type Brief struct { 14 | url string 15 | Title string 16 | // 根据剩余时间推断,可能有±1分钟的误差,对已完成考试无法获取截止时间 17 | // 精确数据请先打开考试 18 | Time time.Time 19 | // 待做、已完成 20 | Status string 21 | req *request.Request 22 | } 23 | 24 | func (b *Brief) Open() (*Exam, error) { 25 | //todo: open a exam 26 | return nil, nil 27 | } 28 | -------------------------------------------------------------------------------- /chaoxing/course/exam/exam_list.go: -------------------------------------------------------------------------------- 1 | package exam 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "github.com/PuerkitoBio/goquery" 7 | "github.com/hduLib/hdu/chaoxing/request" 8 | "github.com/hduLib/hdu/chaoxing/utils" 9 | "strings" 10 | ) 11 | 12 | type List struct { 13 | Exams []Brief 14 | } 15 | 16 | func NewList(resp []byte, req *request.Request, cpi, classId string) (*List, error) { 17 | doc, err := goquery.NewDocumentFromReader(bytes.NewReader(resp)) 18 | if err != nil { 19 | return nil, err 20 | } 21 | var list List 22 | doc.Find("li").Each(func(i int, selection *goquery.Selection) { 23 | status := selection.Find(".status").Contents().Text() 24 | onclickNode := selection.Contents().Get(1) 25 | if len(onclickNode.Attr) < 2 { 26 | return 27 | } 28 | onclick := onclickNode.Attr[1].Val 29 | var courseId, tId, paperId, lookpaperEnc, url string 30 | if len(onclick) > 10 { 31 | subs := strings.Split(onclick[8:len(onclick)-2], ",") 32 | if len(subs) == 7 { 33 | courseId = strings.Trim(subs[0], "'") 34 | tId = subs[1] 35 | //id := subs[2] 36 | //endTime := strings.Trim(subs[3], "'") 37 | paperId = subs[4] 38 | //isRetest := false 39 | //if subs[5] == "true" { 40 | // isRetest = true 41 | //} 42 | lookpaperEnc = strings.Trim(subs[6], "'") 43 | url = fmt.Sprintf("https://mooc2-ans.chaoxing.com/exam-ans/exam/lookPaper?courseId=%s&classId=%s&paperId=%s&position=test&examRelationId=%s&cpi=%s&enc=%s&newMooc=true", courseId, classId, paperId, tId, cpi, lookpaperEnc) 44 | } 45 | } 46 | list.Exams = append(list.Exams, Brief{ 47 | url: url, 48 | Title: selection.Find(".overHidden2").Contents().Text(), 49 | Time: utils.ParseLeftTime2Deadline(strings.TrimSpace(selection.Find(".time").Contents().Text())), 50 | Status: status, 51 | req: req, 52 | }) 53 | }) 54 | return &list, nil 55 | } 56 | -------------------------------------------------------------------------------- /chaoxing/course/work/work.go: -------------------------------------------------------------------------------- 1 | package work 2 | 3 | type Work struct { 4 | } 5 | 6 | func New() { 7 | 8 | } 9 | -------------------------------------------------------------------------------- /chaoxing/course/work/work_brief.go: -------------------------------------------------------------------------------- 1 | package work 2 | 3 | import ( 4 | "github.com/hduLib/hdu/chaoxing/request" 5 | "time" 6 | ) 7 | 8 | type Brief struct { 9 | url string 10 | Title string 11 | // 根据剩余时间推断,可能有±1分钟的误差,对已完成作业无法获取截止时间 12 | // 精确数据请先打开作业 13 | Time time.Time 14 | // 未交(手机作业列表API->未完成)、已完成、待批阅 15 | Status string 16 | ClazzId string 17 | req *request.Request 18 | } 19 | 20 | func (b *Brief) Detail() *Work { 21 | _, err := b.req.Get(b.url) 22 | if err != nil { 23 | return nil 24 | } 25 | // todo: course detail, 其实也不一定会做,因为这个东西做出来也很难应用,brief就够了 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /chaoxing/course/work/work_list.go: -------------------------------------------------------------------------------- 1 | package work 2 | 3 | import ( 4 | "bytes" 5 | "github.com/PuerkitoBio/goquery" 6 | "github.com/hduLib/hdu/chaoxing/request" 7 | "github.com/hduLib/hdu/chaoxing/utils" 8 | "log" 9 | "strings" 10 | ) 11 | 12 | type List struct { 13 | Works []Brief 14 | } 15 | 16 | func NewList(resp []byte, req *request.Request) (*List, error) { 17 | doc, err := goquery.NewDocumentFromReader(bytes.NewReader(resp)) 18 | if err != nil { 19 | return nil, err 20 | } 21 | var list List 22 | doc.Find("li").Each(func(i int, selection *goquery.Selection) { 23 | url, exist := selection.Attr("data") 24 | if !exist { 25 | log.Println("url not exist") 26 | } 27 | list.Works = append(list.Works, Brief{ 28 | url: url, 29 | Title: selection.Find(".overHidden2").Contents().Text(), 30 | Time: utils.ParseLeftTime2Deadline(strings.TrimSpace(selection.Find(".time").Contents().Text())), 31 | Status: selection.Find(".status").Contents().Text(), 32 | req: req, 33 | }) 34 | }) 35 | return &list, nil 36 | } 37 | 38 | func NewListPhoneAPI(resp []byte, req *request.Request) (*List, error) { 39 | doc, err := goquery.NewDocumentFromReader(bytes.NewReader(resp)) 40 | if err != nil { 41 | return nil, err 42 | } 43 | var list List 44 | doc.Find("li").Each(func(i int, selection *goquery.Selection) { 45 | url, exist := selection.Attr("data") 46 | if !exist { 47 | log.Println("url not exist") 48 | } 49 | pos := strings.Index(url, "clazzId=") 50 | 51 | list.Works = append(list.Works, Brief{ 52 | url: url, 53 | ClazzId: url[pos+8 : pos+16], 54 | Title: selection.Find("p").Contents().Text(), 55 | Time: utils.ParseLeftTime2Deadline(strings.TrimSpace(selection.Find(".fr").Contents().Text())), 56 | Status: selection.Find(".status").Contents().Text(), 57 | req: req, 58 | }) 59 | }) 60 | return &list, nil 61 | } 62 | -------------------------------------------------------------------------------- /chaoxing/cx_test.go: -------------------------------------------------------------------------------- 1 | package chaoxing 2 | 3 | import ( 4 | "fmt" 5 | "github.com/hduLib/hdu/client" 6 | "os" 7 | "testing" 8 | ) 9 | 10 | var phone = os.Getenv("phone") 11 | var passwd = os.Getenv("passwd") 12 | var id = os.Getenv("id") 13 | var casPasswd = os.Getenv("casPasswd") 14 | 15 | func TestLogin(t *testing.T) { 16 | user, err := LoginWithPhoneAndPwd(phone, passwd) 17 | if err != nil { 18 | t.Error(err) 19 | return 20 | } 21 | for _, v := range user.req.Cookies { 22 | t.Log(v.String()) 23 | } 24 | } 25 | 26 | func TestLoginWithCas(t *testing.T) { 27 | user, err := LoginWithCas(id, casPasswd) 28 | if err != nil { 29 | if err, ok := err.(*client.ErrNotOk); ok { 30 | fmt.Println(err.Body) 31 | } else { 32 | t.Error(err) 33 | } 34 | 35 | return 36 | } 37 | for _, v := range user.req.Cookies { 38 | t.Log(v.String()) 39 | } 40 | } 41 | 42 | func TestCourseAndExam(t *testing.T) { 43 | user, err := LoginWithPhoneAndPwd(phone, passwd) 44 | if err != nil { 45 | t.Error(err) 46 | return 47 | } 48 | list, err := user.CourseList() 49 | if err != nil { 50 | t.Error(err) 51 | return 52 | } 53 | c, err := list.FindByName("计算机平面动画设计与制作").Detail() 54 | if err != nil { 55 | t.Error(err) 56 | return 57 | } 58 | workList, err := c.WorkList() 59 | if err != nil { 60 | t.Error(err) 61 | return 62 | } 63 | t.Log(workList) 64 | examList, err := c.ExamList() 65 | if err != nil { 66 | t.Error(err) 67 | return 68 | } 69 | t.Log(examList) 70 | } 71 | 72 | func TestWork_Detail(t *testing.T) { 73 | user, err := LoginWithPhoneAndPwd(phone, passwd) 74 | if err != nil { 75 | t.Error(err) 76 | return 77 | } 78 | list, err := user.CourseList() 79 | if err != nil { 80 | t.Error(err) 81 | return 82 | } 83 | c, err := list.FindByName("创新实践B").Detail() 84 | if err != nil { 85 | t.Error(err) 86 | return 87 | } 88 | workList, err := c.WorkList() 89 | if err != nil { 90 | t.Error(err) 91 | return 92 | } 93 | wk := workList.Works[0].Detail() 94 | fmt.Println(wk) 95 | } 96 | 97 | func TestCourseChapter_NewList(t *testing.T) { 98 | user, err := LoginWithPhoneAndPwd(phone, passwd) 99 | if err != nil { 100 | t.Error(err) 101 | return 102 | } 103 | list, err := user.CourseList() 104 | if err != nil { 105 | t.Error(err) 106 | return 107 | } 108 | c, err := list.FindByName("计算机平面动画设计与制作").Detail() 109 | if err != nil { 110 | t.Error(err) 111 | return 112 | } 113 | chapter, err := c.ChapterList() 114 | if err != nil { 115 | t.Error(err) 116 | return 117 | } 118 | t.Log(chapter) 119 | } 120 | 121 | func TestCx_WorkList(t *testing.T) { 122 | user, err := LoginWithPhoneAndPwd(phone, passwd) 123 | if err != nil { 124 | t.Error(err) 125 | return 126 | } 127 | list, err := user.WorkList() 128 | if err != nil { 129 | t.Error(err) 130 | return 131 | } 132 | t.Log(list) 133 | } 134 | -------------------------------------------------------------------------------- /chaoxing/data.go: -------------------------------------------------------------------------------- 1 | package chaoxing 2 | 3 | import ( 4 | "fmt" 5 | "time" 6 | ) 7 | 8 | const ( 9 | fanyaLoginURL = "http://passport2.chaoxing.com/fanyalogin" 10 | ssoLoginURL = "https://cas.hdu.edu.cn/cas/login?service=http://hdu.fanya.chaoxing.com/sso/hdu" 11 | ssoSuccessURL = "http://hdu.fanya.chaoxing.com/portal" 12 | ) 13 | 14 | func courseListURL() string { 15 | return fmt.Sprintf("http://mooc2-ans.chaoxing.com/mooc2-ans/visit/courses/list?v=%d&rss=1&start=0&size=500&catalogId=0&superstarClass=0&searchname=", time.Now().UnixMilli()) 16 | } 17 | -------------------------------------------------------------------------------- /chaoxing/login.go: -------------------------------------------------------------------------------- 1 | package chaoxing 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "fmt" 7 | "github.com/hduLib/hdu/cas" 8 | "github.com/hduLib/hdu/chaoxing/utils" 9 | "github.com/hduLib/hdu/client" 10 | "io" 11 | "net/http" 12 | "net/url" 13 | "strings" 14 | ) 15 | 16 | type loginResp struct { 17 | Msg string `json:"msg2"` 18 | Status bool `json:"status"` 19 | Url string `json:"url"` 20 | } 21 | 22 | func LoginWithPhoneAndPwd(phone string, passwd string) (*Cx, error) { 23 | payload := url.Values{} 24 | payload.Set("fid", "1001") 25 | payload.Set("uname", utils.EncryptByAES(phone)) 26 | payload.Set("password", utils.EncryptByAES(passwd)) 27 | payload.Set("refer", "http://i.mooc.chaoxing.com") 28 | payload.Set("t", "true") 29 | payload.Set("doubleFactorLogin", "0") 30 | payload.Set("independentId", "0") 31 | payload.Set("validate", "") 32 | 33 | req, err := http.NewRequest(http.MethodPost, fanyaLoginURL, strings.NewReader(payload.Encode())) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | req.Header.Set("Referer", "http://passport2.chaoxing.com/login?loginType=4&newversion=true&fid=1001&refer=http://i.mooc.chaoxing.com") 39 | req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36 Edg/106.0.1370.52") 40 | req.Header.Set("X-Requested-With", "XMLHttpRequest") 41 | req.Header.Set("Content-Type", "application/x-www-form-urlencoded") 42 | 43 | resp, err := client.Do(req) 44 | if err != nil { 45 | return nil, fmt.Errorf("request fail:%v", err) 46 | } 47 | if resp.StatusCode != http.StatusOK { 48 | return nil, fmt.Errorf("request fail:http code is %d", resp.StatusCode) 49 | } 50 | 51 | body, err := io.ReadAll(resp.Body) 52 | if err != nil { 53 | return nil, err 54 | } 55 | lres := &loginResp{} 56 | if err := json.Unmarshal(body, lres); err != nil { 57 | return nil, err 58 | } 59 | if !lres.Status { 60 | return nil, fmt.Errorf("login fail:%s", lres.Msg) 61 | } 62 | return newUser(resp.Cookies()), nil 63 | } 64 | 65 | func LoginWithCas(user, passwd string) (*Cx, error) { 66 | req, err := cas.GenLoginReq(ssoLoginURL, user, passwd) 67 | if err != nil { 68 | return nil, fmt.Errorf("fail to gen login request:%v", err) 69 | } 70 | resp, err := client.Do(req) 71 | if err != nil { 72 | return nil, err 73 | } 74 | if resp.StatusCode != http.StatusOK { 75 | body, err := io.ReadAll(resp.Body) 76 | if err != nil { 77 | return nil, err 78 | } 79 | return nil, &client.ErrNotOk{StatusCode: resp.StatusCode, Body: string(body)} 80 | } 81 | if resp.Request.URL.String() != ssoSuccessURL { 82 | return nil, errors.New("invalid id or password") 83 | } 84 | return newUser(resp.Request.Cookies()), nil 85 | } 86 | -------------------------------------------------------------------------------- /chaoxing/request/request.go: -------------------------------------------------------------------------------- 1 | package request 2 | 3 | import ( 4 | "bytes" 5 | "github.com/hduLib/hdu/client" 6 | "io" 7 | "net/http" 8 | ) 9 | 10 | type Request struct { 11 | Cookies []*http.Cookie 12 | } 13 | 14 | func (r *Request) AddCookieAndHeader2Req(req *http.Request) { 15 | for _, v := range r.Cookies { 16 | req.AddCookie(v) 17 | } 18 | req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36 Edg/106.0.1370.52") 19 | } 20 | 21 | func (r *Request) NewGet(url string) (*http.Request, error) { 22 | req, err := http.NewRequest(http.MethodGet, url, nil) 23 | if err != nil { 24 | return nil, err 25 | } 26 | r.AddCookieAndHeader2Req(req) 27 | return req, nil 28 | } 29 | 30 | func (r *Request) NewPost(url string, body []byte) (*http.Request, error) { 31 | req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) 32 | if err != nil { 33 | return nil, err 34 | } 35 | r.AddCookieAndHeader2Req(req) 36 | return req, nil 37 | } 38 | 39 | func (r *Request) Get(url string) ([]byte, error) { 40 | req, err := r.NewGet(url) 41 | if err != nil { 42 | return nil, err 43 | } 44 | resp, err := client.Do(req) 45 | if err != nil { 46 | return nil, err 47 | } 48 | defer resp.Body.Close() 49 | if resp.StatusCode != http.StatusOK { 50 | body, _ := io.ReadAll(resp.Body) 51 | return nil, &client.ErrNotOk{ 52 | StatusCode: resp.StatusCode, 53 | Body: string(body), 54 | } 55 | } 56 | return io.ReadAll(resp.Body) 57 | } 58 | 59 | func (r *Request) Post(url string, data []byte) ([]byte, error) { 60 | req, err := r.NewPost(url, data) 61 | if err != nil { 62 | return nil, err 63 | } 64 | return client.Post(req) 65 | } 66 | -------------------------------------------------------------------------------- /chaoxing/utils/utils.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import ( 4 | "bytes" 5 | "crypto/aes" 6 | "crypto/cipher" 7 | "encoding/base64" 8 | "fmt" 9 | "github.com/PuerkitoBio/goquery" 10 | "log" 11 | "strings" 12 | "time" 13 | ) 14 | 15 | const key = "u2oh6Vu^HWe4_AES" 16 | 17 | type toFind interface { 18 | Find(selector string) *goquery.Selection 19 | } 20 | 21 | func EncryptByAES(msg string) string { 22 | block, _ := aes.NewCipher([]byte(key)) 23 | cbc := cipher.NewCBCEncrypter(block, []byte(key)) 24 | padding := aes.BlockSize - len(msg)%aes.BlockSize 25 | src := append([]byte(msg), bytes.Repeat([]byte{byte(padding)}, padding)...) 26 | dst := make([]byte, len(src)) 27 | cbc.CryptBlocks(dst, src) 28 | return base64.StdEncoding.EncodeToString(dst) 29 | } 30 | 31 | func GetValueAttrBySelector(doc toFind, sel string) string { 32 | val, exist := doc.Find(sel).Attr("value") 33 | if !exist { 34 | log.Printf("%s not existed\n", val) 35 | } 36 | return val 37 | } 38 | 39 | func ParseLeftTime2Deadline(t string) time.Time { 40 | n := time.Now() 41 | if len(t) < 2 { 42 | return time.Unix(0, 0) 43 | } 44 | if strings.Contains(t, "小时") { 45 | var hour, minute int 46 | fmt.Sscanf(t, "剩余%d小时%d分钟", &hour, &minute) 47 | n = n.Add(time.Duration(hour)*time.Hour + time.Duration(minute)*time.Minute) 48 | } else { 49 | var minute int 50 | fmt.Sscanf(t, "剩余%d分钟", &minute) 51 | n = n.Add(time.Duration(minute) * time.Minute) 52 | } 53 | if n.Second() != 0 { 54 | n = n.Add(time.Minute - time.Duration(n.Second())*time.Second) 55 | } 56 | return n.Round(time.Second) 57 | } 58 | -------------------------------------------------------------------------------- /chaoxing/utils/utils_test.go: -------------------------------------------------------------------------------- 1 | package utils 2 | 3 | import "testing" 4 | 5 | func TestEncAes(t *testing.T) { 6 | t.Log(EncryptByAES("123123123") == "GszvoF3bQseqgnnv/WhUZA==") 7 | } 8 | -------------------------------------------------------------------------------- /client/client.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "errors" 5 | "net/http" 6 | ) 7 | 8 | type Client interface { 9 | Do(r *http.Request) (*http.Response, error) 10 | } 11 | 12 | // DefaultClient do all requests, you can change it as you implement the interface. 13 | var DefaultClient Client = CommonClient 14 | 15 | // CommonClient is set as DefaultClient default 16 | // you maybe puzzled about CheckRedirect is used instead of cookieJar. 17 | // it's because cookieJar is global, a cookie from one request may affect in another way 18 | // that never be considered. 19 | var CommonClient = &http.Client{ 20 | CheckRedirect: func(req *http.Request, via []*http.Request) error { 21 | for _, v := range append(via[len(via)-1].Cookies(), req.Response.Cookies()...) { 22 | ck, err := req.Cookie(v.Name) 23 | if err != nil { 24 | if errors.Is(err, http.ErrNoCookie) { 25 | req.AddCookie(v) 26 | } else { 27 | return err 28 | } 29 | continue 30 | } 31 | ck.Value = v.Value 32 | } 33 | return nil 34 | }, 35 | } 36 | -------------------------------------------------------------------------------- /client/err.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import "fmt" 4 | 5 | type ErrNotOk struct { 6 | StatusCode int 7 | Body string 8 | } 9 | 10 | // for further err, do type assertion 11 | func (e *ErrNotOk) Error() string { 12 | return fmt.Sprintf("status code is %d", e.StatusCode) 13 | } 14 | -------------------------------------------------------------------------------- /client/lowNetworkClient.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | ) 7 | 8 | // LowNetworkClient example 9 | type LowNetworkClient struct { 10 | http.Client 11 | retry int 12 | } 13 | 14 | func NewLowNetworkClient(timeout time.Duration, retry int) *LowNetworkClient { 15 | return &LowNetworkClient{ 16 | http.Client{ 17 | Timeout: timeout, 18 | CheckRedirect: CommonClient.CheckRedirect, 19 | }, 20 | retry, 21 | } 22 | } 23 | 24 | // Do is rewritten to retry 25 | func (lc *LowNetworkClient) Do(r *http.Request) (*http.Response, error) { 26 | var ( 27 | err error 28 | resp *http.Response 29 | ) 30 | for i := 0; i < lc.retry; i++ { 31 | resp, err = lc.Client.Do(r) 32 | if err == nil { 33 | return resp, nil 34 | } 35 | } 36 | return nil, err 37 | } 38 | -------------------------------------------------------------------------------- /client/request.go: -------------------------------------------------------------------------------- 1 | package client 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "net/http" 7 | ) 8 | 9 | func Get(req *http.Request, data interface{}) error { 10 | resp, err := Do(req) 11 | if err != nil { 12 | return err 13 | } 14 | defer resp.Body.Close() 15 | body, err := io.ReadAll(resp.Body) 16 | if err != nil { 17 | return err 18 | } 19 | if resp.StatusCode != 200 { 20 | return &ErrNotOk{resp.StatusCode, string(body)} 21 | } 22 | if err := json.Unmarshal(body, data); err != nil { 23 | return err 24 | } 25 | return nil 26 | } 27 | 28 | func Post(req *http.Request) ([]byte, error) { 29 | resp, err := Do(req) 30 | if err != nil { 31 | return nil, err 32 | } 33 | defer resp.Body.Close() 34 | resBody, err := io.ReadAll(resp.Body) 35 | if err != nil { 36 | return nil, err 37 | } 38 | if resp.StatusCode != 200 { 39 | return nil, &ErrNotOk{resp.StatusCode, string(resBody)} 40 | } 41 | return resBody, err 42 | } 43 | 44 | func Do(req *http.Request) (*http.Response, error) { 45 | return DefaultClient.Do(req) 46 | } 47 | -------------------------------------------------------------------------------- /examples/ddl/go.mod: -------------------------------------------------------------------------------- 1 | module ddl 2 | 3 | go 1.19 4 | 5 | require github.com/hduLib/hdu v0.0.0-20221101121300-356e4cdc954c 6 | 7 | require ( 8 | github.com/PuerkitoBio/goquery v1.8.0 // indirect 9 | github.com/andybalholm/cascadia v1.3.1 // indirect 10 | golang.org/x/net v0.1.0 // indirect 11 | ) 12 | 13 | replace github.com/hduLib/hdu v0.0.0-20221101121300-356e4cdc954c => ../../ 14 | -------------------------------------------------------------------------------- /examples/ddl/go.sum: -------------------------------------------------------------------------------- 1 | github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U= 2 | github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI= 3 | github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= 4 | github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= 5 | golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 6 | golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0= 7 | golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= 8 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 9 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 10 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 11 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 12 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 13 | -------------------------------------------------------------------------------- /examples/ddl/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/hduLib/hdu/chaoxing" 6 | "github.com/hduLib/hdu/chaoxing/course" 7 | "log" 8 | "os" 9 | ) 10 | 11 | var phone = os.Getenv("id") 12 | var passwd = os.Getenv("casPasswd") 13 | 14 | func main() { 15 | user, err := chaoxing.LoginWithCas(phone, passwd) 16 | if err != nil { 17 | log.Fatalln(err) 18 | return 19 | } 20 | works, err := user.CourseList() 21 | if err != nil { 22 | log.Fatalln(err) 23 | return 24 | } 25 | works.Each(func(course *course.Brief) bool { 26 | c, err := course.Detail() 27 | if err != nil { 28 | log.Fatalln(err) 29 | } 30 | workList, err := c.WorkList() 31 | if err != nil { 32 | log.Fatalln(err) 33 | } 34 | for _, v := range workList.Works { 35 | if v.Status == "未交" && v.Time.Unix() != 0 { 36 | fmt.Printf("[%s作业]%s---%s\n", course.Title, v.Title, v.Time) 37 | } 38 | } 39 | examList, err := c.ExamList() 40 | if err != nil { 41 | log.Fatalln(err) 42 | } 43 | for _, v := range examList.Exams { 44 | if v.Status == "待做" { 45 | fmt.Printf("[%s考试]%s---%s\n", course.Title, v.Title, v.Time) 46 | } 47 | } 48 | return true 49 | }) 50 | } 51 | -------------------------------------------------------------------------------- /examples/healthcheckin/go.mod: -------------------------------------------------------------------------------- 1 | module checkin 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/BaiMeow/SimpleBot v0.0.0-20211222093213-f63e2ce6423f 7 | github.com/hduLib/hdu v0.0.0-20221101121300-356e4cdc954c 8 | ) 9 | 10 | require ( 11 | github.com/google/uuid v1.3.0 // indirect 12 | github.com/gorilla/websocket v1.4.2 // indirect 13 | github.com/tidwall/gjson v1.14.2 // indirect 14 | github.com/tidwall/match v1.1.1 // indirect 15 | github.com/tidwall/pretty v1.2.0 // indirect 16 | ) 17 | 18 | replace github.com/hduLib/hdu v0.0.0-20221101121300-356e4cdc954c => ../../ 19 | -------------------------------------------------------------------------------- /examples/healthcheckin/go.sum: -------------------------------------------------------------------------------- 1 | github.com/BaiMeow/SimpleBot v0.0.0-20211222093213-f63e2ce6423f h1:eqdCIxO753Ca1teiOShXw88F7VdLXyiKjcw4VjBliaw= 2 | github.com/BaiMeow/SimpleBot v0.0.0-20211222093213-f63e2ce6423f/go.mod h1:BtnE+YJUca3TyMHt7xfzFhfKZWNZsdcvkKyI3txwI2U= 3 | github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= 4 | github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 5 | github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0Ufc= 6 | github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 7 | github.com/hduLib/hdu v0.0.0-20221101121300-356e4cdc954c h1:JF7vXMhtzTUzNiGgn8Wr1pAUIJc8JpsBEnLDcKQswyg= 8 | github.com/hduLib/hdu v0.0.0-20221101121300-356e4cdc954c/go.mod h1:nirTK7OzMgpx3Uu79bdktKMN0sYmnxj7BJxktLTzQNc= 9 | github.com/tidwall/gjson v1.14.2 h1:6BBkirS0rAHjumnjHF6qgy5d2YAJ1TLIaFE2lzfOLqo= 10 | github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 11 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 12 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 13 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= 14 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 15 | -------------------------------------------------------------------------------- /examples/healthcheckin/main.go: -------------------------------------------------------------------------------- 1 | // 健康打卡自动打卡 2 | package main 3 | 4 | /* 5 | qq机器人+自动健康打卡 6 | 已经适配新打卡系统 7 | */ 8 | 9 | import ( 10 | "fmt" 11 | "github.com/BaiMeow/SimpleBot/bot" 12 | "github.com/BaiMeow/SimpleBot/driver" 13 | "github.com/BaiMeow/SimpleBot/message" 14 | "github.com/hduLib/hdu/skl" 15 | "log" 16 | "regexp" 17 | "time" 18 | ) 19 | 20 | type profile struct { 21 | username, password string 22 | UserID int64 23 | } 24 | 25 | var b *bot.Bot 26 | 27 | var students = make(map[int64]*profile) 28 | 29 | var regexpLogin = regexp.MustCompile("/checkin login (\\d{8,9}) (.*)") 30 | 31 | func main() { 32 | b = bot.New(driver.NewWsDriver("ws://localhost:6700", "")) 33 | b.Attach(&bot.PrivateMsgHandler{ 34 | Priority: 1, F: func(MsgID int32, UserID int64, Msg message.Msg) bool { 35 | if Msg[0].GetType() != "text" { 36 | return false 37 | } 38 | msg := Msg[0].(message.Text).Text 39 | //login 40 | matches := regexpLogin.FindStringSubmatch(msg) 41 | if len(matches) == 3 { 42 | c := profile{username: matches[1], password: matches[2], UserID: UserID} 43 | if _, err := skl.Login(c.username, c.password); err != nil { 44 | sendMsg(UserID, "登录失败") 45 | } else { 46 | sendMsg(UserID, "登录成功") 47 | students[UserID] = &c 48 | } 49 | return true 50 | } 51 | //人工打卡 52 | if msg == "/checkin checkin" { 53 | if students[UserID] != nil { 54 | students[UserID].checkin() 55 | } else { 56 | sendMsg(UserID, "未登录") 57 | } 58 | return true 59 | } 60 | return false 61 | }, 62 | }) 63 | err := b.Run() 64 | if err != nil { 65 | log.Fatalln(err) 66 | return 67 | } 68 | log.Println("开始自动打卡") 69 | 70 | for { 71 | now := time.Now() 72 | t := time.NewTimer(time.Until(time.Date(now.Year(), now.Month(), now.Day()+1, 7, 0, 0, 0, now.Location()))) 73 | <-t.C 74 | for _, c := range students { 75 | c.checkin() 76 | } 77 | } 78 | } 79 | 80 | func (p *profile) checkin() { 81 | user, err := skl.Login(p.username, p.password) 82 | if err != nil { 83 | sendMsg(p.UserID, err.Error()) 84 | return 85 | } 86 | err = user.Push(&skl.PushReqHDU) 87 | if err != nil { 88 | sendMsg(p.UserID, err.Error()) 89 | return 90 | } 91 | sendMsg(p.UserID, fmt.Sprintf("打卡完成:%s", time.Now().Format("Jan 2 15:04:05"))) 92 | } 93 | 94 | func sendMsg(qq int64, txt string) { 95 | if _, err := b.SendPrivateMsg(qq, message.New().Text(txt)); err != nil { 96 | fmt.Printf("发送消息时出错(qq:%d,msg:%s):%v", qq, txt, err) 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /examples/sign/go.mod: -------------------------------------------------------------------------------- 1 | module sign 2 | 3 | go 1.19 4 | 5 | require github.com/hduLib/hdu v0.0.0-20221101121300-356e4cdc954c 6 | 7 | require ( 8 | github.com/tidwall/gjson v1.14.2 // indirect 9 | github.com/tidwall/match v1.1.1 // indirect 10 | github.com/tidwall/pretty v1.2.0 // indirect 11 | ) 12 | 13 | replace github.com/hduLib/hdu v0.0.0-20221101121300-356e4cdc954c => ../../ 14 | -------------------------------------------------------------------------------- /examples/sign/go.sum: -------------------------------------------------------------------------------- 1 | github.com/tidwall/gjson v1.14.2 h1:6BBkirS0rAHjumnjHF6qgy5d2YAJ1TLIaFE2lzfOLqo= 2 | github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 3 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 4 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 5 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= 6 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 7 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/hduLib/hdu 2 | 3 | go 1.19 4 | 5 | require ( 6 | github.com/PuerkitoBio/goquery v1.8.0 7 | github.com/joho/godotenv v1.4.0 8 | github.com/tidwall/gjson v1.14.2 9 | ) 10 | 11 | require ( 12 | github.com/andybalholm/cascadia v1.3.1 // indirect 13 | github.com/tidwall/match v1.1.1 // indirect 14 | github.com/tidwall/pretty v1.2.0 // indirect 15 | golang.org/x/net v0.1.0 // indirect 16 | ) 17 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U= 2 | github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI= 3 | github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= 4 | github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= 5 | github.com/joho/godotenv v1.4.0 h1:3l4+N6zfMWnkbPEXKng2o2/MR5mSwTrBih4ZEkkz1lg= 6 | github.com/joho/godotenv v1.4.0/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= 7 | github.com/tidwall/gjson v1.14.2 h1:6BBkirS0rAHjumnjHF6qgy5d2YAJ1TLIaFE2lzfOLqo= 8 | github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= 9 | github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= 10 | github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM= 11 | github.com/tidwall/pretty v1.2.0 h1:RWIZEg2iJ8/g6fDDYzMpobmaoGh5OLl4AXtGUGPcqCs= 12 | github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU= 13 | golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 14 | golang.org/x/net v0.1.0 h1:hZ/3BUoy5aId7sCpA/Tc5lt8DkFgdVS2onTpJsZ/fl0= 15 | golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= 16 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 17 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 18 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 19 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 20 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 21 | -------------------------------------------------------------------------------- /hduhelp/data.go: -------------------------------------------------------------------------------- 1 | package hduhelp 2 | 3 | const timeURL = "https://api.hduhelp.com/time" 4 | 5 | type TimeResp struct { 6 | Error int `json:"error"` 7 | Msg string `json:"msg"` 8 | Data struct { 9 | SchoolYear string `json:"schoolYear"` 10 | Semester string `json:"semester"` 11 | SemesterStartTimestamp int `json:"semester_start_timestamp"` 12 | WeekNow int `json:"weekNow"` 13 | WeekDayNow int `json:"weekDayNow"` 14 | TimeStamp int `json:"timeStamp"` 15 | Section int `json:"section"` 16 | } `json:"data"` 17 | Cache bool `json:"cache"` 18 | } 19 | -------------------------------------------------------------------------------- /hduhelp/time.go: -------------------------------------------------------------------------------- 1 | package hduhelp 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/hduLib/hdu/client" 7 | ) 8 | 9 | func Time() (*TimeResp, error) { 10 | req, err := http.NewRequest("GET", timeURL, nil) 11 | if err != nil { 12 | return nil, err 13 | } 14 | time := new(TimeResp) 15 | err = client.Get(req, time) 16 | if err != nil { 17 | return nil, err 18 | } 19 | return time, nil 20 | } 21 | -------------------------------------------------------------------------------- /hduhelp/time_test.go: -------------------------------------------------------------------------------- 1 | package hduhelp 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestTime(t *testing.T) { 9 | time, err := Time() 10 | if err != nil { 11 | t.Log(err) 12 | return 13 | } 14 | fmt.Println(time) 15 | } 16 | -------------------------------------------------------------------------------- /internal/ocr/ocr.go: -------------------------------------------------------------------------------- 1 | package ocr 2 | 3 | import ( 4 | "encoding/base64" 5 | "errors" 6 | "io" 7 | "log" 8 | 9 | "github.com/hduLib/hdu/internal/utils/convert" 10 | ) 11 | 12 | var DefaultType = Common 13 | 14 | func Parse(captcha interface{}) (string, error) { 15 | return recognizeWithType(DefaultType, captcha) 16 | } 17 | 18 | // recognizeWithType takes an ocr type and an image reader, returns the string 19 | // typed ocr result along with a possible error. 20 | func recognizeWithType(ocrType YunmaOCRType, captcha interface{}) (string, error) { 21 | var bs64captcha string 22 | 23 | // base64 encoded 24 | switch captcha := captcha.(type) { 25 | case []byte: 26 | bs64captcha = base64.StdEncoding.EncodeToString(captcha) 27 | case string: 28 | bs64captcha = base64.StdEncoding.EncodeToString(convert.ToBytes(captcha)) 29 | case io.Reader: 30 | data, err := io.ReadAll(captcha) 31 | if err != nil { 32 | log.Fatal(err) 33 | } 34 | bs64captcha = base64.StdEncoding.EncodeToString(data) 35 | default: 36 | return "", ErrUnsupportCaptchaType 37 | } 38 | 39 | // do ocr 40 | switch ocrType { 41 | case Common: 42 | return commonVerify(bs64captcha) 43 | case Slide: 44 | return slideVerify(bs64captcha) 45 | case SinSlide: 46 | return sinSlideVerify(bs64captcha) 47 | case TrafficSlide: 48 | return trafficSlideVerify(bs64captcha) 49 | case Click: 50 | return clickVerify(bs64captcha) 51 | case Rotate: 52 | return rotateVerify(bs64captcha) 53 | case Google: 54 | return googleVerify(bs64captcha) 55 | case Hcaptcha: 56 | return hcaptchaVerify(bs64captcha) 57 | case FunCaptcha: 58 | return funCaptchaVerify(bs64captcha) 59 | } 60 | return "", ErrUnsupportOCRType 61 | } 62 | 63 | var ( 64 | ErrUnsupportOCRType = errors.New("ocr type is unsupported") 65 | ErrUnsupportCaptchaType = errors.New("ocr parse arg type is unsupported") 66 | ) 67 | 68 | type YunmaOCRType int 69 | 70 | //go:generate stringer -type=YunmaOCRType 71 | const ( 72 | Common YunmaOCRType = iota + 1 73 | Slide 74 | SinSlide 75 | TrafficSlide 76 | Click 77 | Rotate 78 | Google 79 | Hcaptcha 80 | FunCaptcha 81 | ) 82 | -------------------------------------------------------------------------------- /internal/ocr/ocr_test.go: -------------------------------------------------------------------------------- 1 | package ocr_test 2 | 3 | import ( 4 | "io" 5 | "os" 6 | "testing" 7 | 8 | ocr2 "github.com/hduLib/hdu/internal/ocr" 9 | _ "github.com/joho/godotenv/autoload" 10 | ) 11 | 12 | func TestOCR(t *testing.T) { 13 | ocr2.SetToken(os.Getenv("TOKEN")) // you should set your yunma token first 14 | res, err := ocr2.Parse(readInImage()) 15 | if err != nil { 16 | t.Fatal(err) 17 | } 18 | t.Log("ocr result:", res) 19 | if res != "vyza" { 20 | t.Fatalf(`ocr error, expect "vyza", found %s`, res) 21 | } 22 | } 23 | 24 | func readInImage() io.Reader { 25 | f, _ := os.OpenFile("testdata/4.jfif", os.O_RDONLY, 0644) 26 | return f 27 | } 28 | -------------------------------------------------------------------------------- /internal/ocr/readme.md: -------------------------------------------------------------------------------- 1 | # OCR 云码 2 | 3 | OCR 采用云码提供的 api 4 | 5 | 开发文档: 6 | 7 | ## OCR 类型 8 | 9 | 云码对不同的 OCR 类型提供了相应的服务 10 | 11 | 服务类型需在 `type` 字段指定 12 | 13 | 1. 数英汉字类型 14 | 15 | 通用数英1-4位 10110 16 | 17 | 通用数英5-8位 10111 18 | 19 | 通用数英9~11位 10112 20 | 21 | 通用数英12位及以上 10113 22 | 23 | 通用数英1~6位plus 10103 24 | 25 | 定制-数英5位~qcs 9001 26 | 27 | 定制-纯数字4位 193 28 | 29 | 2. 中文类型 30 | 31 | 通用中文字符1~2位 10114 32 | 33 | 通用中文字符 3~5位 10115 34 | 35 | 通用中文字符6~8位 10116 36 | 37 | 通用中文字符9位及以上 10117 38 | 39 | 定制-XX西游苦行中文字符 10107 40 | 41 | 3. 计算类型 42 | 43 | 通用数字计算题 50100 44 | 45 | 通用中文计算题 50101 46 | 47 | 定制-计算题 cni 452 48 | 49 | ## 其他 50 | 51 | 详细文档: 52 | -------------------------------------------------------------------------------- /internal/ocr/testdata/1.jfif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hduLib/hdu/2736b93b8df7cf2556e43d0c76da034257ff33ea/internal/ocr/testdata/1.jfif -------------------------------------------------------------------------------- /internal/ocr/testdata/2.jfif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hduLib/hdu/2736b93b8df7cf2556e43d0c76da034257ff33ea/internal/ocr/testdata/2.jfif -------------------------------------------------------------------------------- /internal/ocr/testdata/3.jfif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hduLib/hdu/2736b93b8df7cf2556e43d0c76da034257ff33ea/internal/ocr/testdata/3.jfif -------------------------------------------------------------------------------- /internal/ocr/testdata/4.jfif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hduLib/hdu/2736b93b8df7cf2556e43d0c76da034257ff33ea/internal/ocr/testdata/4.jfif -------------------------------------------------------------------------------- /internal/ocr/testdata/5.jfif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hduLib/hdu/2736b93b8df7cf2556e43d0c76da034257ff33ea/internal/ocr/testdata/5.jfif -------------------------------------------------------------------------------- /internal/ocr/testdata/6.jfif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hduLib/hdu/2736b93b8df7cf2556e43d0c76da034257ff33ea/internal/ocr/testdata/6.jfif -------------------------------------------------------------------------------- /internal/ocr/testdata/7.jfif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hduLib/hdu/2736b93b8df7cf2556e43d0c76da034257ff33ea/internal/ocr/testdata/7.jfif -------------------------------------------------------------------------------- /internal/ocr/yunma_ocr.go: -------------------------------------------------------------------------------- 1 | package ocr 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "io" 8 | "net/http" 9 | "sync" 10 | ) 11 | 12 | const customUrl = "https://www.jfbym.com/api/YmServer/customApi" 13 | 14 | var token string 15 | 16 | // SetToken sets the token for later usage. 17 | func SetToken(t string) { 18 | token = t 19 | } 20 | 21 | type ( 22 | commonConfig = map[string]interface{} 23 | ) 24 | 25 | var ( 26 | commonConfigPool = &sync.Pool{ 27 | New: func() any { 28 | return make(map[string]interface{}, 3) 29 | }, 30 | } 31 | 32 | // better use obj-pool to release the pressure of GOGC 33 | ) 34 | 35 | // Generated by https://quicktype.io 36 | 37 | type CommonTypeResponse struct { 38 | Msg string `json:"msg"` 39 | Code int64 `json:"code"` 40 | Data Data `json:"data"` 41 | } 42 | 43 | type Data struct { 44 | Code int64 `json:"code"` 45 | Data string `json:"data"` 46 | Time float64 `json:"time"` 47 | UniqueCode string `json:"unique_code"` 48 | } 49 | 50 | var ( 51 | commonTypeRespPool = &sync.Pool{ 52 | New: func() any { 53 | return new(CommonTypeResponse) 54 | }, 55 | } 56 | ) 57 | 58 | // commonVerify needs a base64 encoded image to send a request 59 | // to yunma ocr to get the result. To find more about the common 60 | // type of verification, see: . 61 | func commonVerify(image string) (string, error) { 62 | if token == "" { 63 | return "", errors.New("token unset") 64 | } 65 | 66 | // construct common type config 67 | cfg := commonConfigPool.Get().(map[string]interface{}) 68 | defer commonConfigPool.Put(cfg) 69 | cfg["image"] = image 70 | cfg["type"] = "10110" 71 | cfg["token"] = token 72 | 73 | // send request to yunma ocr 74 | rawcfg, _ := json.Marshal(cfg) 75 | body := bytes.NewReader(rawcfg) 76 | resp, err := http.Post(customUrl, "application/json;charset=utf-8", body) 77 | if err != nil { 78 | return "", err 79 | } 80 | defer resp.Body.Close() 81 | 82 | // recv from yunma ocr 83 | data, err := io.ReadAll(resp.Body) 84 | if err != nil { 85 | return "", err 86 | } 87 | commonTypeResp := commonTypeRespPool.Get().(*CommonTypeResponse) 88 | defer commonTypeRespPool.Put(commonTypeResp) 89 | err = json.Unmarshal(data, commonTypeResp) 90 | if err != nil { 91 | return "", err 92 | } 93 | return commonTypeResp.Data.Data, nil 94 | } 95 | 96 | 97 | func slideVerify(image string) (string, error) { 98 | return "", ErrUnsupportOCRType 99 | } 100 | 101 | func sinSlideVerify(image string) (string, error) { 102 | return "", ErrUnsupportOCRType 103 | } 104 | 105 | func trafficSlideVerify(image string) (string, error) { 106 | return "", ErrUnsupportOCRType 107 | } 108 | 109 | func clickVerify(image string) (string, error) { 110 | return "", ErrUnsupportOCRType 111 | } 112 | 113 | func rotateVerify(image string) (string, error) { 114 | return "", ErrUnsupportOCRType 115 | } 116 | 117 | func googleVerify(image string) (string, error) { 118 | return "", ErrUnsupportOCRType 119 | } 120 | 121 | func hcaptchaVerify(image string) (string, error) { 122 | return "", ErrUnsupportOCRType 123 | } 124 | 125 | func funCaptchaVerify(image string) (string, error) { 126 | return "", ErrUnsupportOCRType 127 | } 128 | -------------------------------------------------------------------------------- /internal/ocr/yunmaocrtype_string.go: -------------------------------------------------------------------------------- 1 | // Code generated by "stringer -type=YunmaOCRType"; DO NOT EDIT. 2 | 3 | package ocr 4 | 5 | import "strconv" 6 | 7 | func _() { 8 | // An "invalid array index" compiler error signifies that the constant values have changed. 9 | // Re-run the stringer command to generate them again. 10 | var x [1]struct{} 11 | _ = x[Common-1] 12 | _ = x[Slide-2] 13 | _ = x[SinSlide-3] 14 | _ = x[TrafficSlide-4] 15 | _ = x[Click-5] 16 | _ = x[Rotate-6] 17 | _ = x[Google-7] 18 | _ = x[Hcaptcha-8] 19 | _ = x[FunCaptcha-9] 20 | } 21 | 22 | const _YunmaOCRType_name = "CommonSlideSinSlideTrafficSlideClickRotateGoogleHcaptchaFunCaptcha" 23 | 24 | var _YunmaOCRType_index = [...]uint8{0, 6, 11, 19, 31, 36, 42, 48, 56, 66} 25 | 26 | func (i YunmaOCRType) String() string { 27 | i -= 1 28 | if i < 0 || i >= YunmaOCRType(len(_YunmaOCRType_index)-1) { 29 | return "YunmaOCRType(" + strconv.FormatInt(int64(i+1), 10) + ")" 30 | } 31 | return _YunmaOCRType_name[_YunmaOCRType_index[i]:_YunmaOCRType_index[i+1]] 32 | } 33 | -------------------------------------------------------------------------------- /internal/utils/convert/converT_test.go: -------------------------------------------------------------------------------- 1 | package convert_test 2 | 3 | import ( 4 | "encoding/base64" 5 | "fmt" 6 | "testing" 7 | 8 | "github.com/hduLib/hdu/internal/utils/convert" 9 | ) 10 | 11 | func TestToString(t *testing.T) { 12 | b := []byte{65} 13 | fmt.Println("b:", b) 14 | 15 | s := convert.ToString(b) 16 | fmt.Println("after convert:", s) 17 | } 18 | 19 | func TestToBytes(t *testing.T) { 20 | s := "Hello, world" 21 | fmt.Println("s:", s) 22 | 23 | b := convert.ToBytes(s) 24 | fmt.Println("after convert", b) 25 | } 26 | 27 | func TestBase64Encoding(t *testing.T) { 28 | username := "limuluteenpzsite" 29 | bUname := convert.ToBytes(username) 30 | obRes := base64.StdEncoding.EncodeToString([]byte(username)) 31 | cbRes := base64.StdEncoding.EncodeToString(bUname) 32 | if cbRes != obRes { 33 | t.FailNow() 34 | } 35 | fmt.Println("It works!") 36 | fmt.Println("obRes:", obRes) 37 | fmt.Println("cbRes:", cbRes) 38 | } 39 | -------------------------------------------------------------------------------- /internal/utils/convert/tostring.go: -------------------------------------------------------------------------------- 1 | package convert 2 | 3 | import ( 4 | "reflect" 5 | "unsafe" 6 | ) 7 | 8 | // ToString is used to zero copy converT `string` 9 | func ToString(b []byte) string { 10 | return *(*string)(unsafe.Pointer(&b)) 11 | } 12 | 13 | // ToBytes is used to zero copy converT `[]byte`. 14 | // Attention! Since `string` is immutable in Go, theconversion 15 | // from `string` to `[]byte` can easily casuse panic as change 16 | // the converted `[]byte` is actually change the immutable 17 | // `string` stored in .RODATA(maybe) section. 18 | func ToBytes(s string) []byte { 19 | bh := (*reflect.SliceHeader)(unsafe.Pointer(&s)) 20 | bh.Cap = len(s) 21 | return *(*[]byte)(unsafe.Pointer(bh)) 22 | } 23 | -------------------------------------------------------------------------------- /phy/captcha_ocr.go: -------------------------------------------------------------------------------- 1 | package phy 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net/http" 7 | 8 | "github.com/hduLib/hdu/client" 9 | "github.com/hduLib/hdu/internal/ocr" 10 | ) 11 | 12 | func getCaptchaContent(u *User) (string, error) { 13 | req, err := http.NewRequest(http.MethodGet, "http://phy.hdu.edu.cn/captcha.svl", nil) 14 | if err != nil { 15 | return "", err 16 | } 17 | 18 | { 19 | req.Header.Set("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9") 20 | req.Header.Set("Accept-Language", "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7") 21 | req.Header.Set("Cache-Control", "max-age=0") 22 | req.Header.Set("Connection", "keep-alive") 23 | req.Header.Set("Upgrade-Insecure-Requests", "1") 24 | req.Header.Set("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36") 25 | } 26 | 27 | resp, err := client.Do(req) 28 | if err != nil { 29 | return "", err 30 | } 31 | 32 | // get JSessionId when get captcha 33 | u.setJSessionId(resp.Cookies()) 34 | 35 | // read in image 36 | b, err := io.ReadAll(resp.Body) 37 | if err != nil { 38 | return "", err 39 | } 40 | 41 | // ocr 42 | rd := bytes.NewReader(b) 43 | captchaContent, err := ocr.Parse(rd) 44 | if err != nil { 45 | return "", err 46 | } 47 | 48 | return captchaContent, nil 49 | } 50 | 51 | func (u *User) setJSessionId(cookies []*http.Cookie) { 52 | for _, cookie := range cookies { 53 | if cookie.Name != "JSESSIONID" { 54 | continue 55 | } 56 | u.SessionId = cookie.Value 57 | return 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /phy/experiment_sche.go: -------------------------------------------------------------------------------- 1 | package phy 2 | 3 | import ( 4 | "encoding/json" 5 | "io" 6 | "net/http" 7 | "net/url" 8 | "strings" 9 | 10 | "github.com/hduLib/hdu/client" 11 | ) 12 | 13 | type ExperSche struct { 14 | QueryWeekDayFlag int64 `json:"queryWeekDayFlag"` 15 | Experiments []Experiment `json:"experiments"` 16 | } 17 | 18 | type Experiment struct { 19 | Selected bool `json:"selected"` 20 | Value int64 `json:"value"` 21 | Text string `json:"text"` 22 | } 23 | 24 | // GetExperimentSche 返回所有实验安排 25 | func (u *User) GetExperimentSche() (*ExperSche, error) { 26 | if u.SessionId == "" { 27 | return nil, ErrNoJSessionId 28 | } 29 | return u.getExperSches() 30 | } 31 | 32 | var ( 33 | CourseId = "325" 34 | SemesterNo = "202220231" 35 | QueryExperimentId = "-1" 36 | ) 37 | 38 | func (u *User) getExperSches() (*ExperSche, error) { 39 | payload := buildExprSchePayload(CourseId, SemesterNo, QueryExperimentId) 40 | 41 | req, err := http.NewRequest(http.MethodPost, "http://phy.hdu.edu.cn/phymember/v_mycourse_changed.jspx", strings.NewReader(payload)) 42 | if err != nil { 43 | return nil, err 44 | } 45 | 46 | { 47 | req.Header.Set("authority", "phy.hdu.edu.cn") 48 | req.Header.Set("accept", "application/json, text/javascript, */*") 49 | req.Header.Set("accept-language", "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7") 50 | req.Header.Set("content-type", "application/x-www-form-urlencoded") 51 | req.Header.Set("cookie", "clientlanguage=zh_CN; JSESSIONID="+u.SessionId) 52 | req.Header.Set("origin", "https://phy.hdu.edu.cn") 53 | req.Header.Set("referer", "https://phy.hdu.edu.cn/phymember/expt_schedule_student.jspx") 54 | req.Header.Set("sec-ch-ua", `"Google Chrome";v="107", "Chromium";v="107", "Not=A?Brand";v="24"`) 55 | req.Header.Set("sec-ch-ua-mobile", "?0") 56 | req.Header.Set("sec-ch-ua-platform", `"Windows"`) 57 | req.Header.Set("sec-fetch-dest", "empty") 58 | req.Header.Set("sec-fetch-mode", "cors") 59 | req.Header.Set("sec-fetch-site", "same-origin") 60 | req.Header.Set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36") 61 | req.Header.Set("x-requested-with", "XMLHttpRequest") 62 | } 63 | 64 | resp, err := client.Do(req) 65 | if err != nil { 66 | return nil, err 67 | } 68 | defer resp.Body.Close() 69 | 70 | b, err := io.ReadAll(resp.Body) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | res := new(ExperSche) 76 | err = json.Unmarshal(b, res) 77 | if err != nil { 78 | return nil, err 79 | } 80 | 81 | return res, nil 82 | } 83 | 84 | func buildExprSchePayload(courseId, semesterNo, queryExperimentId string) string { 85 | payload := make(url.Values, 3) 86 | payload.Add("queryCourseId", courseId) 87 | payload.Add("semesterNo", semesterNo) 88 | payload.Add("queryExperimentId", queryExperimentId) 89 | return payload.Encode() 90 | } 91 | -------------------------------------------------------------------------------- /phy/experiment_sche_test.go: -------------------------------------------------------------------------------- 1 | package phy 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | ) 7 | 8 | func TestExperSche(t *testing.T) { 9 | u, _ := Login(studentId, password) 10 | experiments, err := u.GetExperimentSche() 11 | if err != nil { 12 | t.Fatal(err) 13 | } 14 | fmt.Printf("%+v", experiments) 15 | } 16 | -------------------------------------------------------------------------------- /phy/login.go: -------------------------------------------------------------------------------- 1 | package phy 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "io" 7 | "net/http" 8 | "net/url" 9 | "strings" 10 | 11 | "github.com/hduLib/hdu/client" 12 | "github.com/hduLib/hdu/internal/utils/convert" 13 | ) 14 | 15 | type User struct { 16 | SessionId string 17 | } 18 | 19 | var ( 20 | ErrBeforeLogin = errors.New("before login") 21 | ErrNoJSessionId = errors.New("JSessionId is needed") 22 | ) 23 | 24 | // Login 用于登录省物理实验平台,入参为学号和物理实验平台的密码 25 | func Login(studentId, password string) (*User, error) { 26 | user := new(User) 27 | payload, err := user.buildLoginPayload(studentId, password) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | // send request 33 | err = user.requestWithPayload(payload) 34 | if err != nil { 35 | return nil, err 36 | } 37 | 38 | return user, err 39 | } 40 | 41 | func (u *User) requestWithPayload(payload string) error { 42 | req, err := http.NewRequest(http.MethodPost, "https://phy.hdu.edu.cn/login.jspx", strings.NewReader(payload)) 43 | if err != nil { 44 | return err 45 | } 46 | 47 | { 48 | req.Header.Set("authority", "phy.hdu.edu.cn") 49 | req.Header.Set("accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9") 50 | req.Header.Set("accept-language", "en-US,en;q=0.9,zh-CN;q=0.8,zh;q=0.7") 51 | req.Header.Set("cache-control", "max-age=0") 52 | req.Header.Set("content-type", "application/x-www-form-urlencoded") 53 | req.Header.Set("cookie", "clientlanguage=zh_CN; JSESSIONID="+u.SessionId) 54 | req.Header.Set("origin", "https://phy.hdu.edu.cn") 55 | req.Header.Set("referer", "https://phy.hdu.edu.cn/login.jspx?returnUrl=/") 56 | req.Header.Set("sec-ch-ua", `"Google Chrome";v="107", "Chromium";v="107", "Not=A?Brand";v="24"`) 57 | req.Header.Set("sec-ch-ua-mobile", "?0") 58 | req.Header.Set("sec-ch-ua-platform", `"Windows"`) 59 | req.Header.Set("sec-fetch-dest", "document") 60 | req.Header.Set("sec-fetch-mode", "navigate") 61 | req.Header.Set("sec-fetch-site", "same-origin") 62 | req.Header.Set("sec-fetch-user", "?1") 63 | req.Header.Set("upgrade-insecure-requests", "1") 64 | req.Header.Set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36") 65 | } 66 | 67 | // send login request 68 | resp, err := client.Do(req) 69 | if err != nil { 70 | return err 71 | } 72 | defer resp.Body.Close() 73 | 74 | // check login status 75 | bodyText, err := io.ReadAll(resp.Body) 76 | if err != nil { 77 | return err 78 | } 79 | err = checkLoginStatus(bodyText) 80 | if err != nil { 81 | return err 82 | } 83 | 84 | // refresh JSessionId 85 | u.setJSessionId(resp.Cookies()) 86 | 87 | return nil 88 | } 89 | 90 | // payload: `returnUrl=%2F&username=&password=&captcha=&x=0&y=0` 91 | func (u *User) buildLoginPayload(stuId, passwd string) (string, error) { 92 | payload := make(url.Values, 6) 93 | payload.Add("returnUrl", "/") 94 | payload.Add("username", stuId) 95 | payload.Add("password", passwd) 96 | captchaContent, err := getCaptchaContent(u) 97 | if err != nil { 98 | return "", err 99 | } 100 | payload.Add("captcha", captchaContent) 101 | payload.Add("x", "0") 102 | payload.Add("y", "0") 103 | return payload.Encode(), nil 104 | } 105 | 106 | func checkLoginStatus(respBody []byte) error { 107 | if bytes.Contains(respBody, convert.ToBytes("您还没有登录")) { 108 | return ErrBeforeLogin 109 | } 110 | return nil 111 | } 112 | -------------------------------------------------------------------------------- /phy/login_test.go: -------------------------------------------------------------------------------- 1 | package phy 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | 7 | "github.com/hduLib/hdu/internal/ocr" 8 | 9 | _ "github.com/joho/godotenv/autoload" 10 | ) 11 | 12 | var studentId, password string 13 | 14 | func TestMain(m *testing.M) { 15 | ocr.SetToken(os.Getenv("TOKEN")) 16 | studentId = os.Getenv("STUDENTID") 17 | password = os.Getenv("PASSWORD") 18 | m.Run() 19 | } 20 | 21 | func TestLogin(t *testing.T) { 22 | _, err := Login(studentId, password) 23 | if err != nil { 24 | t.Fatal(err) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /phy/readme.md: -------------------------------------------------------------------------------- 1 | # phy 物理省实验平台 2 | 3 | ## 本地测试 4 | 5 | 在 phy/ 下新建 `.env`,并填入以下内容: 6 | 7 | ```shell 8 | TOKEN=your_yunma_token # 云码的账号 token 9 | STUDENTID=your_username # 学号 10 | PASSWORD=your_password # 省实验平台的密码 11 | ``` 12 | 13 | *注: phy可能~~灵活~~关闭,需要留意由此导致的 404* 14 | -------------------------------------------------------------------------------- /skl/api.go: -------------------------------------------------------------------------------- 1 | package skl 2 | 3 | import ( 4 | "encoding/json" 5 | "github.com/hduLib/hdu/client" 6 | "time" 7 | ) 8 | 9 | func (user *User) Push(payload *PushReq) error { 10 | _, err := user.post(pushURL, payload) 11 | if err == nil { 12 | return nil 13 | } 14 | if err, ok := err.(*client.ErrNotOk); ok { 15 | if err.StatusCode == 400 { 16 | var msg errorMsg 17 | if err1 := json.Unmarshal([]byte(err.Body), &msg); err1 != nil { 18 | return err 19 | } 20 | if msg.Code == 0 && msg.Msg == "今日已经打卡" { 21 | return ErrAlreadyPushed 22 | } 23 | } 24 | } 25 | return err 26 | } 27 | 28 | func (user *User) PushLogs() (*PushLogResp, error) { 29 | resp := new(PushLogResp) 30 | return resp, user.get(pushLogURL, resp) 31 | } 32 | 33 | func (user *User) My() (*MyResp, error) { 34 | resp := new(MyResp) 35 | return resp, user.get(myURL, resp) 36 | } 37 | 38 | func (user *User) UserInfo() (*UserInfoResp, error) { 39 | resp := new(UserInfoResp) 40 | return resp, user.get(userInfoURL, resp) 41 | } 42 | 43 | func (user *User) Leave(payload *LeaveReq) error { 44 | _, err := user.post(leaveURL, payload) 45 | return err 46 | } 47 | 48 | // Course needs a startTime, which determined the semester 49 | // that the returned course list belongs to. So you may simply 50 | // use time.Now() to get the current course list. 51 | func (user *User) Course(startTime time.Time) (*CourseResp, error) { 52 | resp := new(CourseResp) 53 | return resp, user.get(courseURL+"?startTime="+startTime.Format("2006-01-02"), resp) 54 | } 55 | -------------------------------------------------------------------------------- /skl/data.go: -------------------------------------------------------------------------------- 1 | package skl 2 | 3 | const ( 4 | pushURL = "https://skl.hdu.edu.cn/api/punch" 5 | pushLogURL = "https://skl.hdu.edu.cn/api/punch/my" 6 | casLoginURL = "https://skl.hdu.edu.cn/api/userinfo?type=&index=passcard.html" 7 | myURL = "https://skl.hdu.edu.cn/api/passcard/my" 8 | userInfoURL = "https://skl.hdu.edu.cn/api/userinfo?type=" 9 | leaveURL = "https://skl.hdu.edu.cn/api/pass-leave/add" 10 | courseURL = "https://skl.hdu.edu.cn/api/course" 11 | ) 12 | 13 | var PushReqHDU = PushReq{ 14 | CurrentLocation: "浙江省杭州市钱塘区", 15 | City: "杭州市", 16 | DistrictAdcode: "330114", 17 | Province: "浙江省", 18 | District: "钱塘区", 19 | HealthCode: 0, 20 | HealthReport: 0, 21 | CurrentLiving: 0, 22 | Last14Days: 0, 23 | } 24 | 25 | type PushReq struct { 26 | // 定位地址精确到县/区一级,如"浙江省杭州市钱塘区" 27 | CurrentLocation string `json:"currentLocation"` 28 | // 定位地级市,如"杭州市" 29 | City string `json:"city"` 30 | // 中国行政区划代码,精确到县/区一级,如钱塘区为 330114 31 | DistrictAdcode string `json:"districtAdcode"` 32 | // 省份,如"浙江省" 33 | Province string `json:"province"` 34 | // 县/区一级,如"钱塘区" 35 | District string `json:"district"` 36 | // 健康码状态,0绿码,1红码,2橙码,3未领取 37 | HealthCode int `json:"healthCode"` 38 | // 健康状况 39 | // 0 健康 40 | // 1 发烧 41 | // 2 咳嗽腹泻 42 | // 3 确诊病例 43 | // 4 疑似病例 44 | HealthReport int `json:"healthReport"` 45 | // 生活状况 46 | // 0 正常 47 | // 1 发热送检 48 | // 2 集中隔离 49 | // 3 社区要求居家隔离 50 | // 4 学校要求居家隔离 51 | // 5 其他 52 | CurrentLiving int `json:"currentLiving"` 53 | // 14天内密接触情况 54 | // 0 无 55 | // 1 密接 56 | // 2 次密接 57 | Last14Days int `json:"last14days"` 58 | } 59 | 60 | type PushLogResp struct { 61 | PageNo int `json:"pageNo"` 62 | PageSize int `json:"pageSize"` 63 | Count int `json:"count"` 64 | Start int `json:"start"` 65 | OrderByList interface{} `json:"orderByList"` 66 | OrderAscList interface{} `json:"orderAscList"` 67 | List []struct { 68 | StudentName string `json:"studentName"` 69 | CardNo string `json:"cardNo"` 70 | StudentType int `json:"studentType"` 71 | Grade string `json:"grade"` 72 | Sex string `json:"sex"` 73 | ClassNo string `json:"classNo"` 74 | StudentStatus int `json:"studentStatus"` 75 | UnitName string `json:"unitName"` 76 | Id string `json:"id"` 77 | StaffId string `json:"staffId"` 78 | Province string `json:"province"` 79 | City string `json:"city"` 80 | District string `json:"district"` 81 | DistrictAdcode string `json:"districtAdcode"` 82 | HealthCode int `json:"healthCode"` 83 | EnterUniversity interface{} `json:"enterUniversity"` 84 | HealthReport int `json:"healthReport"` 85 | CurrentLiving int `json:"currentLiving"` 86 | Last14Days int `json:"last14days"` 87 | ShotsCompleted interface{} `json:"shotsCompleted"` 88 | NucleicAcid interface{} `json:"nucleicAcid"` 89 | HealthStatus int `json:"healthStatus"` 90 | LocationStatus int `json:"locationStatus"` 91 | ExamineStatus int `json:"examineStatus"` 92 | CreateDate int64 `json:"createDate"` 93 | CreateTime int64 `json:"createTime"` 94 | ModifyTime interface{} `json:"modifyTime"` 95 | UnitId interface{} `json:"unitId"` 96 | TeacherId string `json:"teacherId"` 97 | CreateDateStart interface{} `json:"createDateStart"` 98 | CreateDateEnd interface{} `json:"createDateEnd"` 99 | TeacherName string `json:"teacherName"` 100 | IsRisk interface{} `json:"isRisk"` 101 | Risk interface{} `json:"risk"` 102 | } `json:"list"` 103 | End int `json:"end"` 104 | } 105 | 106 | type MyResp struct { 107 | // 学号 108 | Id string `json:"id"` 109 | // 未知 110 | UnitId string `json:"unitId"` 111 | // 打卡状态 112 | HeathCheckStatus int `json:"heathCheckStatus"` 113 | // 健康码状态 114 | HeathCodeStatus int `json:"heathCodeStatus"` 115 | // 上次核酸的报告日期当天的0点的unix时间(ms) 116 | HeathCheckStartDate int64 `json:"heathCheckStartDate"` 117 | // 核酸状态,0为有有效的核酸报告,其他暂时未知 118 | HsjcStatus int `json:"hsjcStatus"` 119 | // 核酸检测有效期截止时间 120 | HsjcValidTime int64 `json:"hsjcValidTime"` 121 | // 最后一次核酸检测的报告时间 122 | HsjcLastTime int64 `json:"hsjcLastTime"` 123 | // 未知 124 | EntryStatus int `json:"entryStatus"` 125 | // 疑似为离校开始时间 126 | OutStartTime int64 `json:"outStartTime"` 127 | // 最后一次返校的时间 128 | InStartTime int64 `json:"inStartTime"` 129 | // 未知 130 | OutValidTime int64 `json:"outValidTime"` 131 | // 未知 132 | OutStatus int `json:"outStatus"` 133 | // 疑似为在寝室状态 134 | DormitoryStatus int `json:"dormitoryStatus"` 135 | // 疑似为最新寝室闸机刷脸时间 136 | DormitoryArrivalTime int64 `json:"dormitoryArrivalTime"` 137 | // 未知 138 | UpdateTime int64 `json:"updateTime"` 139 | // 未知 140 | Status int `json:"status"` 141 | // 未知 142 | Reason string `json:"reason"` 143 | } 144 | 145 | type UserInfoResp struct { 146 | // 姓名 147 | UserName string `json:"userName"` 148 | // 学生为1 149 | UserType int `json:"userType"` 150 | // 学院 151 | UnitId string `json:"unitId"` 152 | // 学院 153 | UnitCode string `json:"unitCode"` 154 | // 学院名称 155 | UnitName string `json:"unitName"` 156 | // 年级(入学年份) 157 | Grade string `json:"grade"` 158 | // 班号 159 | ClassNo string `json:"classNo"` 160 | // 性别 1为男 161 | Sex string `json:"sex"` 162 | // 专业 163 | Major string `json:"major"` 164 | // 手机号 165 | Phone string `json:"phone"` 166 | // 学号 167 | Id string `json:"id"` 168 | // 生日时间戳(ms) 169 | Birthday int64 `json:"birthday"` 170 | // 未知 171 | SchoolDay interface{} `json:"schoolDay"` 172 | Degree interface{} `json:"degree"` 173 | AcademicCredentials interface{} `json:"academicCredentials"` 174 | RoleList []interface{} `json:"roleList"` 175 | RoleIdList interface{} `json:"roleIdList"` 176 | TeacherName interface{} `json:"teacherName"` 177 | } 178 | 179 | type LeaveReq struct { 180 | // 格式yyyy-mm-dd 181 | StartDate string `json:"startDate"` 182 | // 留空 183 | EndDate string `json:"endDate"` 184 | // 原因 185 | Reason string `json:"reason"` 186 | // 未知 187 | AuditType int `json:"auditType"` 188 | // 离校时间 ms时间戳 189 | OutTime string `json:"outTime"` 190 | // 返校时间 ms时间戳 191 | InTime string `json:"inTime"` 192 | // 前往的地区的行政区划代码 193 | AreaCode string `json:"areaCode"` 194 | // 目的地,格式如"浙江省-杭州市-上城区" 195 | Destination string `json:"destination"` 196 | // 附件列表,疑似先上传到指定oss 197 | FileList []OSSFile `json:"fileList"` 198 | } 199 | 200 | type Course struct { 201 | // 教师学院编号,如计算机为05 202 | TeacherUnitNo string `json:"teacherUnitNo"` 203 | // 教师学院名称 204 | TeacherUnitName string `json:"teacherUnitName"` 205 | // 未知,可能是教师号 206 | TeacherNo string `json:"teacherNo"` 207 | // 开课学年 208 | SchoolYear string `json:"schoolYear"` 209 | // 开课学期 210 | Semester string `json:"semester"` 211 | // 教师职称 212 | TeacherMajor string `json:"teacherMajor"` 213 | // 未知 214 | CourseSchemaId string `json:"courseSchemaId"` 215 | // 课程Id(不是课程代码,是UUID) 216 | CourseId string `json:"courseId"` 217 | // 课程名称 218 | CourseName string `json:"courseName"` 219 | // 上课节次 220 | StartSection int `json:"startSection"` 221 | // 下课节次 222 | EndSection int `json:"endSection"` 223 | // 开始上课周次 224 | StartWeek int `json:"startWeek"` 225 | // 结束上课周次 226 | EndWeek int `json:"endWeek"` 227 | // 单双周,可能为"单","双","" 228 | Period string `json:"period"` 229 | // 上课地址(教室) 230 | ClassRoom string `json:"classRoom"` 231 | // 上课weekday(1-6),周日未知 232 | WeekDay int `json:"weekDay"` 233 | // 教室名称 234 | TeacherName string `json:"teacherName"` 235 | // 课程代码(长,如:"(2022-2023-1)-C5692034-2") 236 | CourseCode string `json:"courseCode"` 237 | // 课程代码 (如:"C5692034") 238 | CourseNo string `json:"courseNo"` 239 | // 课程归属 (如:“艺术创作与审美体验”) 240 | CourseType string `json:"courseType"` 241 | // 学分 242 | Mark float64 `json:"mark"` 243 | // 未知 244 | ListenTime int `json:"listenTime"` 245 | // 未知 246 | ListenStatus interface{} `json:"listenStatus"` 247 | // 教学班组成 248 | CourseClass string `json:"courseClass"` 249 | // 未知 250 | TotalTime int `json:"totalTime"` 251 | // 学生数量 252 | StudentCount int `json:"studentCount"` 253 | // 开课学院名称 254 | UnitName string `json:"unitName"` 255 | // 开课学院编号 256 | UnitCode string `json:"unitCode"` 257 | // 开课时间(如:"星期三第1-2节{1-17周}"),解析可参考course_schema库 258 | CourseSchema string `json:"courseSchema"` 259 | // 未知,貌似均为"1",怀疑研究室不是"1" 260 | StudentType string `json:"studentType"` 261 | } 262 | 263 | type CourseResp struct { 264 | // 周次,请求参数中的startTime所对应的周次 265 | Week int `json:"week"` 266 | // 学年,如"2022-2023" 267 | Xn string `json:"xn"` 268 | // 学期,"1"或"2" 269 | Xq string `json:"xq"` 270 | // 请求参数的startTime,默认为当前学期第一天 271 | StartTime int64 `json:"startTime"` 272 | List []Course `json:"list"` 273 | } 274 | -------------------------------------------------------------------------------- /skl/err.go: -------------------------------------------------------------------------------- 1 | package skl 2 | 3 | import "errors" 4 | 5 | var ErrAlreadyPushed = errors.New("今日已经打卡") 6 | 7 | type errorMsg struct { 8 | Code int `json:"code"` 9 | Msg string `json:"msg"` 10 | } 11 | -------------------------------------------------------------------------------- /skl/file.go: -------------------------------------------------------------------------------- 1 | package skl 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/hduLib/hdu/client" 7 | "io" 8 | "net/http" 9 | "os" 10 | "path/filepath" 11 | ) 12 | 13 | type OSSFile struct { 14 | // "hdu-checkin" 15 | Bucket string `json:"bucket"` 16 | // 文件名(不含路径) 17 | FileName string `json:"fileName"` 18 | // 疑似为文件路径,如"leave-file/2022-08-28/[随机大小写数字混合19位字符串].png" 19 | Key string `json:"key"` 20 | } 21 | 22 | type signeResp struct { 23 | Key string `json:"key"` 24 | ContentType string `json:"contentType"` 25 | Bucket string `json:"bucket"` 26 | Host string `json:"host"` 27 | Url string `json:"url"` 28 | } 29 | 30 | const signeURL = "https://skl.hdu.edu.cn/api/oss/generateSigne?fileName=oss." 31 | 32 | func (user *User) Upload(file string) (*OSSFile, error) { 33 | // check file 34 | f, err := os.Open(file) 35 | if err != nil { 36 | return nil, err 37 | } 38 | // get signe url 39 | req, err := http.NewRequest(http.MethodGet, signeURL+filepath.Ext(file), nil) 40 | if err != nil { 41 | return nil, err 42 | } 43 | user.addHeaderToReq(req) 44 | resp, err := client.Do(req) 45 | if err != nil { 46 | return nil, err 47 | } 48 | body, err := io.ReadAll(resp.Body) 49 | if err != nil { 50 | return nil, err 51 | } 52 | if resp.StatusCode != http.StatusOK { 53 | return nil, fmt.Errorf("statuscode is not ok: %s", string(body)) 54 | } 55 | res := new(signeResp) 56 | if err := json.Unmarshal(body, res); err != nil { 57 | return nil, err 58 | } 59 | // upload 60 | req, err = http.NewRequest(http.MethodPut, res.Url, f) 61 | if err != nil { 62 | return nil, err 63 | } 64 | req.Header.Add("Content-Type", res.ContentType) 65 | resp, err = client.Do(req) 66 | if err != nil { 67 | return nil, err 68 | } 69 | if resp.StatusCode != http.StatusOK { 70 | body, err = io.ReadAll(resp.Body) 71 | if err != nil { 72 | return nil, err 73 | } 74 | return nil, fmt.Errorf("statuscode is not ok: %s", string(body)) 75 | } 76 | return &OSSFile{ 77 | Bucket: res.Bucket, 78 | FileName: filepath.Base(file), 79 | Key: res.Key, 80 | }, nil 81 | } 82 | -------------------------------------------------------------------------------- /skl/file_test.go: -------------------------------------------------------------------------------- 1 | package skl 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestUser_Upload(t *testing.T) { 8 | skl, err := Login(id, passwd) 9 | if err != nil { 10 | t.Error(err) 11 | return 12 | } 13 | file, err := skl.Upload("C:\\Users\\a\\Pictures\\zh.png") 14 | if err != nil { 15 | t.Error(err) 16 | return 17 | } 18 | t.Log(file) 19 | } 20 | -------------------------------------------------------------------------------- /skl/login.go: -------------------------------------------------------------------------------- 1 | package skl 2 | 3 | import ( 4 | "errors" 5 | "github.com/hduLib/hdu/cas" 6 | "github.com/hduLib/hdu/client" 7 | "github.com/tidwall/gjson" 8 | "io" 9 | "net/http" 10 | ) 11 | 12 | func Login(id, password string) (*User, error) { 13 | req, err := http.NewRequest(http.MethodGet, casLoginURL, nil) 14 | if err != nil { 15 | return nil, err 16 | } 17 | resp, err := client.Do(req) 18 | if err != nil { 19 | return nil, err 20 | } 21 | body, err := io.ReadAll(resp.Body) 22 | if err != nil { 23 | return nil, err 24 | } 25 | url := gjson.Get(string(body), "url").String() 26 | req, err = cas.GenLoginReq(url, id, password) 27 | if err != nil { 28 | return nil, err 29 | } 30 | XAuthToken := "" 31 | c := &http.Client{CheckRedirect: func(req *http.Request, via []*http.Request) error { 32 | token := req.Response.Header.Get("X-Auth-Token") 33 | if token != "" { 34 | XAuthToken = token 35 | } 36 | return nil 37 | }} 38 | resp, err = c.Do(req) 39 | if err != nil { 40 | return nil, err 41 | } 42 | if XAuthToken == "" { 43 | return nil, errors.New("fail to get xauthtoken") 44 | } 45 | return &User{ 46 | xAuthToken: XAuthToken, 47 | }, nil 48 | } 49 | -------------------------------------------------------------------------------- /skl/login_test.go: -------------------------------------------------------------------------------- 1 | package skl 2 | 3 | import "testing" 4 | 5 | func TestLogin(t *testing.T) { 6 | skl, err := Login(id, passwd) 7 | if err != nil { 8 | t.Error(err) 9 | return 10 | } 11 | t.Log(skl.xAuthToken) 12 | return 13 | } 14 | -------------------------------------------------------------------------------- /skl/method.go: -------------------------------------------------------------------------------- 1 | package skl 2 | 3 | import ( 4 | "github.com/hduLib/hdu/skl/schema" 5 | "time" 6 | ) 7 | 8 | // HasPush check if HasPushed on the day defined by t 9 | // notice while push logs have multi pages,it only checks one. 10 | func (r *PushLogResp) HasPush(t time.Time) bool { 11 | unix := time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, time.Local).UnixMilli() 12 | for _, v := range r.List { 13 | if unix == v.CreateDate { 14 | return true 15 | } 16 | } 17 | return false 18 | } 19 | 20 | func (c *Course) DecodeSchema() (schema.Schema, error) { 21 | return schema.Decode(c.CourseSchema) 22 | } 23 | -------------------------------------------------------------------------------- /skl/pos/pos.go: -------------------------------------------------------------------------------- 1 | package pos 2 | 3 | type Pos struct { 4 | Longitude float64 5 | Latitude float64 6 | } 7 | 8 | var ( 9 | Building6North = Pos{120.337400, 30.317000} 10 | Building6Middle = Pos{120.337400, 30.316560} 11 | Building6South = Pos{120.337400, 30.3161700} 12 | Building7North = Pos{120.339800, 30.317000} 13 | Building7Middle = Pos{120.339250, 30.316750} 14 | Building7South = Pos{120.339800, 30.316600} 15 | ) 16 | -------------------------------------------------------------------------------- /skl/schema/schema.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "fmt" 5 | "regexp" 6 | "strconv" 7 | "strings" 8 | "time" 9 | ) 10 | 11 | type schemaNode struct { 12 | Begin, End int 13 | Weekday time.Weekday 14 | WeekNum weeks 15 | } 16 | 17 | type Schema []schemaNode 18 | 19 | var schemaReg = regexp.MustCompile(`星期([一二三四五六日])第(\d*)-(\d*)节{(.*)}`) 20 | var weeksReg = regexp.MustCompile(`(\d*)-(\d*)周(\((.)\))?`) 21 | 22 | var weekDayMapping = map[string]time.Weekday{ 23 | "一": time.Monday, 24 | "二": time.Tuesday, 25 | "三": time.Wednesday, 26 | "四": time.Thursday, 27 | "五": time.Friday, 28 | "六": time.Saturday, 29 | "日": time.Sunday, 30 | } 31 | 32 | // Check if exist. especially, [begin,end] is a range , which means it returns true 33 | // with input [1,12] if there is any course this day. 34 | func (s Schema) Check(begin, end int, weekday time.Weekday, weekNum int) bool { 35 | for _, v := range s { 36 | if v.Begin >= begin && v.End <= end && v.Weekday == weekday && v.WeekNum.Check(weekNum) { 37 | return true 38 | } 39 | } 40 | return false 41 | } 42 | 43 | func Decode(str string) (Schema, error) { 44 | s := strings.Split(str, ";") 45 | schema := make(Schema, 0, len(s)) 46 | for _, ss := range s { 47 | matches := schemaReg.FindAllStringSubmatch(ss, -1) 48 | if len(matches) != 1 { 49 | return nil, fmt.Errorf("ErrDecodingSchema:invalid schema node count") 50 | } 51 | info := matches[0] 52 | if len(info) != 5 { 53 | return nil, fmt.Errorf("ErrDecodingSchema:missing schemaNode info") 54 | } 55 | var n schemaNode 56 | n.Weekday = weekDayMapping[info[1]] 57 | var err error 58 | n.Begin, err = strconv.Atoi(info[2]) 59 | if err != nil { 60 | return nil, fmt.Errorf("ErrDecodingSchema:%v", err) 61 | } 62 | n.End, err = strconv.Atoi(info[3]) 63 | if err != nil { 64 | return nil, fmt.Errorf("ErrDecodingSchema:%v", err) 65 | } 66 | n.WeekNum, err = decodeWeeks(info[4]) 67 | if err != nil { 68 | return nil, fmt.Errorf("ErrDecodingSchema:%v", err) 69 | } 70 | schema = append(schema, n) 71 | } 72 | return schema, nil 73 | } 74 | 75 | func decodeWeeks(str string) (weeks, error) { 76 | ss := strings.Split(str, ",") 77 | if len(ss) == 0 { 78 | return 0, nil 79 | } 80 | var w weeks 81 | for _, v := range ss { 82 | if strings.Contains(v, "-") { 83 | var status int 84 | matches := weeksReg.FindAllStringSubmatch(v, -1) 85 | if len(matches) != 1 { 86 | return w, fmt.Errorf("ErrDecodingWeeks:invalid weeks count") 87 | } 88 | begin, err := strconv.Atoi(matches[0][1]) 89 | if err != nil { 90 | return 0, fmt.Errorf("ErrDecodingWeeks:invalid begin time") 91 | } 92 | end, err := strconv.Atoi(matches[0][2]) 93 | if err != nil { 94 | return 0, fmt.Errorf("ErrDecodingWeeks:invalid end time") 95 | } 96 | if len(matches[0]) == 5 { 97 | switch matches[0][4] { 98 | case "单": 99 | status = 0 100 | case "双": 101 | status = 1 102 | default: 103 | status = 2 104 | } 105 | } 106 | for begin <= end { 107 | if status == begin%2 { 108 | begin++ 109 | continue 110 | } 111 | w |= 1 << begin 112 | begin++ 113 | } 114 | } else { 115 | var t int 116 | _, err := fmt.Sscanf(v, "%d周", &t) 117 | if err != nil { 118 | return w, fmt.Errorf("errDecodingWeeks:%v", err) 119 | } 120 | w |= 1 << t 121 | } 122 | } 123 | return w, nil 124 | } 125 | -------------------------------------------------------------------------------- /skl/schema/schema_test.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | import ( 4 | "testing" 5 | "time" 6 | ) 7 | 8 | func TestSchemaDecode(t *testing.T) { 9 | s, err := Decode("星期三第6-7节{8-10周(双),14周};星期日第10-11节{12周}") 10 | if err != nil { 11 | t.Fatal(err) 12 | return 13 | } 14 | t.Log(s) 15 | for _, v := range s { 16 | if v.Weekday == time.Wednesday { 17 | if !v.WeekNum.Check(8) && v.WeekNum.Check(9) && !v.WeekNum.Check(10) { 18 | t.Fatal() 19 | } 20 | } else if v.Weekday == time.Sunday { 21 | if !v.WeekNum.Check(12) { 22 | t.Fatal() 23 | } 24 | } 25 | } 26 | if !s.Check(6, 7, time.Wednesday, 8) { 27 | t.Fatal() 28 | } 29 | if !s.Check(1, 12, time.Wednesday, 8) { 30 | t.Fatal() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /skl/schema/weeks.go: -------------------------------------------------------------------------------- 1 | package schema 2 | 3 | type weeks uint32 4 | 5 | // Check if the schema run at specify week[1-31] 6 | func (w *weeks) Check(n int) bool { 7 | if n > 32 || n < 1 { 8 | return false 9 | } 10 | return *w&(1<(.*?)

") 15 | var executionRegexp = regexp.MustCompile("

(.*?)

") 16 | 17 | func GenLoginReq(URL, user, passwd string) (*http.Request, error) { 18 | var key, execution []byte 19 | req, err := http.NewRequest(http.MethodGet, URL, nil) 20 | if err != nil { 21 | return nil, fmt.Errorf("create request: %v", err) 22 | } 23 | req.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0") 24 | resp, err := client.Do(req) 25 | if err != nil { 26 | return nil, err 27 | } 28 | if resp.StatusCode != 200 { 29 | reason, err := io.ReadAll(resp.Body) 30 | if err != nil { 31 | return nil, fmt.Errorf("read body: %v", err) 32 | } 33 | return nil, fmt.Errorf("get key lt and excution: %s", string(reason)) 34 | } 35 | body, err := io.ReadAll(resp.Body) 36 | if err != nil { 37 | return nil, err 38 | } 39 | tmp := keyRegexp.FindSubmatch(body) 40 | if len(tmp) != 2 { 41 | return nil, errors.New("match key") 42 | } 43 | key = tmp[1] 44 | tmp = executionRegexp.FindSubmatch(body) 45 | if len(tmp) != 2 { 46 | return nil, errors.New("match execution") 47 | } 48 | execution = tmp[1] 49 | bytes.Trim(key, " \"\r\n") 50 | 51 | //获取password 52 | encryptedPasswd, err := EncryptPasswd(key, passwd) 53 | if err != nil { 54 | return nil, fmt.Errorf("encrypt password: %v", err) 55 | } 56 | 57 | postData := url.Values{} 58 | postData.Set("username", user) 59 | postData.Set("passwordPre", passwd) 60 | postData.Set("password", encryptedPasswd) 61 | postData.Set("type", "UsernamePassword") 62 | postData.Set("_eventId", "submit") 63 | postData.Set("geolocation", "") 64 | postData.Set("execution", string(execution)) 65 | // missing spelling from hdu, so what can I say? 66 | postData.Set("croypto", string(key)) 67 | 68 | req, err = http.NewRequest(http.MethodPost, URL, bytes.NewBufferString(postData.Encode())) 69 | if err != nil { 70 | return nil, err 71 | } 72 | req.Header.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36 Edg/124.0.0.0") 73 | req.Header.Add("Referer", URL) 74 | req.Header.Add("Content-Type", "application/x-www-form-urlencoded") 75 | for _, c := range resp.Cookies() { 76 | req.AddCookie(c) 77 | } 78 | 79 | var nextReq *http.Request 80 | c := &http.Client{ 81 | CheckRedirect: func(req *http.Request, via []*http.Request) error { 82 | nextReq = req 83 | return http.ErrUseLastResponse 84 | }, 85 | } 86 | 87 | resp, err = c.Do(req) 88 | if err != nil { 89 | return nil, fmt.Errorf("do request: %v", err) 90 | } 91 | 92 | if nextReq == nil || nextReq.URL.Hostname() == "sso.hdu.edu.cn" { 93 | return nil, errors.New("login failed") 94 | } 95 | 96 | return nextReq, nil 97 | } 98 | -------------------------------------------------------------------------------- /sso/auth_test.go: -------------------------------------------------------------------------------- 1 | package sso 2 | 3 | import ( 4 | "testing" 5 | ) 6 | 7 | func TestLogin(t *testing.T) { 8 | req, err := GenLoginReq("https://sso.hdu.edu.cn/login?service=https%3A%2F%2Fi.hdu.edu.cn%2Fsopcb%2F", "21111111", "11111111") 9 | if err != nil { 10 | t.Error(err) 11 | return 12 | } 13 | t.Log(req) 14 | } 15 | -------------------------------------------------------------------------------- /sso/ecb.go: -------------------------------------------------------------------------------- 1 | package sso 2 | 3 | import ( 4 | "bytes" 5 | "crypto/des" 6 | "encoding/base64" 7 | "fmt" 8 | ) 9 | 10 | func EncryptPasswd(key []byte, password string) (string, error) { 11 | var keyBytes [8]byte 12 | if _, err := base64.StdEncoding.Decode(keyBytes[:], key); err != nil { 13 | return "", fmt.Errorf("decode key: %v", err) 14 | } 15 | cipher, err := des.NewCipher(keyBytes[:]) 16 | if err != nil { 17 | return "", fmt.Errorf("des cipher: %v", err) 18 | } 19 | // padding 20 | text := pkcs7Padding([]byte(password), cipher.BlockSize()) 21 | // ecb mode 22 | for i := 0; i < len(text); i += cipher.BlockSize() { 23 | cipher.Encrypt(text[i:], text[i:]) 24 | } 25 | return base64.StdEncoding.EncodeToString(text), nil 26 | } 27 | 28 | func pkcs7Padding(data []byte, blockSize int) []byte { 29 | padding := blockSize - len(data)%blockSize 30 | return append(data, bytes.Repeat([]byte{byte(padding)}, padding)...) 31 | } 32 | -------------------------------------------------------------------------------- /zjooc/api.go: -------------------------------------------------------------------------------- 1 | package zjooc 2 | 3 | const pageSize = 10 4 | 5 | func (u *User) CurrentCourses(status publishStatus) ([]Course, error) { 6 | var no = 0 7 | return allPages[Course](u, func() string { 8 | no++ 9 | return coursesUrl(status, no, pageSize) 10 | }) 11 | } 12 | 13 | // PapersByCourse batchKey 同 Course 的 batchId 14 | func (u *User) PapersByCourse(courseId string, Type paperType, batchKey string) ([]Paper, error) { 15 | var no = 0 16 | return allPages[Paper](u, func() string { 17 | no++ 18 | return paperUrl(Type, courseId, batchKey, no, pageSize) 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /zjooc/course_test.go: -------------------------------------------------------------------------------- 1 | package zjooc 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | ) 8 | 9 | func TestUser_GetCurrentCourses(t *testing.T) { 10 | user, err := Login(os.Getenv("zjooc_account"), os.Getenv("zjooc_passwd")) 11 | if err != nil { 12 | t.Log(err) 13 | return 14 | } 15 | courses, err := user.CurrentCourses(Published) 16 | if err != nil { 17 | t.Log(err) 18 | return 19 | } 20 | fmt.Println(courses) 21 | } 22 | 23 | func TestUser_PapersByCourse(t *testing.T) { 24 | user, err := Login(os.Getenv("zjooc_account"), os.Getenv("zjooc_passwd")) 25 | if err != nil { 26 | t.Log(err) 27 | return 28 | } 29 | papers, err := user.PapersByCourse("2c91808281b87da50181cd026be14a85", Assignment, "20221") 30 | if err != nil { 31 | t.Log(err) 32 | return 33 | } 34 | fmt.Println(papers) 35 | } 36 | -------------------------------------------------------------------------------- /zjooc/data.go: -------------------------------------------------------------------------------- 1 | package zjooc 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | const ( 8 | loginUrl = "https://service.zjooc.cn/service/centro/api/auth/app/authorize" 9 | chapterUrl = "https://service.zjooc.cn/service/jxxt/api/app/course/chapter/getStudentCourseChapters" 10 | videoUrl = "https://service.zjooc.cn/service/learningmonitor/api/learning/monitor/videoPlaying" 11 | textUrl = "https://service.zjooc.cn/service/learningmonitor/api/learning/monitor/finishTextChapter" 12 | ) 13 | 14 | type publishStatus = int 15 | 16 | const ( 17 | Published publishStatus = iota + 3 18 | Finished 19 | // 未发布课程的publishStatus未知 20 | ) 21 | 22 | type paperType = int 23 | 24 | const ( 25 | // Exam 考试 26 | Exam paperType = iota 27 | // Test 测验 28 | Test 29 | // Assignment 作业 30 | Assignment 31 | ) 32 | 33 | func coursesUrl(status publishStatus, pageNo, pageSize int) string { 34 | return fmt.Sprintf("https://service.zjooc.cn/service/jxxt/api/app/course/student/course?publishStatus=%d&pageNo=%d&pageSize=%d", status, pageNo, pageSize) 35 | } 36 | 37 | func paperUrl(Type paperType, courseId string, batchKey string, pageNo, pageSize int) string { 38 | return fmt.Sprintf("https://service.zjooc.cn/service/tkksxt/api/admin/paper/student/page?paperType=%d&courseId=%s&batchKey=%s&pageNo=%d&pageSize=%d", Type, courseId, batchKey, pageNo, pageSize) 39 | } 40 | 41 | type Resp[T any] struct { 42 | // 成功为"操作成功" 43 | Message string `json:"message"` 44 | // 成功为0 45 | ResultCode int `json:"resultCode"` 46 | // 成功为true 47 | Success bool `json:"success"` 48 | Data T `json:"data"` 49 | } 50 | 51 | type LoginReq struct { 52 | LoginName string `json:"login_name"` 53 | Password string `json:"password"` 54 | Type int `json:"type"` 55 | } 56 | 57 | type LoginResp struct { 58 | LoginResult struct { 59 | AccessToken string `json:"access_token"` 60 | AuthorizationCode string `json:"authorization_code"` 61 | // 后续仅需要openid 62 | Openid string `json:"openid"` 63 | RefreshToken string `json:"refresh_token"` 64 | UserCenterOpenId string `json:"userCenterOpenId"` 65 | } `json:"loginResult"` 66 | User struct { 67 | // 证件号 68 | Certificate string `json:"certificate"` 69 | // 证件类型,"1"为身份证 70 | CertificateType string `json:"certificateType"` 71 | CompleteInfo struct { 72 | // "杭州电子科技大学" 73 | CorpName string `json:"corpName"` 74 | InputCertificate int `json:"inputCertificate"` 75 | InputEmail int `json:"inputEmail"` 76 | InputLoginName int `json:"inputLoginName"` 77 | InputName int `json:"inputName"` 78 | InputPhone int `json:"inputPhone"` 79 | NeedComplete int `json:"needComplete"` 80 | RepeatCertificate int `json:"repeatCertificate"` 81 | RepeatPhone int `json:"repeatPhone"` 82 | } `json:"completeInfo"` 83 | // 邮箱 84 | Email string `json:"email"` 85 | // 疑似与学期相关,第几个学期就是几 86 | Gender int `json:"gender"` 87 | Id string `json:"id"` 88 | IsEmailVerified int `json:"isEmailVerified"` 89 | IsPhoneVerified int `json:"isPhoneVerified"` 90 | IsUserNameModified int `json:"isUserNameModified"` 91 | // "hdu_"+学号 92 | LoginName string `json:"loginName"` 93 | // 姓名 94 | Name string `json:"name"` 95 | // 学号 96 | No string `json:"no"` 97 | // 电话 98 | Phone string `json:"phone"` 99 | PsdAuth string `json:"psdAuth"` 100 | } `json:"user"` 101 | } 102 | 103 | type Course struct { 104 | Id string `json:"id"` 105 | TeacherName string `json:"teacherName"` 106 | TeacherId string `json:"teacherId"` 107 | CourseName string `json:"courseName"` 108 | CourseImgUrl string `json:"courseImgUrl"` 109 | CourseProgress float64 `json:"courseProgress"` 110 | PersistentPeriod int `json:"persistentPeriod"` 111 | PublishStatus int `json:"publishStatus"` 112 | StartDate string `json:"startDate"` 113 | EndDate string `json:"endDate"` 114 | CurrentCycle int `json:"currentCycle"` 115 | CorpId string `json:"corpId"` 116 | TemplateType interface{} `json:"templateType"` 117 | Source int `json:"source"` 118 | Qxfbzt interface{} `json:"qxfbzt"` 119 | Profile string `json:"profile"` 120 | Current int `json:"current"` 121 | Duration int `json:"duration"` 122 | BatchId string `json:"batchId"` 123 | } 124 | 125 | type Paper struct { 126 | PublishTime string `json:"publishTime"` 127 | EndTime string `json:"endTime"` 128 | ClassId string `json:"classId"` 129 | PaperId string `json:"paperId"` 130 | PaperName string `json:"paperName"` 131 | FinalScore float64 `json:"finalScore"` 132 | TotalScore float64 `json:"totalScore"` 133 | CourseId string `json:"courseId"` 134 | CourseName string `json:"courseName"` 135 | // 0 沒做, 1 做了 136 | ReviewStatus int `json:"reviewStatus"` 137 | // 0 开放, 2 截止, 4 应该是截止了但是没交 138 | ProcessStatus int `json:"processStatus"` 139 | ScorePropor string `json:"scorePropor"` 140 | PaperStyle int `json:"paperStyle"` 141 | PaperArchive int `json:"paperArchive"` 142 | } 143 | -------------------------------------------------------------------------------- /zjooc/login.go: -------------------------------------------------------------------------------- 1 | package zjooc 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "github.com/hduLib/hdu/client" 8 | "github.com/tidwall/gjson" 9 | "net/http" 10 | ) 11 | 12 | func Login(account, password string) (*User, error) { 13 | payload := LoginReq{ 14 | LoginName: account, 15 | Password: password, 16 | Type: 1, 17 | } 18 | b, err := json.Marshal(&payload) 19 | if err != nil { 20 | return nil, err 21 | } 22 | req, err := http.NewRequest(http.MethodPost, loginUrl, bytes.NewReader(b)) 23 | // UA:在浙学/2 CFNetwork/1390 Darwin/22.0.0 24 | req.Header.Set("User-Agent", "%E5%9C%A8%E6%B5%99%E5%AD%A6/2 CFNetwork/1390 Darwin/22.0.0") 25 | req.Header.Set("Content-Type", "application/json; charset=utf-8") 26 | if err != nil { 27 | return nil, err 28 | } 29 | body, err := client.Post(req) 30 | str := string(body) 31 | success := gjson.Get(str, "success").Bool() 32 | if !success { 33 | return nil, errors.New(gjson.Get(str, "message").String()) 34 | } 35 | user := new(User) 36 | user.openid = gjson.Get(str, "data.loginResult.openid").String() 37 | return user, nil 38 | } 39 | -------------------------------------------------------------------------------- /zjooc/login_test.go: -------------------------------------------------------------------------------- 1 | package zjooc 2 | 3 | import ( 4 | "os" 5 | "testing" 6 | ) 7 | 8 | func TestLogin(t *testing.T) { 9 | user, err := Login(os.Getenv("zjooc_account"), os.Getenv("zjooc_passwd")) 10 | if err != nil { 11 | t.Log(err) 12 | return 13 | } 14 | t.Log(user.openid) 15 | } 16 | -------------------------------------------------------------------------------- /zjooc/pages.go: -------------------------------------------------------------------------------- 1 | package zjooc 2 | 3 | func allPages[T any](u *User, getNextURL func() string) ([]T, error) { 4 | var items []T 5 | for no := 1; ; no++ { 6 | url := getNextURL() 7 | resp := new(Resp[[]T]) 8 | err := u.get(url, resp) 9 | if err != nil { 10 | return nil, err 11 | } 12 | for _, v := range resp.Data { 13 | items = append(items, v) 14 | } 15 | if len(items) < no*pageSize { 16 | break 17 | } 18 | } 19 | return items, nil 20 | } 21 | -------------------------------------------------------------------------------- /zjooc/zjooc.go: -------------------------------------------------------------------------------- 1 | package zjooc 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "github.com/hduLib/hdu/client" 7 | "net/http" 8 | ) 9 | 10 | type User struct { 11 | openid string 12 | } 13 | 14 | func (u *User) addHeaderToReq(req *http.Request) { 15 | if req == nil { 16 | return 17 | } 18 | req.Header.Add("openid", u.openid) 19 | req.Header.Set("User-Agent", "%E5%9C%A8%E6%B5%99%E5%AD%A6/2 CFNetwork/1390 Darwin/22.0.0") 20 | req.Header.Set("Content-Type", "application/json; charset=utf-8") 21 | } 22 | 23 | func (u *User) newPost(url string, body []byte) (*http.Request, error) { 24 | req, err := http.NewRequest(http.MethodPost, url, bytes.NewReader(body)) 25 | if err != nil { 26 | return nil, err 27 | } 28 | u.addHeaderToReq(req) 29 | return req, nil 30 | } 31 | 32 | func (u *User) newGet(url string) (*http.Request, error) { 33 | req, err := http.NewRequest(http.MethodGet, url, nil) 34 | if err != nil { 35 | return nil, err 36 | } 37 | u.addHeaderToReq(req) 38 | return req, nil 39 | } 40 | 41 | func (u *User) get(url string, data interface{}) error { 42 | req, err := u.newGet(url) 43 | if err != nil { 44 | return err 45 | } 46 | return client.Get(req, data) 47 | } 48 | 49 | func (u *User) post(url string, data interface{}) ([]byte, error) { 50 | reqBody, err := json.Marshal(data) 51 | if err != nil { 52 | return nil, err 53 | } 54 | req, err := u.newPost(url, reqBody) 55 | if err != nil { 56 | return nil, err 57 | } 58 | return client.Post(req) 59 | } 60 | --------------------------------------------------------------------------------