account.swift 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503
  1. //
  2. // account.swift
  3. // 74 桌
  4. //
  5. // Created by yunli on 2023/5/20.
  6. //
  7. import Alamofire
  8. import SwiftUI
  9. struct Constant {
  10. var cookiesDefaultsKey: String
  11. }
  12. let Constants: Constant = .init(cookiesDefaultsKey: "JSESSIONID")
  13. class CookieHandler {
  14. static let shared: CookieHandler = .init()
  15. let defaults = UserDefaults.standard
  16. let cookieStorage = HTTPCookieStorage.shared
  17. func getCookie(forURL url: String) -> [HTTPCookie] {
  18. let computedUrl = URL(string: url)
  19. let cookies = cookieStorage.cookies(for: computedUrl!) ?? []
  20. return cookies
  21. }
  22. func getAllCookie() -> [HTTPCookie] {
  23. let cookies = cookieStorage.cookies ?? []
  24. return cookies
  25. }
  26. func delCookie() {}
  27. }
  28. struct Login: Encodable {
  29. let user: String
  30. let pwd: String
  31. let cook: String
  32. }
  33. struct UserInfo: Decodable {
  34. var name: String
  35. var oid: String
  36. var id: String
  37. var xydLink: String
  38. var serviceLink: String
  39. var studyLink: String
  40. var tableData: [[PurpleDatum]]
  41. init() {
  42. name = "-"
  43. oid = "-"
  44. id = "-"
  45. xydLink = "-"
  46. serviceLink = "-"
  47. studyLink = "-"
  48. tableData = []
  49. }
  50. }
  51. class LoginHandler {
  52. static let sharedInstance = LoginHandler()
  53. func getCookie(name: String) -> [HTTPCookie] {
  54. return CookieHandler().getCookie(forURL: "https://debug.sdsz.icu:81/").filter { cookie -> Bool in
  55. cookie.name == name
  56. }
  57. }
  58. func getAllCookie(name: String) -> [HTTPCookie] {
  59. return CookieHandler().getAllCookie().filter { cookie -> Bool in
  60. cookie.name == name
  61. }
  62. }
  63. func fetchLoginCookie(action: @escaping (_: String) -> Void) {
  64. if let cookie = getCookie(name: "JSESSIONID").first {
  65. HTTPCookieStorage.shared.deleteCookie(cookie)
  66. print(cookie)
  67. }
  68. let url = "https://debug.sdsz.icu:81/sso/login"
  69. AF.request(url, method: .get, parameters: nil, encoding: URLEncoding.default, headers: nil).response { _ in
  70. if let cookie = self.getCookie(name: "JSESSIONID").first {
  71. action(cookie.value)
  72. }
  73. }
  74. }
  75. }
  76. class FetchHandler {
  77. static let sharedInstance = FetchHandler()
  78. func matches(url: String) -> Bool {
  79. do {
  80. return try url.matches(of: Regex(#"(^\[\]$|param":"|"workRests"|<tr>|DOCTYPE|in\?username=|requestParams|\"newmessage\"|Sorry, Page Not Found|北师大实验中学--登录|404 Not Found|502 Bad Gateway)"#)).count != 0
  81. } catch {
  82. return true
  83. }
  84. }
  85. func getUrl(url: String) -> String {
  86. var ret: String = url
  87. ret = ret
  88. .replacing("dd.sdsz.com.cn", with: "debug.sdsz.icu:81")
  89. .replacing(/^http:/, with: "https:")
  90. .replacing("service=http%3A%2F%2Fdebug.sdsz.icu:81", with: "service=http%3A%2F%2Fdd.sdsz.com.cn")
  91. print("URL: \(ret)")
  92. return ret
  93. }
  94. func workFetch(url: String, action: @escaping (_: String) -> Void) {
  95. if matches(url: url) {
  96. action(url)
  97. } else {
  98. AF.request(getUrl(url: url), method: .get, parameters: nil, encoding: URLEncoding.default, headers: nil).responseString { res in
  99. if let resv = res.value {
  100. self.workFetch(url: resv, action: action)
  101. }
  102. }
  103. }
  104. }
  105. func fetchAny(url: String, action: @escaping (_: String) -> Void) {
  106. let urlFull = "https://debug.sdsz.icu:81/\(url)"
  107. workFetch(url: urlFull, action: action)
  108. }
  109. }
  110. func doLogin(user: String, password: String, action: @escaping (_: Int) -> Void) {
  111. LoginHandler().fetchLoginCookie { (cookie: String) in
  112. let login = Login(user: user, pwd: password, cook: cookie)
  113. print(login)
  114. AF.request("https://debug.sdsz.icu/andlogin", method: .post, parameters: login, encoder: JSONParameterEncoder.default, headers: nil).responseString { res in
  115. print("\(res)")
  116. if res.value == "success" {
  117. action(1)
  118. } else {
  119. action(0)
  120. }
  121. }
  122. }
  123. }
  124. // This file was generated from JSON Schema using quicktype, do not modify it directly.
  125. // To parse the JSON, add this file to your project and do:
  126. //
  127. // let welcome = try? JSONDecoder().decode(Welcome.self, from: jsonData)
  128. import Foundation
  129. // MARK: - Welcome
  130. struct Welcome: Codable {
  131. let workRests: [WorkREST]
  132. let blocks: [Block]
  133. }
  134. // MARK: - Block
  135. struct Block: Codable {
  136. let ownObjID, type: String
  137. let name: String
  138. let data: [BlockDatum]
  139. let weekMetas: JSONNull?
  140. enum CodingKeys: String, CodingKey {
  141. case ownObjID = "ownObjId"
  142. case type, name, data, weekMetas
  143. }
  144. }
  145. // MARK: - BlockDatum
  146. struct BlockDatum: Codable {
  147. let week: Int
  148. let weekMeta: WeekMeta
  149. let data: [PurpleDatum]
  150. }
  151. // MARK: - PurpleDatum
  152. struct PurpleDatum: Codable {
  153. let day, lesson: Int
  154. let data: [FluffyDatum]
  155. }
  156. // MARK: - FluffyDatum
  157. struct FluffyDatum: Codable {
  158. let timetableID: JSONNull?
  159. let className: String
  160. let classID: String
  161. let courseName: String
  162. let courseID, courseType: Int
  163. let placeID: JSONNull?
  164. let placeName: String
  165. let placeSn: String
  166. let teacher: [Teacher]
  167. let room: JSONNull?
  168. let start, end: String
  169. let timeScope: Int
  170. enum CodingKeys: String, CodingKey {
  171. case timetableID = "timetableId"
  172. case className
  173. case classID = "classId"
  174. case courseName
  175. case courseID = "courseId"
  176. case courseType
  177. case placeID = "placeId"
  178. case placeName, placeSn, teacher, room, start, end, timeScope
  179. }
  180. }
  181. // MARK: - Teacher
  182. struct Teacher: Codable {
  183. let userID: Int
  184. let userName: JSONNull?
  185. let fullName: String
  186. let userNo: JSONNull?
  187. enum CodingKeys: String, CodingKey {
  188. case userID = "userId"
  189. case userName, fullName, userNo
  190. }
  191. }
  192. // MARK: - WeekMeta
  193. struct WeekMeta: Codable {
  194. let morningRead: Bool
  195. let night, afternoon, morning, teachingDay: Int
  196. let time: [Time]
  197. }
  198. // MARK: - Time
  199. struct Time: Codable {
  200. let seq: Int
  201. let start, end: String
  202. }
  203. // MARK: - WorkREST
  204. struct WorkREST: Codable {
  205. let beginTime, endTime, time, lessonName: String
  206. }
  207. // MARK: - Encode/decode helpers
  208. class JSONNull: Codable, Hashable {
  209. public static func == (_: JSONNull, _: JSONNull) -> Bool {
  210. return true
  211. }
  212. func hash(into _: inout Hasher) {}
  213. public init() {}
  214. public required init(from decoder: Decoder) throws {
  215. let container = try decoder.singleValueContainer()
  216. if !container.decodeNil() {
  217. throw DecodingError.typeMismatch(JSONNull.self, DecodingError.Context(codingPath: decoder.codingPath, debugDescription: "Wrong type for JSONNull"))
  218. }
  219. }
  220. public func encode(to encoder: Encoder) throws {
  221. var container = encoder.singleValueContainer()
  222. try container.encodeNil()
  223. }
  224. }
  225. var Count: Int = 0
  226. func addCompleted(userInfo: UserInfo, action: @escaping (_: UserInfo) -> Void) {
  227. Count += 1
  228. if Count == 3 {
  229. print(userInfo.tableData)
  230. action(userInfo)
  231. }
  232. }
  233. extension String {
  234. func replace(regex: String, with: String, options: NSRegularExpression.Options) -> String {
  235. do {
  236. let RE = try NSRegularExpression(pattern: regex, options: options)
  237. let modified = RE.stringByReplacingMatches(in: self, range: NSRange(location: 0, length: count), withTemplate: with)
  238. return modified
  239. } catch {
  240. return "ERR"
  241. }
  242. }
  243. func replace(regex: String, with: String) -> String {
  244. do {
  245. let RE = try NSRegularExpression(pattern: regex)
  246. let modified = RE.stringByReplacingMatches(in: self, range: NSRange(location: 0, length: count), withTemplate: with)
  247. return modified
  248. } catch {
  249. return "ERR"
  250. }
  251. }
  252. }
  253. struct TableParam: Encodable {
  254. var oid: String
  255. }
  256. func getUserInfo(userInfoIn: UserInfo, action: @escaping (_: UserInfo) -> Void) {
  257. var userInfo: UserInfo = userInfoIn
  258. FetchHandler().fetchAny(url: "bxn-portal/portal/osforstudent/index") { res in
  259. if let mat = res.firstMatch(of: /userFullName" value="(.*?)"/) {
  260. userInfo.name = "\(mat.1)"
  261. }
  262. if let mat = res.firstMatch(of: /userId" value="(.*?)"/) {
  263. userInfo.oid = "\(mat.1)"
  264. }
  265. if let mat = res.firstMatch(of: /'https:\/\/service.*?'/) {
  266. userInfo.serviceLink = "\(mat.output.split(separator: "'")[0])"
  267. userInfo.studyLink = mat.output.split(separator: "'")[0] + "%2Ffe-pc%2Fb%2Ffe_leco_student%2Fportal%2F%3Fsystem_partition_gId%3D3"
  268. }
  269. print(userInfo)
  270. let tableParam: TableParam = .init(oid: userInfo.oid)
  271. let header: HTTPHeaders = [
  272. "user": userInfo.id,
  273. ]
  274. AF.request("https://debug.sdsz.icu/getWeek", method: .post, parameters: tableParam, encoder: JSONParameterEncoder.default, headers: header).responseString { res in
  275. FetchHandler().fetchAny(url: "bxn-timetable/timetable/monitor/homepage/data/student?studentId=\(userInfo.oid)&dateScope=\(res.value ?? "")&_=0") { res in
  276. do {
  277. let data = res.data(using: String.Encoding.utf8)
  278. let table = try! JSONSerialization.jsonObject(with: data!) as? [String: Any]
  279. let decoder = JSONDecoder()
  280. let kkd = try decoder.decode(Welcome.self, from: data!)
  281. let pp = kkd.blocks.first?.data.first?.data ?? []
  282. var ret: [[PurpleDatum]] = [], mx: Int = 0
  283. for i in 0 ..< pp.count {
  284. print(pp[i])
  285. mx = max(mx, pp[i].lesson)
  286. }
  287. for i in 0 ... mx {
  288. ret.append([])
  289. for j in 0 ... 5 {
  290. ret[i].append(PurpleDatum(day: 0, lesson: 0, data: []))
  291. }
  292. }
  293. for i in 0 ..< pp.count {
  294. ret[pp[i].lesson][pp[i].day] = pp[i]
  295. }
  296. userInfo.tableData = ret
  297. } catch {
  298. print(error)
  299. }
  300. addCompleted(userInfo: userInfo, action: action)
  301. }
  302. }
  303. addCompleted(userInfo: userInfo, action: action)
  304. }
  305. FetchHandler().fetchAny(url: "bxn-library/library/jumpExamreport?jumpUrl=http://36.112.23.77/analysis/auto/%23/autoLogin") { res in
  306. userInfo.xydLink = res
  307. addCompleted(userInfo: userInfo, action: action)
  308. }
  309. }
  310. struct loginView: View {
  311. @State var username: String = ""
  312. @State var password: String = ""
  313. @State var btnText: String = "登录"
  314. @State var btnColor: Color = .blue
  315. @State var inProgress: Bool = false
  316. @FocusState private var isFocused: Bool
  317. @Binding var isLoggedIn: Int
  318. @Binding var userInfo: UserInfo
  319. var body: some View {
  320. VStack {
  321. Form {
  322. List {
  323. HStack {
  324. Image(systemName: "person.fill").foregroundColor(Color(red: 0.7, green: 0.7, blue: 0.7))
  325. TextField(text: $username, prompt: Text("数字校园号")) {
  326. Text("数字校园号")
  327. }.focused($isFocused)
  328. .keyboardType(.numberPad)
  329. }
  330. HStack {
  331. Image(systemName: "key.fill").foregroundColor(Color(red: 0.7, green: 0.7, blue: 0.7))
  332. SecureField(text: $password, prompt: Text("密码")) {
  333. Text("密码")
  334. }.focused($isFocused)
  335. }
  336. }.onTapGesture {
  337. isFocused = false
  338. }
  339. HStack {
  340. Spacer()
  341. Button(action: {
  342. inProgress = true
  343. doLogin(user: username, password: password) { (ret: Int) in
  344. isLoggedIn = ret
  345. inProgress = false
  346. if ret == 0 {
  347. btnText = "登录失败"
  348. btnColor = .red
  349. } else {
  350. userInfo.id = username
  351. getUserInfo(userInfoIn: userInfo) { res in
  352. userInfo = res
  353. }
  354. }
  355. }
  356. }) {
  357. HStack {
  358. Text(btnText)
  359. if inProgress {
  360. ProgressView()
  361. } else {
  362. Image(systemName: "chevron.right")
  363. }
  364. }
  365. }.foregroundColor(btnColor).buttonStyle(.bordered)
  366. }
  367. }
  368. }
  369. }
  370. }
  371. struct accountView: View {
  372. @State var username: String = ""
  373. @State var password: String = ""
  374. @FocusState private var isFocused: Bool
  375. @Binding var isLoggedIn: Int
  376. @Binding var userInfo: UserInfo
  377. func delAllCookie(name: String) {
  378. for cookie in LoginHandler().getAllCookie(name: name) {
  379. print(cookie)
  380. HTTPCookieStorage.shared.deleteCookie(cookie)
  381. }
  382. }
  383. var body: some View {
  384. VStack {
  385. Form {
  386. Section(header: Text("基本信息")) {
  387. HStack {
  388. Text("姓名")
  389. Spacer()
  390. Text(userInfo.name).foregroundColor(.gray)
  391. }
  392. HStack {
  393. Text("内部 ID")
  394. Spacer()
  395. Text(userInfo.oid).foregroundColor(.gray)
  396. }
  397. }
  398. Section {
  399. NavigationLink(destination: aboutView()) {
  400. Text("关于")
  401. }
  402. }
  403. Section {
  404. Button(action: {
  405. isLoggedIn = 0
  406. delAllCookie(name: "CASTGC")
  407. delAllCookie(name: "JSESSIONID")
  408. }) {
  409. VStack {
  410. Text("退出").foregroundColor(.red)
  411. }
  412. }
  413. }
  414. }
  415. Spacer()
  416. }
  417. }
  418. }
  419. struct aboutView: View {
  420. var body: some View {
  421. List {
  422. Section(footer: Text("实验中学 74 桌是一款面向北师大附属实验中学学生的 App,提供查分及数字校园基础服务。")) {
  423. Text("实验中学 74 桌")
  424. HStack {
  425. Text("开发者")
  426. Spacer()
  427. Text("74 开发组").foregroundColor(.gray)
  428. }
  429. }
  430. }.navigationBarTitle("关于")
  431. }
  432. }
  433. struct accountView_Previews: PreviewProvider {
  434. @State static var isLoggedIn = 1
  435. @State static var userInfo: UserInfo = .init()
  436. static var previews: some View {
  437. loginView(isLoggedIn: $isLoggedIn, userInfo: $userInfo)
  438. }
  439. }