├── .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 |
3 |
4 |
5 |
6 |
7 | # hdu
8 |
9 | 
10 | 
11 | [](https://pkg.go.dev/github.com/hduLib/hdu)
12 | [](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 |
--------------------------------------------------------------------------------