Skip to content

Commit 8af96df

Browse files
authored
Merge pull request #1 from ddanilyuk/feature/campus
feature/campus
2 parents ab0af8c + 65aaada commit 8af96df

19 files changed

+568
-28
lines changed
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
//
2+
// CampusController.swift
3+
//
4+
//
5+
// Created by Denys Danyliuk on 02.06.2022.
6+
//
7+
8+
import Vapor
9+
import KPIHubParser
10+
import Routes
11+
12+
struct StudySheetResponse: Content {
13+
let studySheet: [StudySheetItem]
14+
}
15+
16+
struct StudySheetItem: Content {
17+
let lesson: StudySheetLesson
18+
let activities: [StudySheetActivity]
19+
}
20+
21+
22+
final class CampusController {
23+
24+
func userInfo(
25+
request: Request,
26+
loginQuery: CampusLoginQuery
27+
) async throws -> UserInfo {
28+
let oauthResponse = try await oauth(request: request, loginQuery: loginQuery)
29+
let campusAPICredentials = try oauthResponse.content.decode(CampusAPICredentials.self)
30+
31+
let accountInfoResponse: ClientResponse = try await request.client.get(
32+
"https://api.campus.kpi.ua/Account/Info",
33+
beforeSend: { clientRequest in
34+
let auth = BearerAuthorization(token: campusAPICredentials.accessToken)
35+
clientRequest.headers.bearerAuthorization = auth
36+
}
37+
)
38+
return try accountInfoResponse.content.decode(UserInfo.self)
39+
}
40+
41+
func studySheet(
42+
request: Request,
43+
loginQuery: CampusLoginQuery
44+
) async throws -> StudySheetResponse {
45+
46+
let oauthResponse = try await oauth(request: request, loginQuery: loginQuery)
47+
let campusAPICredentials = try oauthResponse.content.decode(CampusAPICredentials.self)
48+
49+
let authPHPResponse: ClientResponse = try await request.client.get(
50+
"https://campus.kpi.ua/auth.php",
51+
beforeSend: { clientRequest in
52+
clientRequest.headers.cookie = oauthResponse.headers.setCookie
53+
}
54+
)
55+
56+
let studySheetResponse: ClientResponse = try await request.client.get(
57+
"https://campus.kpi.ua/student/index.php?mode=studysheet",
58+
beforeSend: { clientRequest in
59+
let auth = BearerAuthorization(token: campusAPICredentials.accessToken)
60+
clientRequest.headers.bearerAuthorization = auth
61+
if let phpSessionId = authPHPResponse.headers.setCookie?.all["PHPSESSID"] {
62+
clientRequest.headers.cookie = HTTPCookies(
63+
dictionaryLiteral: ("PHPSESSID", phpSessionId)
64+
)
65+
}
66+
}
67+
)
68+
69+
let html = try (studySheetResponse.body).htmlString(encoding: .windowsCP1251)
70+
71+
let studySheetItems = try await StudySheetLessonsParser().parse(html)
72+
.asyncMap { lesson -> StudySheetItem in
73+
let response: ClientResponse = try await request.client.post(
74+
"https://campus.kpi.ua\(lesson.link)",
75+
beforeSend: { clientRequest in
76+
let auth = BearerAuthorization(token: campusAPICredentials.accessToken)
77+
clientRequest.headers.bearerAuthorization = auth
78+
if let phpSessionId = authPHPResponse.headers.setCookie?.all["PHPSESSID"] {
79+
clientRequest.headers.cookie = HTTPCookies(
80+
dictionaryLiteral: ("PHPSESSID", phpSessionId)
81+
)
82+
}
83+
}
84+
)
85+
let html = try (response.body).htmlString(encoding: .windowsCP1251)
86+
let activities = try StudySheetActivitiesParser().parse(html)
87+
return StudySheetItem(lesson: lesson, activities: activities)
88+
}
89+
90+
return StudySheetResponse(studySheet: studySheetItems)
91+
}
92+
93+
// MARK: - Helpers
94+
95+
func oauth(
96+
request: Request,
97+
loginQuery: CampusLoginQuery
98+
) async throws -> ClientResponse {
99+
try await request.client.post(
100+
"https://api.campus.kpi.ua/oauth/token",
101+
beforeSend: { clientRequest in
102+
try clientRequest.query.encode(loginQuery)
103+
}
104+
)
105+
}
106+
107+
}

Sources/App/Controllers/GroupsController.swift

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import KPIHubParser
1010
import Fluent
1111
import FluentPostgresDriver
1212
import Foundation
13+
import Routes
1314

