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