|
"source": "import Foundation\nimport KreeRequest\n\npublic struct DevRantRequest {\n let request: KreeRequest\n let backend = DevRantBackend()\n \n public init(requestLogger: Logger) {\n self.request = KreeRequest(encoder: .devRant, decoder: .devRant, logger: requestLogger)\n }\n \n private func makeConfig(_ method: KreeRequest.Method, path: String, urlParameters: [String: String] = [:], headers: [String: String] = [:], token: AuthToken? = nil) -> KreeRequest.Config {\n var urlParameters = urlParameters\n urlParameters[\"app\"] = \"3\"\n \n if let token {\n urlParameters[\"token_id\"] = String(token.id)\n urlParameters[\"token_key\"] = token.key\n urlParameters[\"user_id\"] = String(token.userId)\n }\n \n var headers = headers\n headers[\"Content-Type\"] = \"application/x-www-form-urlencoded\"\n \n return .init(method: method, backend: backend, path: path, urlParameters: urlParameters, headers: headers)\n }\n \n private func makeMultipartConfig(_ method: KreeRequest.Method, path: String, parameters: [String: String] = [:], boundary: String, headers: [String: String] = [:], token: AuthToken? = nil) -> KreeRequest.Config {\n var parameters = parameters\n parameters[\"app\"] = \"3\"\n \n if let token {\n parameters[\"token_id\"] = String(token.id)\n parameters[\"token_key\"] = token.key\n parameters[\"user_id\"] = String(token.userId)\n }\n \n var headers = headers\n headers[\"Content-Type\"] = \"multipart/form-data; boundary=\\(boundary)\"\n \n return .init(method: method, backend: backend, path: path, urlParameters: parameters, headers: headers)\n }\n \n private func multipartBody(parameters: [String: String], boundary: String, imageData: Data?) -> Data {\n var body = Data()\n \n let boundaryPrefix = \"--\\(boundary)\\r\\n\"\n \n for (key, value) in parameters {\n body.appendString(boundaryPrefix)\n body.appendString(\"Content-Disposition: form-data; name=\\\"\\(key)\\\"\\r\\n\\r\\n\")\n body.appendString(\"\\(value)\\r\\n\")\n }\n \n if let imageData {\n //TODO: the image is not always jpeg. not sure if it matters here.\n body.appendString(boundaryPrefix)\n body.appendString(\"Content-Disposition: form-data; name=\\\"image\\\"; filename=\\\"image.jpeg\\\"\\r\\n\")\n body.appendString(\"Content-Type: image/jpeg\\r\\n\\r\\n\")\n body.append(imageData)\n body.appendString(\"\\r\\n\")\n }\n \n body.appendString(\"--\".appending(boundary.appending(\"--\")))\n \n return body\n }\n \n /// For endpoints with the POST method in the devRant API the url parameters need to be passed as a string in the http body rather than in the URL.\n /// The url encoding works but it might also work with other encodings like json or multipart form data.\n private func stringBody(fromUrlParameters urlParameters: [String: String]) -> String {\n String(KreeRequest.urlEncodedQueryString(from: urlParameters).dropFirst()) // dropping the first character \"?\"\n }\n}\n\npublic extension DevRantRequest {\n func logIn(username: String, password: String) async throws -> AuthToken {\n var parameters: [String: String] = [:]\n parameters[\"app\"] = \"3\"\n parameters[\"username\"] = username\n parameters[\"password\"] = password\n \n let config = makeConfig(.post, path: \"users/auth-token\")\n \n let body = stringBody(fromUrlParameters: parameters)\n \n let response: AuthToken.CodingData.Container = try await request.requestJson(config: config, string: body, apiError: DevRantApiError.CodingData.self)\n \n return response.auth_token.decoded\n }\n \n /// Gets a personalized feed of rants.\n ///\n /// - Parameters:\n /// - token: The token from the `logIn` call response.\n /// - limit: The number of rants to get for pagination.\n /// - skip: How many rants to skip for pagination.\n /// - sessionHash: Pass the session hash value from the last rant feed response or `nil` if calling for the first time.\n func getRantFeed(token: AuthToken, sort: RantFeed.Sort = .algorithm, limit: Int = 20, skip: Int, sessionHash: String?) async throws -> RantFeed {\n var parameters: [String: String] = [:]\n \n parameters[\"sort\"] = switch sort {\n case .algorithm: \"algo\"\n case .recent: \"recent\"\n case .top: \"top\"\n }\n \n switch sort {\n case .top(range: let range):\n parameters[\"range\"] = switch range {\n case .day: \"day\"\n case .week: \"week\"\n case .month: \"month\"\n case .all: \"all\"\n }\n default:\n break\n }\n \n parameters[\"limit\"] = String(limit)\n parameters[\"skip\"] = String(skip)\n parameters[\"prev_set\"] = sessionHash\n \n parameters[\"plat\"] = \"1\" // I don't know wtf that is.\n parameters[\"nari\"] = \"1\" // I don't know wtf that is.\n \n let config = makeConfig(.get, path: \"devrant/rants\", urlParameters: parameters, token: token)\n \n let response: RantFeed.CodingData = try await request.requestJson(config: config, apiError: DevRantApiError.CodingData.self)\n \n return response.decoded\n }\n \n /// Gets all weeklies as a list.\n ///\n /// - Parameters:\n /// - token: The token from the `logIn` call response.\n func getWeeklies(token: AuthToken) async throws -> [Weekly] {\n let config = makeConfig(.get, path: \"devrant/weekly-list\", token: token)\n \n let response: Weekly.CodingData.List = try await request.requestJson(config: config, apiError: DevRantApiError.CodingData.self)\n \n return response.weeks.map(\\.decoded)\n }\n \n /// Gets a specific week's weekly rants.\n ///\n /// - Parameters:\n /// - token: The token from the `logIn` call response.\n /// - week: The number of the week. Pass `nil` to get the latest week's rants.\n /// - limit: The number of rants for pagination.\n /// - skip: How many rants to skip for pagination.\n func getWeeklyRants(token: AuthToken, week: Int?, limit: Int = 20, skip: Int) async throws -> RantFeed {\n var parameters: [String: String] = [:]\n \n parameters[\"week\"] = week.flatMap { String($0) }\n parameters[\"limit\"] = String(limit)\n parameters[\"skip\"] = String(skip)\n \n let config = makeConfig(.get, path: \"devrant/weekly-rants\", urlParameters: parameters, token: token)\n \n let response: RantFeed.CodingData = try await request.requestJson(config: config, apiError: DevRantApiError.CodingData.self)\n \n return response.decoded\n }\n \n /// Gets the list of notifications and numbers for each notification type.\n ///\n /// - Parameters:\n /// - token: The token from the `logIn` call response.\n /// - lastChecked: Pass the value from the last response or `nil`.\n func getNotificationFeed(token: AuthToken, lastChecked: Date?, category: NotificationFeed.Category) async throws -> NotificationFeed {\n var parameters: [String: String] = [:]\n \n parameters[\"last_time\"] = lastChecked.flatMap { String(Int($0.timeIntervalSince1970)) } ?? \"0\"\n parameters[\"ext_prof\"] = \"1\" // I don't know wtf that is.\n \n let config = makeConfig(.get, path: \"users/me/notif-feed\\(category.rawValue)\", urlParameters: parameters, token: token)\n \n let response: NotificationFeed.CodingData.Container = try await request.requestJson(config: config, apiError: DevRantApiError.CodingData.self)\n \n return response.data.decoded\n }\n \n /// Gets a single rant and its comments.\n ///\n /// - Parameters:\n /// - token: The token from the `logIn` call response.\n /// - rantId: The id of the rant.\n /// - lastCommentId: Only fetch the comments which were posted after the one corresponding to this id. Pass `nil` to get all comments.\n func getRant(token: AuthToken, rantId: Int, lastCommentId: Int? = nil) async throws -> (rant: Rant, comments: [Comment]) {\n var parameters: [String: String] = [:]\n\n parameters[\"last_comment_id\"] = lastCommentId.flatMap { String($0) }\n \n let config = makeConfig(.get, path: \"devrant/rants/\\(rantId)\", urlParameters: parameters, token: token)\n \n struct Response: Codable {\n let rant: Rant.CodingData\n let comments: [Comment.CodingData]?\n }\n \n let response: Response = try await request.requestJson(config: config, apiError: DevRantApiError.CodingData.self)\n \n return (rant: response.rant.decoded, comments: response.comments?.map(\\.decoded) ?? [])\n }\n \n /// Gets a single comment.\n ///\n /// - Parameters:\n /// - token: The token from the `logIn` call response.\n /// - commentId: The id of the comment.\n func getComment(token: AuthToken, commentId: Int) async throws -> Comment {\n let config = makeConfig(.get, path: \"comments/\\(commentId)\", token: token)\n \n let response: Comment.CodingData = try await request.requestJson(config: config, apiError: DevRantApiError.CodingData.self)\n \n return response.decoded\n }\n \n /// Gets the id of a user.\n ///\n /// - Parameters:\n /// - username: The username of the user.\n func getUserId(username: String) async throws -> Int {\n var parameters: [String: String] = [:]\n\n parameters[\"username\"] = username\n \n let config = makeConfig(.get, path: \"get-user-id\", urlParameters: parameters)\n \n struct Response: Decodable {\n let user_id: Int\n }\n \n let response: Response = try await request.requestJson(config: config, apiError: DevRantApiError.CodingData.self)\n \n return response.user_id\n }\n \n /// Gets a user's profile data.\n ///\n /// - Parameters:\n /// - token: The token from the `logIn` call response.\n /// - userId: The id of the user.\n /// - contentType: The type of content created by the user to be fetched.\n /// - skip: The number of content items to skip for pagination.\n func getProfile(token: AuthToken, userId: Int, contentType: Profile.ContentType, skip: Int) async throws -> Profile {\n var parameters: [String: String] = [:]\n\n parameters[\"skip\"] = String(skip)\n parameters[\"content\"] = contentType.rawValue\n \n let config = makeConfig(.get, path: \"users/\\(userId)\", urlParameters: parameters, token: token)\n \n let response: Profile.CodingData.Container = try await request.requestJson(config: config, apiError: DevRantApiError.CodingData.self)\n \n return response.decoded\n }\n \n /// Votes on a rant.\n ///\n /// - Parameters:\n /// - token: The token from the `logIn` call response.\n /// - rantId: The id of the rant.\n /// - vote: The vote for this rant.\n func voteOnRant(token: AuthToken, rantId: Int, vote: VoteState, downvoteReason: DownvoteReason = .notForMe) async throws -> Rant {\n let config = makeConfig(.post, path: \"devrant/rants/\\(rantId)/vote\", token: token)\n \n var parameters = config.urlParameters\n\n parameters[\"vote\"] = String(vote.rawValue)\n \n if vote == .downvoted {\n parameters[\"reason\"] = String(downvoteReason.rawValue)\n }\n \n let body = stringBody(fromUrlParameters: parameters)\n \n struct Response: Codable {\n let rant: Rant.CodingData\n }\n \n let response: Response = try await request.requestJson(config: config, string: body, apiError: DevRantApiError.CodingData.self)\n \n return response.rant.decoded\n }\n \n /// Votes on a comment.\n ///\n /// - Parameters:\n /// - token: The token from the `logIn` call response.\n /// - commentId: The id of the comment.\n /// - vote: The vote for this comment.\n func voteOnComment(token: AuthToken, commentId: Int, vote: VoteState, downvoteReason: DownvoteReason = .notForMe) async throws -> Comment {\n let config = makeConfig(.post, path: \"comments/\\(commentId)/vote\", token: token)\n \n var parameters = config.urlParameters\n\n parameters[\"vote\"] = String(vote.rawValue)\n \n if vote == .downvoted {\n parameters[\"reason\"] = String(downvoteReason.rawValue)\n }\n \n let body = stringBody(fromUrlParameters: parameters)\n \n struct Response: Decodable {\n let comment: Comment.CodingData\n }\n \n let response: Response = try await request.requestJson(config: config, string: body, apiError: DevRantApiError.CodingData.self)\n \n return response.comment.decoded\n }\n \n /// Updates the user profile.\n ///\n /// - Parameters:\n /// - token: The token from the `logIn` call response.\n /// - about: The user's about text.\n /// - skills: The user's list of skills.\n /// - github: The user's GitHub link.\n /// - location: The user's geographic location.\n /// - website: The user's personal website.\n func editUserProfile(token: AuthToken, about: String, skills: String, github: String, location: String, website: String) async throws {\n let config = makeConfig(.post, path: \"users/me/edit-profile\", token: token)\n \n var parameters = config.urlParameters\n\n parameters[\"profile_about\"] = about\n parameters[\"profile_skills\"] = skills\n parameters[\"profile_github\"] = github\n parameters[\"profile_location\"] = location\n parameters[\"profile_website\"] = website\n \n let body = stringBody(fromUrlParameters: parameters)\n \n try await request.requestJson(config: config, string: body, apiError: DevRantApiError.CodingData.self)\n }\n \n /// Creates and posts a rant.\n ///\n /// - Parameters:\n /// - token: The token from the `logIn` call response.\n /// - kind: The type of rant.\n /// - text: The text content of the rant.\n /// - tags: The rant's associated tags.\n /// - image: An image to attach to the rant.\n /// - imageConversion: The image conversion methods for unsupported image formats.\n /// - Returns:\n /// The id of the posted rant.\n func postRant(token: AuthToken, kind: Rant.Kind, text: String, tags: String, image: Data?, imageConversion: [ImageDataConverter] = [.unsupportedToJpeg]) async throws -> Int {\n let boundary = UUID().uuidString\n \n let config = makeMultipartConfig(.post, path: \"devrant/rants\", boundary: boundary, token: token)\n \n var parameters = config.urlParameters\n\n parameters[\"rant\"] = text\n parameters[\"tags\"] = tags\n parameters[\"type\"] = String(kind.rawValue)\n \n let convertedImage = image.flatMap { imageConversion.convert($0) }\n \n let bodyData = multipartBody(parameters: parameters, boundary: boundary, imageData: convertedImage)\n \n struct Response: Decodable {\n let rant_id: Int\n }\n \n let response: Response = try await request.requestJson(config: config, data: bodyData, apiError: DevRantApiError.CodingData.self)\n \n return response.rant_id\n }\n \n /// Deletes a rant.\n ///\n /// - Parameters:\n /// - token: The token from the `logIn` call response.\n /// - rantId: The id of the rant.\n func deleteRant(token: AuthToken, rantId: Int) async throws {\n let config = makeConfig(.delete, path: \"devrant/rants/\\(rantId)\", token: token)\n \n try await request.requestJson(config: config, apiError: DevRantApiError.CodingData.self)\n }\n \n /// Sets or unsets a rant as a favorite.\n ///\n /// - Parameters:\n /// - token: The token from the `logIn` call response.\n /// - rantId: The id of the rant.\n /// - favorite: `true` sets the rant as favorite and `false` sets it as not favorite.\n func favoriteRant(token: AuthToken, rantId: Int, favorite: Bool) async throws {\n let favoritePath = favorite ? \"favorite\" : \"unfavorite\"\n \n let config = makeConfig(.post, path: \"devrant/rants/\\(rantId)/\\(favoritePath)\", token: token)\n \n let parameters = config.urlParameters\n \n let body = stringBody(fromUrlParameters: parameters)\n \n try await request.requestJson(config: config, string: body, apiError: DevRantApiError.CodingData.self)\n }\n \n /// Edits a posted rant.\n ///\n /// - Parameters:\n /// - token: The token from the `logIn` call response.\n /// - rantId: The id of the rant.\n /// - text: The text content of the rant.\n /// - tags: The rants's associated tags.\n /// - image: An image to attach to the rant.\n func editRant(token: AuthToken, rantId: Int, text: String, tags: String, image: Data?, imageConversion: [ImageDataConverter] = [.unsupportedToJpeg]) async throws {\n let boundary = UUID().uuidString\n \n let config = makeMultipartConfig(.post, path: \"devrant/rants/\\(rantId)\", boundary: boundary, token: token)\n \n var parameters = config.urlParameters\n\n parameters[\"rant\"] = text\n parameters[\"tags\"] = tags\n \n let convertedImage = image.flatMap { imageConversion.convert($0) }\n \n let bodyData = multipartBody(parameters: parameters, boundary: boundary, imageData: convertedImage)\n \n try await request.requestJson(config: config, data: bodyData, apiError: DevRantApiError.CodingData.self)\n }\n \n /// Creates and posts a comment for a specific rant.\n ///\n /// - Parameters:\n /// - token: The token from the `logIn` call response.\n /// - rantId: The id of the rant that this comment should be posted for.\n /// - text: The text content of the comment.\n /// - image: An image to attach to the comment.\n func postComment(token: AuthToken, rantId: Int, text: String, image: Data?, imageConversion: [ImageDataConverter] = [.unsupportedToJpeg]) async throws {\n let boundary = UUID().uuidString\n \n let config = makeMultipartConfig(.post, path: \"devrant/rants/\\(rantId)/comments\", boundary: boundary, token: token)\n \n var parameters = config.urlParameters\n\n parameters[\"comment\"] = text\n \n let convertedImage = image.flatMap { imageConversion.convert($0) }\n \n let bodyData = multipartBody(parameters: parameters, boundary: boundary, imageData: convertedImage)\n \n try await request.requestJson(config: config, data: bodyData, apiError: DevRantApiError.CodingData.self)\n }\n \n /// Edits a posted comment.\n ///\n /// - Parameters:\n /// - token: The token from the `logIn` call response.\n /// - commentId: The id of the comment.\n /// - text: The text content of the comment.\n /// - image: An image to attach to the comment.\n func editComment(token: AuthToken, commentId: Int, text: String, image: Data?, imageConversion: [ImageDataConverter] = [.unsupportedToJpeg]) async throws {\n let boundary = UUID().uuidString\n \n let config = makeMultipartConfig(.post, path: \"comments/\\(commentId)\", boundary: boundary, token: token)\n \n var parameters = config.urlParameters\n\n parameters[\"comment\"] = text\n \n let convertedImage = image.flatMap { imageConversion.convert($0) }\n \n let bodyData = multipartBody(parameters: parameters, boundary: boundary, imageData: convertedImage)\n \n try await request.requestJson(config: config, data: bodyData, apiError: DevRantApiError.CodingData.self)\n }\n \n /// Deletes a comment.\n ///\n /// - Parameters:\n /// - token: The token from the `logIn` call response.\n /// - commentId: The id of the comment.\n func deleteComment(token: AuthToken, commentId: Int) async throws {\n let config = makeConfig(.delete, path: \"comments/\\(commentId)\", token: token)\n \n try await request.requestJson(config: config, apiError: DevRantApiError.CodingData.self)\n }\n \n /// Marks all notifications as read.\n ///\n /// - Parameters:\n /// - token: The token from the `logIn` call response.\n func markAllNotificationsAsRead(token: AuthToken) async throws {\n let config = makeConfig(.delete, path: \"users/me/notif-feed\", token: token)\n \n try await request.requestJson(config: config, apiError: DevRantApiError.CodingData.self)\n }\n \n /// Subscribes to a user.\n ///\n /// - Parameters:\n /// - token: The token from the `logIn` call response.\n /// - userId: The id of the user to subscribe to.\n func subscribeToUser(token: AuthToken, userId: Int) async throws {\n let config = makeConfig(.post, path: \"users/\\(userId)/subscribe\", token: token)\n \n let parameters = config.urlParameters\n \n let body = stringBody(fromUrlParameters: parameters)\n \n try await request.requestJson(config: config, string: body, apiError: DevRantApiError.CodingData.self)\n }\n \n /// Unsubscribes from a user.\n ///\n /// - Parameters:\n /// - token: The token from the `logIn` call response.\n /// - userId: The id of the user to unsubscribe from.\n func unsubscribeFromUser(token: AuthToken, userId: Int) async throws {\n let config = makeConfig(.delete, path: \"users/\\(userId)/subscribe\", token: token)\n \n try await request.requestJson(config: config, apiError: DevRantApiError.CodingData.self)\n }\n}\n\nprivate extension Data {\n mutating func appendString(_ string: String) {\n let data = string.data(using: .utf8, allowLossyConversion: false)\n append(data!)\n }\n}\n",
|