1415
final class GroupsController {
1516

@@ -21,6 +22,23 @@ final class GroupsController {
2122

2223
// MARK: - Requests
2324

25+
func allGroups(request: Request) async throws -> GroupsResponse {
26+
let groupModels = try await GroupModel.query(on: request.db).all()
27+
return GroupsResponse(numberOfGroups: groupModels.count, groups: groupModels)
28+
}
29+
30+
func search(request: Request, searchQuery: GroupSearchQuery) async throws -> GroupModel {
31+
// TODO: Handle multiple groups with one name
32+
let groupModel = try await GroupModel.query(on: request.db)
33+
.filter(\.$name == searchQuery.groupName)
34+
.first()
35+
if let groupModel = groupModel {
36+
return groupModel
37+
} else {
38+
throw Abort(.notFound, reason: "Group not found")
39+
}
40+
}
41+
2442
func forceRefresh(request: Request) async throws -> GroupsResponse {
2543
let groups = try await getNewGroups(
2644
client: request.client,
@@ -35,11 +53,6 @@ final class GroupsController {
3553
)
3654
}
3755

38-
func allGroups(request: Request) async throws -> GroupsResponse {
39-
let groupModels = try await GroupModel.query(on: request.db).all()
40-
return GroupsResponse(numberOfGroups: groupModels.count, groups: groupModels)
41-
}
42-
4356
// MARK: - Other methods
4457

4558
func getNewGroups(client: Client, logger: Logger) async throws -> [GroupModel] {
@@ -79,12 +92,7 @@ final class GroupsController {
7992
)
8093
numberOfParsedGroups += 1
8194
logger.info("\(numberOfParsedGroups) \(response.headers)")
82-
guard
83-
var body = response.body,
84-
let html = body.readString(length: body.readableBytes)
85-
else {
86-
throw Abort(.internalServerError)
87-
}
95+
let html = try (response.body).htmlString(encoding: .utf8)
8896
return try GroupParser(groupName: groupName)
8997
.parse(html)
9098
.map { GroupModel(id: $0.id, name: $0.name) }

Sources/App/Controllers/LessonsController.swift

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,7 @@ final class LessonsController {
1414
let response = try await request.client.get(
1515
"http://rozklad.kpi.ua/Schedules/ViewSchedule.aspx?g=\(uuid.uuidString)"
1616
)
17-
guard
18-
var body = response.body,
19-
let html = body.readString(length: body.readableBytes)
20-
else {
21-
throw Abort(.internalServerError)
22-
}
17+
let html = try (response.body).htmlString(encoding: .utf8)
2318
let lessons = try LessonsParser().parse(html)
2419
return LessonsResponse(id: uuid, lessons: lessons)
2520
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
//
2+
// ByteBuffer+HTML.swift
3+
//
4+
//
5+
// Created by Denys Danyliuk on 04.06.2022.
6+
//
7+
8+
import Vapor
9+
import Foundation
10+
11+
extension ByteBuffer {
12+
13+
public func htmlString(encoding: String.Encoding = .utf8) throws -> String {
14+
let data = Data(buffer: self)
15+
guard
16+
let html = String(data: data, encoding: encoding)
17+
else {
18+
throw Abort(.internalServerError)
19+
}
20+
return html
21+
}
22+
23+
}
24+
25+
extension Optional where Wrapped == ByteBuffer {
26+
27+
public func htmlString(encoding: String.Encoding = .utf8) throws -> String {
28+
switch self {
29+
case let .some(buffer):
30+
return try buffer.htmlString(encoding: encoding)
31+
case .none:
32+
throw Abort(.internalServerError)
33+
}
34+
}
35+
36+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
//
2+
// CampusAPICredentials.swift
3+
//
4+
//
5+
// Created by Denys Danyliuk on 04.06.2022.
6+
//
7+
8+
import Foundation
9+
10+
struct CampusAPICredentials: Equatable, Codable {
11+
12+
let accessToken: String
13+
let sessionId: String
14+
15+
enum CodingKeys: String, CodingKey {
16+
case accessToken = "access_token"
17+
case sessionId
18+
}
19+
}

Sources/App/Models/ClientRequestResponse/AllGroupClientRequest.swift renamed to Sources/App/Models/ClientRequestResponse/Rozklad/AllGroupClientRequest.swift

File renamed without changes.

Sources/App/Models/ClientRequestResponse/AllGroupsClientResponse.swift renamed to Sources/App/Models/ClientRequestResponse/Rozklad/AllGroupsClientResponse.swift

File renamed without changes.

Sources/App/Models/GroupModel.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,3 +48,9 @@ extension GroupModel: AsyncMigration {
4848
}
4949

5050
}
51+
52+
// MARK: - GroupModel + Content
53+
54+
extension GroupModel: Content {
55+
56+
}

Sources/App/Models/UserInfo.swift

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
//
2+
// UserInfo.swift
3+
//
4+
//
5+
// Created by Denys Danyliuk on 04.06.2022.
6+
//
7+
8+
import Foundation
9+
import Vapor
10+
11+
struct UserInfo: Codable, Equatable {
12+
13+
// MARK: - StudyGroup
14+
15+
struct InfoItem: Codable, Equatable {
16+
let id: Int
17+
let name: String
18+
}
19+
20+
// MARK: - Profile
21+
22+
struct Profile: Codable, Equatable {
23+
let id: Int
24+
let profile: String
25+
let subdivision: InfoItem
26+
}
27+
28+
let modules: [String]
29+
let position: [InfoItem]
30+
let subdivision: [InfoItem]
31+
let studyGroup: InfoItem
32+
let sid: String
33+
let email: String
34+
let scientificInterest: String
35+
let username: String
36+
let tgAuthLinked: Bool
37+
let profiles: [Profile]
38+
let id: Int
39+
let userIdentifier: String
40+
let fullName: String
41+
let photo: String
42+
let credo: String
43+
}
44+
45+
// MARK: UserInfo + Content
46+
47+
extension UserInfo: Content {
48+
49+
}

Sources/App/configure.swift

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,10 @@ public func configure(_ app: Application) throws {
3535

3636
app.mount(rootRouter, use: rootHandler)
3737

38+
// MARK: - Client configuration
39+
40+
app.http.client.configuration.redirectConfiguration = .disallow
41+
3842
// MARK: - Cron
3943

4044
try app.cron.schedule(RefreshGroupsCron.self)

0 commit comments

Comments
 (0)