diff --git a/README.md b/README.md index 5e40fd1..08b2d2c 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,10 @@ ![SlackKit](https://cloud.githubusercontent.com/assets/8311605/10260893/5ec60f96-694e-11e5-91fd-da6845942201.png) ##iOS/OS X Slack Client Library ###Description -This is a Slack client library for iOS and OS X written in Swift. It's intended to expose all of the functionality of Slack's [Real Time Messaging API](https://api.slack.com/rtm). +This is a Slack client library for iOS and OS X written in Swift. It's intended to expose all of the functionality of Slack's [Real Time Messaging API](https://api.slack.com/rtm) as well as the [web APIs](https://api.slack.com/web) that are accessible by [bot users](https://api.slack.com/bot-users). ###Installation -####Swift Package Manager (Swift 2.2 and up) +####Swift Package Manager Add SlackKit to your Package.swift ```swift @@ -37,17 +37,88 @@ import SlackKit ###Usage To use SlackKit you'll need a bearer token which identifies a single user. You can generate a [full access token or create one using OAuth 2](https://api.slack.com/web). -Once you have a token, give it to the Client: +Once you have a token, initialize a client instance using it: ```swift -Client.sharedInstance.setAuthToken("YOUR_SLACK_AUTH_TOKEN") +let client = Client(apiToken: "YOUR_SLACK_API_TOKEN") + ``` -and connect: + +If you want to receive messages from the Slack RTM API, connect to it. ```swift -Client.sharedInstance.connect() +client.connect() ``` + Once connected, the client will begin to consume any messages sent by the Slack RTM API. +####Web API Methods +SlackKit currently supports the a subset of the Slack Web APIs that are available to bot users: + +- api.test +- auth.test +- channels.history +- channels.info +- channels.list +- channels.mark +- channels.setPurpose +- channels.setTopic +- chat.delete +- chat.postMessage +- chat.update +- emoji.list +- files.delete +- files.upload +- groups.close +- groups.history +- groups.info +- groups.list +- groups.mark +- groups.open +- groups.setPurpose +- groups.setTopic +- im.close +- im.history +- im.list +- im.mark +- im.open +- mpim.close +- mpim.history +- mpim.list +- mpim.mark +- mpim.open +- pins.add +- pins.list +- pins.remove +- reactions.add +- reactions.get +- reactions.list +- reactions.remove +- rtm.start +- stars.add +- stars.remove +- team.info +- users.getPresence +- users.info +- users.list +- users.setActive +- users.setPresence + +They can be accessed through a Client object’s `webAPI` property: +```swift +client.webAPI.authenticationTest({ +(authenticated) -> Void in + print(authenticated) + }){(error) -> Void in + print(error) +} +``` + ####Delegate methods + +To receive delegate callbacks for certain events, register an object as the delegate for those events: +```swift +client.slackEventsDelegate = self +``` + There are a number of delegates that you can set to receive callbacks for certain events. #####SlackEventsDelegate @@ -138,18 +209,6 @@ func subteamSelfAdded(subteamID: String) func subteamSelfRemoved(subteamID: String) ``` -###Examples -####Sending a Message: -```swift -Client.sharedInstance.sendMessage(message: "Hello, world!", channelID: "CHANNEL_ID") -``` - -####Print a List of Users in a Channel: -```swift -let users = Client.sharedInstance.channels?["CHANNEL_ID"]?.members -print(users) -``` - ###Get In Touch [@pvzig](https://twitter.com/pvzig) diff --git a/SlackKit.podspec b/SlackKit.podspec index 9cb4c41..f0d7894 100644 --- a/SlackKit.podspec +++ b/SlackKit.podspec @@ -1,6 +1,6 @@ Pod::Spec.new do |s| s.name = "SlackKit" - s.version = "0.9.7" + s.version = "0.9.6" s.summary = "a Slack client library for iOS and OS X written in Swift" s.homepage = "https://github.com/pvzig/SlackKit" s.license = 'MIT' diff --git a/SlackKit.xcodeproj/project.pbxproj b/SlackKit.xcodeproj/project.pbxproj index 7c31cd0..e6f559c 100644 --- a/SlackKit.xcodeproj/project.pbxproj +++ b/SlackKit.xcodeproj/project.pbxproj @@ -7,6 +7,9 @@ objects = { /* Begin PBXBuildFile section */ + 260EC2331C4DC61D0093B253 /* ClientExtensions.swift in Sources */ = {isa = PBXBuildFile; fileRef = 260EC2301C4DC61D0093B253 /* ClientExtensions.swift */; }; + 260EC2341C4DC61D0093B253 /* NetworkInterface.swift in Sources */ = {isa = PBXBuildFile; fileRef = 260EC2311C4DC61D0093B253 /* NetworkInterface.swift */; }; + 260EC2351C4DC61D0093B253 /* SlackWebAPI.swift in Sources */ = {isa = PBXBuildFile; fileRef = 260EC2321C4DC61D0093B253 /* SlackWebAPI.swift */; }; 26BBA1941C398E3C00BF7225 /* Bot.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26BBA1871C398E3C00BF7225 /* Bot.swift */; }; 26BBA1951C398E3C00BF7225 /* Channel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26BBA1881C398E3C00BF7225 /* Channel.swift */; }; 26BBA1961C398E3C00BF7225 /* Client.swift in Sources */ = {isa = PBXBuildFile; fileRef = 26BBA1891C398E3C00BF7225 /* Client.swift */; }; @@ -25,6 +28,9 @@ /* Begin PBXFileReference section */ 26072A341BB48B3A00CD650C /* SlackKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = SlackKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 260EC2301C4DC61D0093B253 /* ClientExtensions.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = ClientExtensions.swift; path = Sources/ClientExtensions.swift; sourceTree = ""; }; + 260EC2311C4DC61D0093B253 /* NetworkInterface.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = NetworkInterface.swift; path = Sources/NetworkInterface.swift; sourceTree = ""; }; + 260EC2321C4DC61D0093B253 /* SlackWebAPI.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = SlackWebAPI.swift; path = Sources/SlackWebAPI.swift; sourceTree = ""; }; 2661A6A41BBF62FF0026F67B /* SlackKit.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SlackKit.h; sourceTree = ""; }; 266E05F01BBF780C00840D76 /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 26BBA1871C398E3C00BF7225 /* Bot.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; name = Bot.swift; path = Sources/Bot.swift; sourceTree = ""; }; @@ -90,16 +96,19 @@ 26BBA1871C398E3C00BF7225 /* Bot.swift */, 26BBA1881C398E3C00BF7225 /* Channel.swift */, 26BBA1891C398E3C00BF7225 /* Client.swift */, + 260EC2301C4DC61D0093B253 /* ClientExtensions.swift */, 26BBA18A1C398E3C00BF7225 /* Event.swift */, 26BBA18B1C398E3C00BF7225 /* EventDelegate.swift */, 26BBA18C1C398E3C00BF7225 /* EventDispatcher.swift */, 26BBA18D1C398E3C00BF7225 /* EventHandler.swift */, 26BBA18E1C398E3C00BF7225 /* File.swift */, 26BBA18F1C398E3C00BF7225 /* Message.swift */, + 260EC2311C4DC61D0093B253 /* NetworkInterface.swift */, 26BBA1901C398E3C00BF7225 /* Team.swift */, 26BBA1911C398E3C00BF7225 /* Types.swift */, 26BBA1921C398E3C00BF7225 /* User.swift */, 26BBA1931C398E3C00BF7225 /* UserGroup.swift */, + 260EC2321C4DC61D0093B253 /* SlackWebAPI.swift */, 2661A6A41BBF62FF0026F67B /* SlackKit.h */, 266E05F01BBF780C00840D76 /* Info.plist */, ); @@ -236,9 +245,12 @@ 26BBA1971C398E3C00BF7225 /* Event.swift in Sources */, 26BBA1941C398E3C00BF7225 /* Bot.swift in Sources */, 26BBA19B1C398E3C00BF7225 /* File.swift in Sources */, + 260EC2351C4DC61D0093B253 /* SlackWebAPI.swift in Sources */, 26BBA19C1C398E3C00BF7225 /* Message.swift in Sources */, 26BBA19D1C398E3C00BF7225 /* Team.swift in Sources */, + 260EC2331C4DC61D0093B253 /* ClientExtensions.swift in Sources */, 26BBA1A01C398E3C00BF7225 /* UserGroup.swift in Sources */, + 260EC2341C4DC61D0093B253 /* NetworkInterface.swift in Sources */, 26BBA1981C398E3C00BF7225 /* EventDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/SlackKit/Sources/Client.swift b/SlackKit/Sources/Client.swift index fed3313..1850f4c 100644 --- a/SlackKit/Sources/Client.swift +++ b/SlackKit/Sources/Client.swift @@ -50,42 +50,38 @@ public class Client: WebSocketDelegate { public var reactionEventsDelegate: ReactionEventsDelegate? public var teamEventsDelegate: TeamEventsDelegate? public var subteamEventsDelegate: SubteamEventsDelegate? + + internal var token = "SLACK_AUTH_TOKEN" - private var token = "SLACK_AUTH_TOKEN" public func setAuthToken(token: String) { self.token = token } - private var webSocket: WebSocket? - private var dispatcher: EventDispatcher? - - required public init() { + public var webAPI: SlackWebAPI { + return SlackWebAPI(client: self) } + + internal var webSocket: WebSocket? + private var dispatcher: EventDispatcher? + + internal let api = NetworkInterface() - public static let sharedInstance = Client() + required public init(apiToken: String) { + self.token = apiToken + } - //MARK: - Connection public func connect() { dispatcher = EventDispatcher(client: self) - let request = NSURLRequest(URL: NSURL(string:"https://slack.com/api/rtm.start?token="+token)!) - NSURLConnection.sendAsynchronousRequest(request, queue: NSOperationQueue.currentQueue()!) { - (response, data, error) -> Void in - guard let data = data else { - return + webAPI.rtmStart(success: { + (response) -> Void in + self.initialSetup(response) + if let socketURL = response["url"] as? String { + let url = NSURL(string: socketURL) + self.webSocket = WebSocket(url: url!) + self.webSocket?.delegate = self + self.webSocket?.connect() } - do { - let result = try NSJSONSerialization.JSONObjectWithData(data, options: []) as! [String: AnyObject] - if (result["ok"] as! Bool == true) { - self.initialSetup(result) - let socketURL = NSURL(string: result["url"] as! String) - self.webSocket = WebSocket(url: socketURL!) - self.webSocket?.delegate = self - self.webSocket?.connect() - } - } catch _ { - print(error) - } - } + }, failure:nil) } //MARK: - Message send @@ -93,7 +89,7 @@ public class Client: WebSocketDelegate { if (connected) { if let data = formatMessageToSlackJsonString(msg: message, channel: channelID) { let string = NSString(data: data, encoding: NSUTF8StringEncoding) - self.webSocket?.writeString(string as! String) + webSocket?.writeString(string as! String) } } } @@ -103,7 +99,7 @@ public class Client: WebSocketDelegate { "id": NSDate().timeIntervalSince1970, "type": "message", "channel": message.channel, - "text": slackFormatEscaping(message.msg) + "text": message.msg.slackFormatEscaping() ] addSentMessage(json) do { @@ -124,15 +120,8 @@ public class Client: WebSocketDelegate { sentMessages[ts!.stringValue] = Message(message: message) } - private func slackFormatEscaping(string: String) -> String { - var escapedString = string.stringByReplacingOccurrencesOfString("&", withString: "&") - escapedString = escapedString.stringByReplacingOccurrencesOfString("<", withString: "<") - escapedString = escapedString.stringByReplacingOccurrencesOfString(">", withString: ">") - return escapedString - } - //MARK: - Client setup - private func initialSetup(json: [String: AnyObject]) { + internal func initialSetup(json: [String: AnyObject]) { team = Team(team: json["team"] as? [String: AnyObject]) authenticatedUser = User(user: json["self"] as? [String: AnyObject]) authenticatedUser?.doNotDisturbStatus = DoNotDisturbStatus(status: json["dnd"] as? [String: AnyObject]) @@ -145,7 +134,7 @@ public class Client: WebSocketDelegate { enumerateSubteams(json["subteams"] as? [String: AnyObject]) } - private func enumerateUsers(users: [AnyObject]?) { + internal func enumerateUsers(users: [AnyObject]?) { if let users = users { for user in users { let u = User(user: user as? [String: AnyObject]) @@ -154,7 +143,7 @@ public class Client: WebSocketDelegate { } } - private func enumerateChannels(channels: [AnyObject]?) { + internal func enumerateChannels(channels: [AnyObject]?) { if let channels = channels { for channel in channels { let c = Channel(channel: channel as? [String: AnyObject]) @@ -163,7 +152,7 @@ public class Client: WebSocketDelegate { } } - private func enumerateGroups(groups: [AnyObject]?) { + internal func enumerateGroups(groups: [AnyObject]?) { if let groups = groups { for group in groups { let g = Channel(channel: group as? [String: AnyObject]) @@ -172,7 +161,7 @@ public class Client: WebSocketDelegate { } } - private func enumerateIMs(ims: [AnyObject]?) { + internal func enumerateIMs(ims: [AnyObject]?) { if let ims = ims { for im in ims { let i = Channel(channel: im as? [String: AnyObject]) @@ -181,7 +170,7 @@ public class Client: WebSocketDelegate { } } - private func enumerateMPIMs(mpims: [AnyObject]?) { + internal func enumerateMPIMs(mpims: [AnyObject]?) { if let mpims = mpims { for mpim in mpims { let m = Channel(channel: mpim as? [String: AnyObject]) @@ -190,7 +179,7 @@ public class Client: WebSocketDelegate { } } - private func enumerateBots(bots: [AnyObject]?) { + internal func enumerateBots(bots: [AnyObject]?) { if let bots = bots { for bot in bots { let b = Bot(bot: bot as? [String: AnyObject]) @@ -199,7 +188,7 @@ public class Client: WebSocketDelegate { } } - private func enumerateSubteams(subteams: [String: AnyObject]?) { + internal func enumerateSubteams(subteams: [String: AnyObject]?) { if let subteams = subteams { if let all = subteams["all"] as? [[String: AnyObject]] { for item in all { @@ -217,8 +206,7 @@ public class Client: WebSocketDelegate { } // MARK: - WebSocketDelegate - public func websocketDidConnect(socket: WebSocket) { - } + public func websocketDidConnect(socket: WebSocket) {} public func websocketDidDisconnect(socket: WebSocket, error: NSError?) { connected = false @@ -241,7 +229,6 @@ public class Client: WebSocketDelegate { } } - public func websocketDidReceiveData(socket: WebSocket, data: NSData) { - } + public func websocketDidReceiveData(socket: WebSocket, data: NSData) {} } diff --git a/SlackKit/Sources/ClientExtensions.swift b/SlackKit/Sources/ClientExtensions.swift new file mode 100644 index 0000000..8e8cc05 --- /dev/null +++ b/SlackKit/Sources/ClientExtensions.swift @@ -0,0 +1,85 @@ +// +// ClientExtensions.swift +// +// Copyright © 2016 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. + +import Foundation + +extension Client { + + //MARK: - User & Channel + public func getChannelOrUserIdByName(name: String) -> String? { + if (name[name.startIndex] == "@") { + return getUserIdByName(name) + } else if (name[name.startIndex] == "C") { + return getChannelIDByName(name) + } + return nil + } + + public func getChannelIDByName(name: String) -> String? { + return channels.filter{$0.1.name == stripString(name)}.first?.0 + } + + public func getUserIdByName(name: String) -> String? { + return users.filter{$0.1.name == stripString(name)}.first?.0 + } + + public func getImIDForUserWithID(id: String, success: (imID: String?)->Void, failure: (error: SlackError)->Void) { + let ims = channels.filter{$0.1.isIM == true} + let channel = ims.filter{$0.1.user == id}.first + if let channel = channel { + success(imID: channel.0) + } else { + webAPI.openIM(id, success: success, failure: failure) + } + } + + //MARK: - Utilities + internal func stripString(var string: String) -> String { + if string[string.startIndex] == "@" { + string = string.substringFromIndex(string.startIndex.advancedBy(1)) + } else if string[string.startIndex] == "#" { + string = string.substringFromIndex(string.startIndex.advancedBy(1)) + } + return string + } + +} + +internal extension String { + + func slackFormatEscaping() -> String { + var escapedString = stringByReplacingOccurrencesOfString("&", withString: "&") + escapedString = stringByReplacingOccurrencesOfString("<", withString: "<") + escapedString = stringByReplacingOccurrencesOfString(">", withString: ">") + return escapedString + } + +} + +public extension NSDate { + + func slackTimestamp() -> String { + return NSNumber(double: timeIntervalSince1970).stringValue + } + +} diff --git a/SlackKit/Sources/EventDispatcher.swift b/SlackKit/Sources/EventDispatcher.swift index cb2e2b0..53b6a07 100644 --- a/SlackKit/Sources/EventDispatcher.swift +++ b/SlackKit/Sources/EventDispatcher.swift @@ -137,7 +137,7 @@ internal class EventDispatcher { // Not implemented per Slack documentation. break case .TeamMigrationStarted: - Client.sharedInstance.connect() + client.connect() case .SubteamCreated, .SubteamUpdated: handler.subteam(event) case .SubteamSelfAdded: diff --git a/SlackKit/Sources/File.swift b/SlackKit/Sources/File.swift index 221e665..eb0e482 100644 --- a/SlackKit/Sources/File.swift +++ b/SlackKit/Sources/File.swift @@ -22,7 +22,7 @@ // THE SOFTWARE. public struct File { - + public let id: String? public let created: Int? public let name: String? @@ -36,8 +36,6 @@ public struct File { public let isExternal: Bool? public let externalType: String? public let size: Int? - public let url: String? - public let urlDownload: String? public let urlPrivate: String? public let urlPrivateDownload: String? public let thumb64: String? @@ -78,8 +76,6 @@ public struct File { isExternal = file?["is_external"] as? Bool externalType = file?["external_type"] as? String size = file?["size"] as? Int - url = file?["url"] as? String - urlDownload = file?["url_download"] as? String urlPrivate = file?["url_private"] as? String urlPrivateDownload = file?["url_private_download"] as? String thumb64 = file?["thumb_64"] as? String @@ -122,8 +118,6 @@ public struct File { isExternal = nil externalType = nil size = nil - url = nil - urlDownload = nil urlPrivate = nil urlPrivateDownload = nil thumb64 = nil diff --git a/SlackKit/Sources/NetworkInterface.swift b/SlackKit/Sources/NetworkInterface.swift new file mode 100644 index 0000000..47bcba5 --- /dev/null +++ b/SlackKit/Sources/NetworkInterface.swift @@ -0,0 +1,129 @@ +// +// NetworkInterface.swift +// +// Copyright © 2016 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. + +import Foundation + +internal struct NetworkInterface { + + private let apiUrl = "https://slack.com/api/" + + internal func request(endpoint: SlackAPIEndpoint, token: String, parameters: [String: AnyObject]?, successClosure: ([String: AnyObject])->Void, errorClosure: (SlackError)->Void) { + var requestString = "\(apiUrl)\(endpoint.rawValue)?token=\(token)" + if let params = parameters { + requestString = requestString + requestStringFromParameters(params) + } + let request = NSURLRequest(URL: NSURL(string: requestString)!) + NSURLSession.sharedSession().dataTaskWithRequest(request) { + (data, response, internalError) -> Void in + guard let data = data else { + return + } + do { + let result = try NSJSONSerialization.JSONObjectWithData(data, options: []) as! [String: AnyObject] + if (result["ok"] as! Bool == true) { + successClosure(result) + } else { + if let errorString = result["error"] as? String { + throw ErrorDispatcher.dispatch(errorString) + } else { + throw SlackError.UnknownError + } + } + } catch let error { + if let slackError = error as? SlackError { + errorClosure(slackError) + } else { + errorClosure(SlackError.UnknownError) + } + } + }.resume() + } + + internal func uploadRequest(token: String, data: NSData, parameters: [String: AnyObject]?, successClosure: ([String: AnyObject])->Void, errorClosure: (SlackError)->Void) { + var requestString = "\(apiUrl)\(SlackAPIEndpoint.FilesUpload.rawValue)?token=\(token)" + if let params = parameters { + requestString = requestString + requestStringFromParameters(params) + } + + let request = NSMutableURLRequest(URL: NSURL(string: requestString)!) + request.HTTPMethod = "POST" + let boundaryConstant = randomBoundary() + let contentType = "multipart/form-data; boundary=" + boundaryConstant + let boundaryStart = "--\(boundaryConstant)\r\n" + let boundaryEnd = "--\(boundaryConstant)--\r\n" + let contentDispositionString = "Content-Disposition: form-data; name=\"file\"; filename=\"\(parameters!["filename"])\"\r\n" + let contentTypeString = "Content-Type: \(parameters!["filetype"])\r\n\r\n" + + let requestBodyData : NSMutableData = NSMutableData() + requestBodyData.appendData(boundaryStart.dataUsingEncoding(NSUTF8StringEncoding)!) + requestBodyData.appendData(contentDispositionString.dataUsingEncoding(NSUTF8StringEncoding)!) + requestBodyData.appendData(contentTypeString.dataUsingEncoding(NSUTF8StringEncoding)!) + requestBodyData.appendData(data) + requestBodyData.appendData("\r\n".dataUsingEncoding(NSUTF8StringEncoding)!) + requestBodyData.appendData(boundaryEnd.dataUsingEncoding(NSUTF8StringEncoding)!) + + request.setValue(contentType, forHTTPHeaderField: "Content-Type") + request.HTTPBody = requestBodyData + + NSURLSession.sharedSession().dataTaskWithRequest(request) { + (data, response, internalError) -> Void in + guard let data = data else { + return + } + do { + let result = try NSJSONSerialization.JSONObjectWithData(data, options: []) as! [String: AnyObject] + if (result["ok"] as! Bool == true) { + successClosure(result) + } else { + if let errorString = result["error"] as? String { + throw ErrorDispatcher.dispatch(errorString) + } else { + throw SlackError.UnknownError + } + } + } catch let error { + if let slackError = error as? SlackError { + errorClosure(slackError) + } else { + errorClosure(SlackError.UnknownError) + } + } + }.resume() + } + + private func randomBoundary() -> String { + return String(format: "slackkit.boundary.%08x%08x", arc4random(), arc4random()) + } + + private func requestStringFromParameters(parameters: [String: AnyObject]) -> String { + var requestString = "" + for key in parameters.keys { + if let value = parameters[key] as? String, encodedValue = value.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLHostAllowedCharacterSet()) { + requestString = requestString + "&"+key+"="+encodedValue + } + } + + return requestString + } + +} diff --git a/SlackKit/Sources/SlackWebAPI.swift b/SlackKit/Sources/SlackWebAPI.swift new file mode 100644 index 0000000..57816a8 --- /dev/null +++ b/SlackKit/Sources/SlackWebAPI.swift @@ -0,0 +1,636 @@ +// +// SlackWebAPI.swift +// +// Copyright © 2016 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. + +import Foundation + +internal enum SlackAPIEndpoint: String { + case APITest = "api.test" + case AuthTest = "auth.test" + case ChannelsHistory = "channels.history" + case ChannelsInfo = "channels.info" + case ChannelsList = "channels.list" + case ChannelsMark = "channels.mark" + case ChannelsSetPurpose = "channels.setPurpose" + case ChannelsSetTopic = "channels.setTopic" + case ChatDelete = "chat.delete" + case ChatPostMessage = "chat.postMessage" + case ChatUpdate = "chat.update" + case EmojiList = "emoji.list" + case FilesDelete = "files.delete" + case FilesUpload = "files.upload" + case GroupsClose = "groups.close" + case GroupsHistory = "groups.history" + case GroupsInfo = "groups.info" + case GroupsList = "groups.list" + case GroupsMark = "groups.mark" + case GroupsOpen = "groups.open" + case GroupsSetPurpose = "groups.setPurpose" + case GroupsSetTopic = "groups.setTopic" + case IMClose = "im.close" + case IMHistory = "im.history" + case IMList = "im.list" + case IMMark = "im.mark" + case IMOpen = "im.open" + case MPIMClose = "mpim.close" + case MPIMHistory = "mpim.history" + case MPIMList = "mpim.list" + case MPIMMark = "mpim.mark" + case MPIMOpen = "mpim.open" + case PinsAdd = "pins.add" + case PinsRemove = "pins.remove" + case ReactionsAdd = "reactions.add" + case ReactionsGet = "reactions.get" + case ReactionsList = "reactions.list" + case ReactionsRemove = "reactions.remove" + case RTMStart = "rtm.start" + case StarsAdd = "stars.add" + case StarsRemove = "stars.remove" + case TeamInfo = "team.info" + case UsersGetPresence = "users.getPresence" + case UsersInfo = "users.info" + case UsersList = "users.list" + case UsersSetActive = "users.setActive" + case UsersSetPresence = "users.setPresence" +} + +public class SlackWebAPI { + + public typealias FailureClosure = (error: SlackError)->Void + + public enum InfoType: String { + case Purpose = "purpose" + case Topic = "topic" + } + + public enum ParseMode: String { + case Full = "full" + case None = "none" + } + + public enum Presence: String { + case Auto = "auto" + case Away = "away" + } + + private enum ChannelType: String { + case Channel = "channel" + case Group = "group" + case IM = "im" + } + + private let client: Client + + required public init(client: Client) { + self.client = client + } + + //MARK: - RTM + public func rtmStart(success success: ((response: [String: AnyObject])->Void)?, failure: FailureClosure?) { + client.api.request(.RTMStart, token: client.token, parameters: nil, successClosure: { + (response) -> Void in + success?(response: response) + }) {(error) -> Void in + failure?(error: error) + } + } + + //MARK: - Auth Test + public func authenticationTest(success: ((authenticated: Bool)->Void)?, failure: FailureClosure?) { + client.api.request(.AuthTest, token: client.token, parameters: nil, successClosure: { + (response) -> Void in + success?(authenticated: true) + }) {(error) -> Void in + failure?(error: error) + } + } + + //MARK: - Channels + public func channelHistory(id: String, latest: String = "\(NSDate().timeIntervalSince1970)", oldest: String = "0", inclusive: Bool = false, count: Int = 100, unreads: Bool = false, success: ((history: [String: AnyObject]?)->Void)?, failure: FailureClosure?) { + history(.ChannelsHistory, id: id, latest: latest, oldest: oldest, inclusive: inclusive, count: count, unreads: unreads, success: { + (history) -> Void in + success?(history:history) + }) {(error) -> Void in + failure?(error: error) + } + } + + public func channelInfo(id: String, success: ((channel: Channel?)->Void)?, failure: FailureClosure?) { + info(.ChannelsInfo, type:ChannelType.Channel, id: id, success: { + (channel) -> Void in + success?(channel: channel) + }) { (error) -> Void in + failure?(error: error) + } + } + + public func channelsList(excludeArchived: Bool = false, success: ((channels: [[String: AnyObject]]?)->Void)?, failure: FailureClosure?) { + list(.ChannelsList, type:ChannelType.Channel, excludeArchived: excludeArchived, success: { + (channels) -> Void in + success?(channels: channels) + }) {(error) -> Void in + failure?(error: error) + } + } + + public func markChannel(channel: String, timestamp: String, success: ((ts: String)->Void)?, failure: FailureClosure?) { + mark(.ChannelsMark, channel: channel, timestamp: timestamp, success: { + (ts) -> Void in + success?(ts:timestamp) + }) {(error) -> Void in + failure?(error: error) + } + } + + public func setChannelPurpose(channel: String, purpose: String, success: ((purposeSet: Bool)->Void)?, failure: FailureClosure?) { + setInfo(.ChannelsSetPurpose, type: .Purpose, channel: channel, text: purpose, success: { + (purposeSet) -> Void in + success?(purposeSet: purposeSet) + }) { (error) -> Void in + failure?(error: error) + } + } + + public func setChannelTopic(channel: String, topic: String, success: ((topicSet: Bool)->Void)?, failure: FailureClosure?) { + setInfo(.ChannelsSetTopic, type: .Topic, channel: channel, text: topic, success: { + (topicSet) -> Void in + success?(topicSet: topicSet) + }) {(error) -> Void in + failure?(error: error) + } + } + + //MARK: - Messaging + public func deleteMessage(channel: String, ts: String, success: ((deleted: Bool)->Void)?, failure: FailureClosure?) { + let parameters: [String: AnyObject] = ["channel": channel, "ts": ts] + client.api.request(.ChatDelete, token: client.token, parameters: parameters, successClosure: { (response) -> Void in + success?(deleted: true) + }) {(error) -> Void in + failure?(error: error) + } + } + + public func sendMessage(channel: String, text: String, username: String? = nil, asUser: Bool = false, parse: ParseMode = .Full, linkNames: Bool = false, attachments: [[String: AnyObject]]? = nil, unfurlLinks: Bool = false, unfurlMedia: Bool = false, iconURL: String? = nil, iconEmoji: String? = nil, success: (((ts: String?, channel: String?))->Void)?, failure: FailureClosure?) { + let parameters: [String: AnyObject?] = ["channel":channel, "text":text.slackFormatEscaping(), "as_user":asUser, "parse":parse.rawValue, "link_names":linkNames, "unfurl_links":unfurlLinks, "unfurlMedia":unfurlMedia, "username":username, "attachments":attachments, "icon_url":iconURL, "icon_emoji":iconEmoji] + client.api.request(.ChatPostMessage, token: client.token, parameters: filterNilParameters(parameters), successClosure: { + (response) -> Void in + success?((ts: response["ts"] as? String, response["channel"] as? String)) + }) {(error) -> Void in + failure?(error: error) + } + } + + public func updateMessage(channel: String, ts: String, message: String, attachments: [[String: AnyObject]]? = nil, parse:ParseMode = .None, linkNames: Bool = false, success: ((updated: Bool)->Void)?, failure: FailureClosure?) { + let parameters: [String: AnyObject?] = ["channel": channel, "ts": ts, "text": message.slackFormatEscaping(), "parse": parse.rawValue, "link_names": linkNames, "attachments":attachments] + client.api.request(.ChatUpdate, token: client.token, parameters: filterNilParameters(parameters), successClosure: { + (response) -> Void in + success?(updated: true) + }) {(error) -> Void in + failure?(error: error) + } + } + + //MARK: - Emoji + public func emojiList(success: ((emojiList: [String: AnyObject]?)->Void)?, failure: FailureClosure?) { + client.api.request(.EmojiList, token: client.token, parameters: nil, successClosure: { + (response) -> Void in + success?(emojiList: response["emoji"] as? [String: AnyObject]) + }) { (error) -> Void in + failure?(error: error) + } + } + + //MARK: - Files + public func deleteFile(fileID: String, success: ((deleted: Bool)->Void)?, failure: FailureClosure?) { + let parameters = ["file":fileID] + client.api.request(.FilesDelete, token: client.token, parameters: parameters, successClosure: { + (response) -> Void in + success?(deleted: true) + }) {(error) -> Void in + failure?(error: error) + } + } + + public func uploadFile(file: NSData, filename: String, filetype: String = "auto", title: String? = nil, initialComment: String? = nil, channels: [String]? = nil, success: ((file: File?)->Void)?, failure: FailureClosure?) { + let parameters: [String: AnyObject?] = ["file":file, "filename": filename, "filetype":filetype, "title":title, "initial_comment":initialComment, "channels":channels?.joinWithSeparator(",")] + client.api.uploadRequest(client.token, data: file, parameters: filterNilParameters(parameters), successClosure: { + (response) -> Void in + success?(file: File(file: response["file"] as? [String: AnyObject])) + }) {(error) -> Void in + failure?(error: error) + } + } + + //MARK: - Groups + public func closeGroup(groupID: String, success: ((closed: Bool)->Void)?, failure: FailureClosure?) { + close(.GroupsClose, channelID: groupID, success: { + (closed) -> Void in + success?(closed:closed) + }) {(error) -> Void in + failure?(error:error) + } + } + + public func groupHistory(id: String, latest: String = "\(NSDate().timeIntervalSince1970)", oldest: String = "0", inclusive: Bool = false, count: Int = 100, unreads: Bool = false, success: ((history: [String: AnyObject]?)->Void)?, failure: FailureClosure?) { + history(.GroupsHistory, id: id, latest: latest, oldest: oldest, inclusive: inclusive, count: count, unreads: unreads, success: { + (history) -> Void in + success?(history: history) + }) {(error) -> Void in + failure?(error: error) + } + } + + public func groupInfo(id: String, success: ((channel: Channel?)->Void)?, failure: FailureClosure?) { + info(.GroupsInfo, type:ChannelType.Group, id: id, success: { + (channel) -> Void in + success?(channel: channel) + }) {(error) -> Void in + failure?(error: error) + } + } + + public func groupsList(excludeArchived: Bool = false, success: ((channels: [[String: AnyObject]]?)->Void)?, failure: FailureClosure?) { + list(.GroupsList, type:ChannelType.Group, excludeArchived: excludeArchived, success: { + (channels) -> Void in + success?(channels: channels) + }) {(error) -> Void in + failure?(error: error) + } + } + + public func markGroup(channel: String, timestamp: String, success: ((ts: String)->Void)?, failure: FailureClosure?) { + mark(.GroupsMark, channel: channel, timestamp: timestamp, success: { + (ts) -> Void in + success?(ts: timestamp) + }) {(error) -> Void in + failure?(error: error) + } + } + + public func openGroup(channel: String, success: ((opened: Bool)->Void)?, failure: FailureClosure?) { + let parameters = ["channel":channel] + client.api.request(.GroupsOpen, token: client.token, parameters: parameters, successClosure: { + (response) -> Void in + success?(opened: true) + }) {(error) -> Void in + failure?(error: error) + } + } + + public func setGroupPurpose(channel: String, purpose: String, success: ((purposeSet: Bool)->Void)?, failure: FailureClosure?) { + setInfo(.GroupsSetPurpose, type: .Purpose, channel: channel, text: purpose, success: { + (purposeSet) -> Void in + success?(purposeSet: purposeSet) + }) {(error) -> Void in + failure?(error: error) + } + } + + public func setGroupTopic(channel: String, topic: String, success: ((topicSet: Bool)->Void)?, failure: FailureClosure?) { + setInfo(.GroupsSetTopic, type: .Topic, channel: channel, text: topic, success: { + (topicSet) -> Void in + success?(topicSet: topicSet) + }) {(error) -> Void in + failure?(error: error) + } + } + + //MARK: - IM + public func closeIM(channel: String, success: ((closed: Bool)->Void)?, failure: FailureClosure?) { + close(.IMClose, channelID: channel, success: { + (closed) -> Void in + success?(closed: closed) + }) {(error) -> Void in + failure?(error: error) + } + } + + public func imHistory(id: String, latest: String = "\(NSDate().timeIntervalSince1970)", oldest: String = "0", inclusive: Bool = false, count: Int = 100, unreads: Bool = false, success: ((history: [String: AnyObject]?)->Void)?, failure: FailureClosure?) { + history(.IMHistory, id: id, latest: latest, oldest: oldest, inclusive: inclusive, count: count, unreads: unreads, success: { + (history) -> Void in + success?(history: history) + }) {(error) -> Void in + failure?(error: error) + } + } + + public func imsList(excludeArchived: Bool = false, success: ((channels: [[String: AnyObject]]?)->Void)?, failure: FailureClosure?) { + list(.IMList, type:ChannelType.IM, excludeArchived: excludeArchived, success: { + (channels) -> Void in + success?(channels: channels) + }) {(error) -> Void in + failure?(error: error) + } + } + + public func markIM(channel: String, timestamp: String, success: ((ts: String)->Void)?, failure: FailureClosure?) { + mark(.IMMark, channel: channel, timestamp: timestamp, success: { + (ts) -> Void in + success?(ts: timestamp) + }) {(error) -> Void in + failure?(error: error) + } + } + + public func openIM(userID: String, success: ((imID: String?)->Void)?, failure: FailureClosure?) { + let parameters = ["user":userID] + client.api.request(.IMOpen, token: client.token, parameters: parameters, successClosure: { + (response) -> Void in + let group = response["channel"] as? [String: AnyObject] + success?(imID: group?["id"] as? String) + }) {(error) -> Void in + failure?(error: error) + } + } + + //MARK: - MPIM + public func closeMPIM(channel: String, success: ((closed: Bool)->Void)?, failure: FailureClosure?) { + close(.MPIMClose, channelID: channel, success: { + (closed) -> Void in + success?(closed: closed) + }) {(error) -> Void in + failure?(error: error) + } + } + + public func mpimHistory(id: String, latest: String = "\(NSDate().timeIntervalSince1970)", oldest: String = "0", inclusive: Bool = false, count: Int = 100, unreads: Bool = false, success: ((history: [String: AnyObject]?)->Void)?, failure: FailureClosure?) { + history(.MPIMHistory, id: id, latest: latest, oldest: oldest, inclusive: inclusive, count: count, unreads: unreads, success: { + (history) -> Void in + success?(history: history) + }) {(error) -> Void in + failure?(error: error) + } + } + + public func mpimsList(excludeArchived: Bool = false, success: ((channels: [[String: AnyObject]]?)->Void)?, failure: FailureClosure?) { + list(.MPIMList, type:ChannelType.Group, excludeArchived: excludeArchived, success: { + (channels) -> Void in + success?(channels: channels) + }) {(error) -> Void in + failure?(error: error) + } + } + + public func markMPIM(channel: String, timestamp: String, success: ((ts: String)->Void)?, failure: FailureClosure?) { + mark(.MPIMMark, channel: channel, timestamp: timestamp, success: { + (ts) -> Void in + success?(ts: timestamp) + }) {(error) -> Void in + failure?(error: error) + } + } + + public func openMPIM(userIDs: [String], success: ((mpimID: String?)->Void)?, failure: FailureClosure?) { + let parameters = ["users":userIDs.joinWithSeparator(",")] + client.api.request(.MPIMOpen, token: client.token, parameters: parameters, successClosure: { + (response) -> Void in + let group = response["group"] as? [String: AnyObject] + success?(mpimID: group?["id"] as? String) + }) {(error) -> Void in + failure?(error: error) + } + } + + //MARK: - Pins + public func pinItem(channel: String, file: String? = nil, fileComment: String? = nil, timestamp: String? = nil, success: ((pinned: Bool)->Void)?, failure: FailureClosure?) { + pin(.PinsAdd, channel: channel, file: file, fileComment: fileComment, timestamp: timestamp, success: { + (ok) -> Void in + success?(pinned: ok) + }) {(error) -> Void in + failure?(error: error) + } + } + + public func unpinItem(channel: String, file: String? = nil, fileComment: String? = nil, timestamp: String? = nil, success: ((unpinned: Bool)->Void)?, failure: FailureClosure?) { + pin(.PinsRemove, channel: channel, file: file, fileComment: fileComment, timestamp: timestamp, success: { + (ok) -> Void in + success?(unpinned: ok) + }) {(error) -> Void in + failure?(error: error) + } + } + + private func pin(endpoint: SlackAPIEndpoint, channel: String, file: String? = nil, fileComment: String? = nil, timestamp: String? = nil, success: ((ok: Bool)->Void)?, failure: FailureClosure?) { + let parameters: [String: AnyObject?] = ["channel":channel, "file":file, "file_comment":fileComment, "timestamp":timestamp] + client.api.request(endpoint, token: client.token, parameters: filterNilParameters(parameters), successClosure: { + (response) -> Void in + success?(ok: true) + }){(error) -> Void in + failure?(error: error) + } + } + + //MARK: - Reactions + // One of file, file_comment, or the combination of channel and timestamp must be specified. + public func addReaction(name: String, file: String? = nil, fileComment: String? = nil, channel: String? = nil, timestamp: String? = nil, success: ((reacted: Bool)->Void)?, failure: FailureClosure?) { + react(.ReactionsAdd, name: name, file: file, fileComment: fileComment, channel: channel, timestamp: timestamp, success: { + (ok) -> Void in + success?(reacted: ok) + }) {(error) -> Void in + failure?(error: error) + } + } + + // One of file, file_comment, or the combination of channel and timestamp must be specified. + public func removeReaction(name: String, file: String? = nil, fileComment: String? = nil, channel: String? = nil, timestamp: String? = nil, success: ((unreacted: Bool)->Void)?, failure: FailureClosure?) { + react(.ReactionsRemove, name: name, file: file, fileComment: fileComment, channel: channel, timestamp: timestamp, success: { + (ok) -> Void in + success?(unreacted: ok) + }) {(error) -> Void in + failure?(error: error) + } + } + + private func react(endpoint: SlackAPIEndpoint, name: String, file: String? = nil, fileComment: String? = nil, channel: String? = nil, timestamp: String? = nil, success: ((ok: Bool)->Void)?, failure: FailureClosure?) { + let parameters: [String: AnyObject?] = ["name":name, "file":file, "file_comment":fileComment, "channel":channel, "timestamp":timestamp] + client.api.request(endpoint, token: client.token, parameters: filterNilParameters(parameters), successClosure: { + (response) -> Void in + success?(ok: true) + }) {(error) -> Void in + failure?(error: error) + } + } + + //MARK: - Stars + // One of file, file_comment, channel, or the combination of channel and timestamp must be specified. + public func addStar(file: String? = nil, fileComment: String? = nil, channel: String? = nil, timestamp: String? = nil, success: ((starred: Bool)->Void)?, failure: FailureClosure?) { + star(.StarsAdd, file: file, fileComment: fileComment, channel: channel, timestamp: timestamp, success: { + (ok) -> Void in + success?(starred: ok) + }) {(error) -> Void in + failure?(error: error) + } + } + + // One of file, file_comment, channel, or the combination of channel and timestamp must be specified. + public func removeStar(file: String? = nil, fileComment: String? = nil, channel: String? = nil, timestamp: String? = nil, success: ((unstarred: Bool)->Void)?, failure: FailureClosure?) { + star(.StarsRemove, file: file, fileComment: fileComment, channel: channel, timestamp: timestamp, success: { + (ok) -> Void in + success?(unstarred: ok) + }) {(error) -> Void in + failure?(error: error) + } + } + + private func star(endpoint: SlackAPIEndpoint, file: String?, fileComment: String?, channel: String?, timestamp: String?, success: ((ok: Bool)->Void)?, failure: FailureClosure?) { + let parameters: [String: AnyObject?] = ["file":file, "file_comment":fileComment, "channel":channel, "timestamp":timestamp] + client.api.request(endpoint, token: client.token, parameters: filterNilParameters(parameters), successClosure: { + (response) -> Void in + success?(ok: true) + }) {(error) -> Void in + failure?(error: error) + } + } + + + //MARK: - Team + public func teamInfo(success: ((info: [String: AnyObject]?)->Void)?, failure: FailureClosure?) { + client.api.request(.TeamInfo, token: client.token, parameters: nil, successClosure: { + (response) -> Void in + success?(info: response["team"] as? [String: AnyObject]) + }) {(error) -> Void in + failure?(error: error) + } + } + + //MARK: - Users + public func userPresence(user: String, success: ((presence: String?)->Void)?, failure: FailureClosure?) { + let parameters: [String: AnyObject] = ["user":user] + client.api.request(.UsersGetPresence, token: client.token, parameters: parameters, successClosure: { + (response) -> Void in + success?(presence: response["presence"] as? String) + }){(error) -> Void in + failure?(error: error) + } + } + + public func userInfo(id: String, success: ((user: User?)->Void)?, failure: FailureClosure?) { + let parameters: [String: AnyObject] = ["user":id] + client.api.request(.UsersInfo, token: client.token, parameters: parameters, successClosure: { + (response) -> Void in + success?(user: User(user: response["user"] as? [String: AnyObject])) + }) {(error) -> Void in + failure?(error: error) + } + } + + public func usersList(includePresence: Bool = false, success: ((userList: [[String: AnyObject]]?)->Void)?, failure: FailureClosure?) { + let parameters: [String: AnyObject] = ["presence":includePresence] + client.api.request(.UsersList, token: client.token, parameters: parameters, successClosure: { + (response) -> Void in + success?(userList: response["members"] as? [[String: AnyObject]]) + }){(error) -> Void in + failure?(error: error) + } + } + + public func setUserActive(success: ((success: Bool)->Void)?, failure: FailureClosure?) { + client.api.request(.UsersSetActive, token: client.token, parameters: nil, successClosure: { + (response) -> Void in + success?(success: true) + }) {(error) -> Void in + failure?(error: error) + } + } + + public func setUserPresence(presence: Presence, success: ((success: Bool)->Void)?, failure: FailureClosure?) { + let parameters: [String: AnyObject] = ["presence":presence.rawValue] + client.api.request(.UsersSetPresence, token: client.token, parameters: parameters, successClosure: { + (response) -> Void in + success?(success:true) + }) {(error) -> Void in + failure?(error: error) + } + } + + //MARK: - Channel Utilities + private func close(endpoint: SlackAPIEndpoint, channelID: String, success: ((closed: Bool)->Void)?, failure: FailureClosure?) { + let parameters: [String: AnyObject] = ["channel":channelID] + client.api.request(endpoint, token: client.token, parameters: parameters, successClosure: { + (response) -> Void in + success?(closed: true) + }) {(error) -> Void in + failure?(error: error) + } + } + + private func history(endpoint: SlackAPIEndpoint, id: String, latest: String = "\(NSDate().timeIntervalSince1970)", oldest: String = "0", inclusive: Bool = false, count: Int = 100, unreads: Bool = false, success: ((history: [String: AnyObject]?)->Void)?, failure: FailureClosure?) { + let parameters: [String: AnyObject] = ["channel": id, "latest": latest, "oldest": oldest, "inclusive":inclusive, "count":count, "unreads":unreads] + client.api.request(endpoint, token: client.token, parameters: parameters, successClosure: { + (response) -> Void in + success?(history: response) + }) {(error) -> Void in + failure?(error: error) + } + } + + private func info(endpoint: SlackAPIEndpoint, type: ChannelType, id: String, success: ((channel: Channel?)->Void)?, failure: FailureClosure?) { + let parameters: [String: AnyObject] = ["channel": id] + client.api.request(endpoint, token: client.token, parameters: parameters, successClosure: { + (response) -> Void in + success?(channel: Channel(channel: response[type.rawValue] as? [String: AnyObject])) + }) {(error) -> Void in + failure?(error: error) + } + } + + private func list(endpoint: SlackAPIEndpoint, type: ChannelType, excludeArchived: Bool = false, success: ((channels: [[String: AnyObject]]?)->Void)?, failure: FailureClosure?) { + let parameters: [String: AnyObject] = ["exclude_archived": excludeArchived] + client.api.request(endpoint, token: client.token, parameters: parameters, successClosure: { + (response) -> Void in + success?(channels: response[type.rawValue+"s"] as? [[String: AnyObject]]) + }) {(error) -> Void in + failure?(error: error) + } + } + + private func mark(endpoint: SlackAPIEndpoint, channel: String, timestamp: String, success: ((ts: String)->Void)?, failure: FailureClosure?) { + let parameters: [String: AnyObject] = ["channel": channel, "ts": timestamp] + client.api.request(endpoint, token: client.token, parameters: parameters, successClosure: { + (response) -> Void in + success?(ts: timestamp) + }) {(error) -> Void in + failure?(error: error) + } + } + + private func setInfo(endpoint: SlackAPIEndpoint, type: InfoType, channel: String, text: String, success: ((success: Bool)->Void)?, failure: FailureClosure?) { + let parameters: [String: AnyObject] = ["channel": channel, type.rawValue: text] + client.api.request(endpoint, token: client.token, parameters: parameters, successClosure: { + (response) -> Void in + success?(success: true) + }) {(error) -> Void in + failure?(error: error) + } + } + + //MARK: - Filter Nil Parameters + private func filterNilParameters(parameters: [String: AnyObject?]) -> [String: AnyObject] { + var finalParameters = [String: AnyObject]() + for key in parameters.keys { + if parameters[key] != nil { + finalParameters[key] = parameters[key]! + } + } + return finalParameters + } +} diff --git a/SlackKit/Sources/SlackWebAPIErrorDispatcher.swift b/SlackKit/Sources/SlackWebAPIErrorDispatcher.swift new file mode 100644 index 0000000..798b5fd --- /dev/null +++ b/SlackKit/Sources/SlackWebAPIErrorDispatcher.swift @@ -0,0 +1,284 @@ +// +// SlackWebAPIErrorHandling.swift +// +// Copyright © 2016 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. + +import Foundation + +public enum SlackError: ErrorType { + case AccountInactive + case AlreadyArchived + case AlreadyInChannel + case AlreadyPinned + case AlreadyReacted + case AlreadyStarred + case BadClientSecret + case BadRedirectURI + case BadTimeStamp + case CantArchiveGeneral + case CantDeleteFile + case CantDeleteMessage + case CantInvite + case CantInviteSelf + case CantKickFromGeneral + case CantKickFromLastChannel + case CantKickSelf + case CantLeaveGeneral + case CantLeaveLastChannel + case CantUpdateMessage + case ChannelNotFound + case ComplianceExportsPreventDeletion + case EditWindowClosed + case FileCommentNotFound + case FileDeleted + case FileNotFound + case FileNotShared + case GroupContainsOthers + case InvalidArrayArg + case InvalidAuth + case InvalidChannel + case InvalidCharSet + case InvalidClientID + case InvalidCode + case InvalidFormData + case InvalidName + case InvalidPostType + case InvalidPresence + case InvalidTS + case InvalidTSLatest + case InvalidTSOldest + case IsArchived + case LastMember + case LastRAChannel + case MessageNotFound + case MessageTooLong + case MigrationInProgress + case MissingDuration + case MissingPostType + case NameTaken + case NoChannel + case NoItemSpecified + case NoReaction + case NoText + case NotArchived + case NotAuthed + case NotEnoughUsers + case NotInChannel + case NotInGroup + case NotPinned + case NotStarred + case OverPaginationLimit + case PaidOnly + case PermissionDenied + case PostingToGeneralChannelDenied + case RateLimited + case RequestTimeout + case RestrictedAction + case SnoozeEndFailed + case SnoozeFailed + case SnoozeNotActive + case TooLong + case TooManyEmoji + case TooManyReactions + case TooManyUsers + case UnknownError + case UnknownType + case UserDisabled + case UserDoesNotOwnChannel + case UserIsBot + case UserIsRestricted + case UserIsUltraRestricted + case UserListNotSupplied + case UserNotFound + case UserNotVisible +} + +internal struct ErrorDispatcher { + + static func dispatch(error: String) -> SlackError { + switch error { + case "account_inactive": + return .AccountInactive + case "already_in_channel": + return .AlreadyInChannel + case "already_pinned": + return .AlreadyPinned + case "already_reacted": + return .AlreadyReacted + case "already_starred": + return .AlreadyStarred + case "bad_client_secret": + return .BadClientSecret + case "bad_redirect_uri": + return .BadRedirectURI + case "bad_timestamp": + return .BadTimeStamp + case "cant_delete_file": + return .CantDeleteFile + case "cant_delete_message": + return .CantDeleteMessage + case "cant_invite": + return .CantInvite + case "cant_invite_self": + return .CantInviteSelf + case "cant_kick_from_general": + return .CantKickFromGeneral + case "cant_kick_from_last_channel": + return .CantKickFromLastChannel + case "cant_kick_self": + return .CantKickSelf + case "cant_leave_general": + return .CantLeaveGeneral + case "cant_leave_last_channel": + return .CantLeaveLastChannel + case "cant_update_message": + return .CantUpdateMessage + case "compliance_exports_prevent_deletion": + return .ComplianceExportsPreventDeletion + case "channel_not_found": + return .ChannelNotFound + case "edit_window_closed": + return .EditWindowClosed + case "file_comment_not_found": + return .FileCommentNotFound + case "file_deleted": + return .FileDeleted + case "file_not_found": + return .FileNotFound + case "file_not_shared": + return .FileNotShared + case "group_contains_others": + return .GroupContainsOthers + case "invalid_array_arg": + return .InvalidArrayArg + case "invalid_auth": + return .InvalidAuth + case "invalid_channel": + return .InvalidChannel + case "invalid_charset": + return .InvalidCharSet + case "invalid_client_id": + return .InvalidClientID + case "invalid_code": + return .InvalidCode + case "invalid_form_data": + return .InvalidFormData + case "invalid_name": + return .InvalidName + case "invalid_post_type": + return .InvalidPostType + case "invalid_presence": + return .InvalidPresence + case "invalid_timestamp": + return .InvalidTS + case "invalid_ts_latest": + return .InvalidTSLatest + case "invalid_ts_oldest": + return .InvalidTSOldest + case "is_archived": + return .IsArchived + case "last_member": + return .LastMember + case "last_ra_channel": + return .LastRAChannel + case "message_not_found": + return .MessageNotFound + case "msg_too_long": + return .MessageTooLong + case "migration_in_progress": + return .MigrationInProgress + case "missing_duration": + return .MissingDuration + case "missing_post_type": + return .MissingPostType + case "name_taken": + return .NameTaken + case "no_channel": + return .NoChannel + case "no_reaction": + return .NoReaction + case "no_item_specified": + return .NoItemSpecified + case "no_text": + return .NoText + case "not_archived": + return .NotArchived + case "not_authed": + return .NotAuthed + case "not_enough_users": + return .NotEnoughUsers + case "not_in_channel": + return .NotInChannel + case "not_in_group": + return .NotInGroup + case "not_pinned": + return .NotPinned + case "not_starred": + return .NotStarred + case "over_pagination_limit": + return .OverPaginationLimit + case "paid_only": + return .PaidOnly + case "perimssion_denied": + return .PermissionDenied + case "posting_to_general_channel_denied": + return .PostingToGeneralChannelDenied + case "rate_limited": + return .RateLimited + case "request_timeout": + return .RequestTimeout + case "snooze_end_failed": + return .SnoozeEndFailed + case "snooze_failed": + return .SnoozeFailed + case "snooze_not_active": + return .SnoozeNotActive + case "too_long": + return .TooLong + case "too_many_emoji": + return .TooManyEmoji + case "too_many_reactions": + return .TooManyReactions + case "too_many_users": + return .TooManyUsers + case "unknown_type": + return .UnknownType + case "user_disabled": + return .UserDisabled + case "user_does_not_own_channel": + return .UserDoesNotOwnChannel + case "user_is_bot": + return .UserIsBot + case "user_is_restricted": + return .UserIsRestricted + case "user_is_ultra_restricted": + return .UserIsUltraRestricted + case "user_list_not_supplied": + return .UserListNotSupplied + case "user_not_found": + return .UserNotFound + case "user_not_visible": + return .UserNotVisible + default: + return .UnknownError + } + } +} diff --git a/SlackKit/Sources/User.swift b/SlackKit/Sources/User.swift index b29638f..46c68c8 100644 --- a/SlackKit/Sources/User.swift +++ b/SlackKit/Sources/User.swift @@ -71,7 +71,7 @@ public struct User { internal(set) public var timeZoneLabel: String? internal(set) public var timeZoneOffSet: Int? internal(set) public var preferences: [String: AnyObject]? - // Client use + // Client properties internal(set) public var userGroups: [String: String]? internal init?(user: [String: AnyObject]?) { @@ -100,4 +100,4 @@ public struct User { self.id = id self.isBot = nil } -} +} \ No newline at end of file