e178c3748a
* Added passing access token as a Bearer token. * Removed superfluous code
247 lines
9.8 KiB
Swift
Executable File
247 lines
9.8 KiB
Swift
Executable File
//
|
|
// NetworkInterface.swift
|
|
//
|
|
// Copyright © 2017 Peter Zignego. All rights reserved.
|
|
//
|
|
// Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
// of this software and associated documentation files (the "Software"), to deal
|
|
// in the Software without restriction, including without limitation the rights
|
|
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
// copies of the Software, and to permit persons to whom the Software is
|
|
// furnished to do so, subject to the following conditions:
|
|
//
|
|
// The above copyright notice and this permission notice shall be included in
|
|
// all copies or substantial portions of the Software.
|
|
//
|
|
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
// THE SOFTWARE.
|
|
|
|
#if os(Linux)
|
|
import Dispatch
|
|
#endif
|
|
#if canImport(FoundationNetworking)
|
|
import FoundationNetworking
|
|
#endif
|
|
import Foundation
|
|
|
|
#if !COCOAPODS
|
|
import SKCore
|
|
#endif
|
|
|
|
|
|
public struct NetworkInterface {
|
|
|
|
private let apiUrl = "https://slack.com/api/"
|
|
#if canImport(FoundationNetworking)
|
|
private let session = FoundationNetworking.URLSession(configuration: .default)
|
|
#else
|
|
private let session = URLSession(configuration: .default)
|
|
#endif
|
|
|
|
internal init() {}
|
|
|
|
internal func request(
|
|
_ endpoint: Endpoint,
|
|
accessToken: String,
|
|
parameters: [String: Any?],
|
|
successClosure: @escaping ([String: Any]) -> Void,
|
|
errorClosure: @escaping (SlackError) -> Void
|
|
) {
|
|
guard !accessToken.isEmpty else {
|
|
errorClosure(.invalidAuth)
|
|
return
|
|
}
|
|
|
|
guard let url = requestURL(for: endpoint, parameters: parameters) else {
|
|
errorClosure(SlackError.clientNetworkError)
|
|
return
|
|
}
|
|
|
|
var request = URLRequest(url: url)
|
|
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
|
|
|
|
session.dataTask(with: request) {(data, response, publicError) in
|
|
do {
|
|
successClosure(try NetworkInterface.handleResponse(data, response: response, publicError: publicError))
|
|
} catch let error {
|
|
errorClosure(error as? SlackError ?? SlackError.unknownError)
|
|
}
|
|
}.resume()
|
|
}
|
|
|
|
//Adapted from https://gist.github.com/erica/baa8a187a5b4796dab27
|
|
internal func synchronusRequest(_ endpoint: Endpoint, parameters: [String: Any?]) -> [String: Any]? {
|
|
guard let url = requestURL(for: endpoint, parameters: parameters) else {
|
|
return nil
|
|
}
|
|
let request = URLRequest(url: url)
|
|
var data: Data? = nil
|
|
var response: URLResponse? = nil
|
|
var error: Error? = nil
|
|
let semaphore = DispatchSemaphore(value: 0)
|
|
session.dataTask(with: request) { (reqData, reqResponse, reqError) in
|
|
data = reqData
|
|
response = reqResponse
|
|
error = reqError
|
|
if data == nil, let error = error { print(error) }
|
|
semaphore.signal()
|
|
}.resume()
|
|
_ = semaphore.wait(timeout: DispatchTime.distantFuture)
|
|
return try? NetworkInterface.handleResponse(data, response: response, publicError: error)
|
|
}
|
|
|
|
internal func customRequest(
|
|
_ url: String,
|
|
token: String,
|
|
data: Data,
|
|
success: @escaping (Bool) -> Void,
|
|
errorClosure: @escaping (SlackError) -> Void
|
|
) {
|
|
guard let string = url.removingPercentEncoding, let url = URL(string: string) else {
|
|
errorClosure(SlackError.clientNetworkError)
|
|
return
|
|
}
|
|
var request = URLRequest(url:url)
|
|
request.httpMethod = "POST"
|
|
let contentType = "application/json; charset: utf-8"
|
|
request.setValue(contentType, forHTTPHeaderField: "Content-Type")
|
|
request.setValue("Bearer \(token)", forHTTPHeaderField: "Authorization")
|
|
request.httpBody = data
|
|
|
|
session.dataTask(with: request) {(data, response, publicError) in
|
|
if publicError == nil {
|
|
success(true)
|
|
} else {
|
|
errorClosure(SlackError.clientNetworkError)
|
|
}
|
|
}.resume()
|
|
}
|
|
|
|
internal func uploadRequest(
|
|
data: Data,
|
|
accessToken: String,
|
|
parameters: [String: Any?],
|
|
successClosure: @escaping ([String: Any]) -> Void, errorClosure: @escaping (SlackError) -> Void
|
|
) {
|
|
guard
|
|
let url = requestURL(for: .filesUpload, parameters: parameters),
|
|
let filename = parameters["filename"] as? String,
|
|
let filetype = parameters["filetype"] as? String
|
|
else {
|
|
errorClosure(SlackError.clientNetworkError)
|
|
return
|
|
}
|
|
var request = URLRequest(url:url)
|
|
request.httpMethod = "POST"
|
|
let boundaryConstant = randomBoundary()
|
|
let contentType = "multipart/form-data; boundary=" + boundaryConstant
|
|
request.setValue(contentType, forHTTPHeaderField: "Content-Type")
|
|
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
|
|
request.httpBody = requestBodyData(data: data, boundaryConstant: boundaryConstant, filename: filename, filetype: filetype)
|
|
|
|
session.dataTask(with: request) {(data, response, publicError) in
|
|
do {
|
|
successClosure(try NetworkInterface.handleResponse(data, response: response, publicError: publicError))
|
|
} catch let error {
|
|
errorClosure(error as? SlackError ?? SlackError.unknownError)
|
|
}
|
|
}.resume()
|
|
}
|
|
|
|
internal static func handleResponse(_ data: Data?, response: URLResponse?, publicError: Error?) throws -> [String: Any] {
|
|
guard let data = data, let response = response as? HTTPURLResponse else {
|
|
throw SlackError.clientNetworkError
|
|
}
|
|
do {
|
|
guard let json = try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] else {
|
|
throw SlackError.clientJSONError
|
|
}
|
|
|
|
switch response.statusCode {
|
|
case 200:
|
|
if json["ok"] as? Bool == true {
|
|
return json
|
|
} else {
|
|
if let errorString = json["error"] as? String {
|
|
throw SlackError(rawValue: errorString) ?? .unknownError
|
|
} else {
|
|
throw SlackError.unknownError
|
|
}
|
|
}
|
|
case 429:
|
|
throw SlackError.tooManyRequests
|
|
default:
|
|
throw SlackError.clientNetworkError
|
|
}
|
|
} catch let error {
|
|
if let slackError = error as? SlackError {
|
|
throw slackError
|
|
} else {
|
|
throw SlackError.unknownError
|
|
}
|
|
}
|
|
}
|
|
|
|
private func requestURL(for endpoint: Endpoint, parameters: [String: Any?]) -> URL? {
|
|
var components = URLComponents(string: "\(apiUrl)\(endpoint.rawValue)")
|
|
if parameters.count > 0 {
|
|
components?.queryItems = parameters.compactMapValues({$0}).map { URLQueryItem(name: $0.0, value: "\($0.1)") }
|
|
}
|
|
|
|
// As discussed http://www.openradar.me/24076063 and https://stackoverflow.com/a/37314144/407523, Apple considers
|
|
// a + and ? as valid characters in a URL query string, but Slack has recently started enforcing that they be
|
|
// encoded when included in a query string. As a result, we need to manually apply the encoding after Apple's
|
|
// default encoding is applied.
|
|
var encodedQuery = components?.percentEncodedQuery
|
|
encodedQuery = encodedQuery?.replacingOccurrences(of: ">", with: "%3E")
|
|
encodedQuery = encodedQuery?.replacingOccurrences(of: "<", with: "%3C")
|
|
encodedQuery = encodedQuery?.replacingOccurrences(of: "@", with: "%40")
|
|
|
|
encodedQuery = encodedQuery?.replacingOccurrences(of: "?", with: "%3F")
|
|
encodedQuery = encodedQuery?.replacingOccurrences(of: "+", with: "%2B")
|
|
components?.percentEncodedQuery = encodedQuery
|
|
|
|
return components?.url
|
|
}
|
|
|
|
private func requestBodyData(data: Data, boundaryConstant: String, filename: String, filetype: String) -> Data? {
|
|
let boundaryStart = "--\(boundaryConstant)\r\n"
|
|
let boundaryEnd = "--\(boundaryConstant)--\r\n"
|
|
let contentDispositionString = "Content-Disposition: form-data; name=\"file\"; filename=\"\(filename)\"\r\n"
|
|
let contentTypeString = "Content-Type: \(filetype)\r\n\r\n"
|
|
let dataEnd = "\r\n"
|
|
|
|
guard
|
|
let boundaryStartData = boundaryStart.data(using: .utf8),
|
|
let dispositionData = contentDispositionString.data(using: .utf8),
|
|
let contentTypeData = contentTypeString.data(using: .utf8),
|
|
let boundaryEndData = boundaryEnd.data(using: .utf8),
|
|
let dataEndData = dataEnd.data(using: .utf8)
|
|
else {
|
|
return nil
|
|
}
|
|
|
|
var requestBodyData = Data()
|
|
requestBodyData.append(contentsOf: boundaryStartData)
|
|
requestBodyData.append(contentsOf: dispositionData)
|
|
requestBodyData.append(contentsOf: contentTypeData)
|
|
requestBodyData.append(contentsOf: data)
|
|
requestBodyData.append(contentsOf: dataEndData)
|
|
requestBodyData.append(contentsOf: boundaryEndData)
|
|
return requestBodyData
|
|
}
|
|
|
|
private func randomBoundary() -> String {
|
|
#if os(Linux)
|
|
return "slackkit.boundary.\(Int(random()))\(Int(random()))"
|
|
#else
|
|
return "slackkit.boundary.\(arc4random())\(arc4random())"
|
|
#endif
|
|
}
|
|
}
|