Compare commits

..

6 Commits

Author SHA1 Message Date
Peter Zignego 247fe176fe Version bump 2016-09-26 00:05:56 -04:00
Peter Zignego c7365455d2 Update readme 2016-09-25 18:10:26 -04:00
Peter Zignego 3a03f745bf CocoaPods 2016-09-25 18:10:20 -04:00
Peter Zignego d291975368 SPM doesn’t support Swift 2.3 2016-09-25 18:08:31 -04:00
Peter Zignego cd273f266e Update Cartfile with 2.3 targets 2016-09-25 12:51:15 -04:00
Peter Zignego a01c51dd0d Set SWIFT_VERSION = 2.3 2016-09-25 12:49:07 -04:00
68 changed files with 2963 additions and 2812 deletions
+1 -1
View File
@@ -1 +1 @@
3.0
2.3
+2 -2
View File
@@ -1,2 +1,2 @@
github "https://github.com/daltoniam/Starscream" == 2.0.3
github "https://github.com/httpswift/swifter" == 1.3.3
github "daltoniam/Starscream" "swift-23"
github "pvzig/swifter.git" "077f1000d26cc5fa436a26e6d67284130b735a12"
+2
View File
@@ -0,0 +1,2 @@
github "daltoniam/Starscream" "8f3761215b3bd70d5111048e5e3ac435c862c13d"
github "pvzig/swifter" "077f1000d26cc5fa436a26e6d67284130b735a12"
-10
View File
@@ -1,10 +0,0 @@
import PackageDescription
let package = Package(
name: "SlackKit",
targets: [],
dependencies: [
.Package(url: "https://github.com/httpswift/swifter", majorVersion: 1),
.Package(url: "https://github.com/daltoniam/Starscream", majorVersion: 2)
]
)
+15 -6
View File
@@ -3,16 +3,25 @@ source 'https://github.com/CocoaPods/Specs.git'
use_frameworks!
target 'SlackKit OS X' do
pod 'Starscream', '~> 2.0.3'
pod 'Swifter', '~> 1.3.3'
pod 'Starscream', :git => 'https://github.com/pvzig/Starscream.git', :branch => 'swift-23'
pod 'Swifter', :git => 'https://github.com/pvzig/swifter.git', :branch => 'stable'
end
target 'SlackKit iOS' do
pod 'Starscream', '~> 2.0.3'
pod 'Swifter', '~> 1.3.3'
pod 'Starscream', :git => 'https://github.com/pvzig/Starscream.git', :branch => 'swift-23'
pod 'Swifter', :git => 'https://github.com/pvzig/swifter.git', :branch => 'stable'
end
target 'SlackKit tvOS' do
pod 'Starscream', '~> 2.0.3'
pod 'Swifter', '~> 1.3.3'
pod 'Starscream', :git => 'https://github.com/pvzig/Starscream.git', :branch => 'swift-23'
pod 'Swifter', :git => 'https://github.com/pvzig/swifter.git', :branch => 'stable'
end
# Set SWIFT_VERSION to 2.3 manually
post_install do |installer|
installer.pods_project.targets.each do |target|
target.build_configurations.each do |config|
config.build_settings['SWIFT_VERSION'] = '2.3'
end
end
end
+31
View File
@@ -0,0 +1,31 @@
PODS:
- Starscream (1.1.4)
- Swifter (1.2.6)
DEPENDENCIES:
- Starscream (from `https://github.com/pvzig/Starscream.git`, branch `swift-23`)
- Swifter (from `https://github.com/pvzig/swifter.git`, branch `stable`)
EXTERNAL SOURCES:
Starscream:
:branch: swift-23
:git: https://github.com/pvzig/Starscream.git
Swifter:
:branch: stable
:git: https://github.com/pvzig/swifter.git
CHECKOUT OPTIONS:
Starscream:
:commit: fc5529e18f1352fbe18bf8c0d8b333e02b636a1d
:git: https://github.com/pvzig/Starscream.git
Swifter:
:commit: 077f1000d26cc5fa436a26e6d67284130b735a12
:git: https://github.com/pvzig/swifter.git
SPEC CHECKSUMS:
Starscream: f919aeaf81afca6d5d4d3e5f6dd136ec92af2aa1
Swifter: c6f13d053398f1e2f209c7a394dfd5e863ae0157
PODFILE CHECKSUM: eb199f4d00b2166c447035baee5537560b583b38
COCOAPODS: 1.0.1
+66 -84
View File
@@ -1,28 +1,22 @@
![SlackKit](https://cloud.githubusercontent.com/assets/8311605/10260893/5ec60f96-694e-11e5-91fd-da6845942201.png)
![Swift Version](https://img.shields.io/badge/Swift-3.1.0-orange.svg) ![Plaforms](https://img.shields.io/badge/Platforms-macOS,iOS,tvOS-lightgrey.svg) ![License MIT](https://img.shields.io/badge/License-MIT-lightgrey.svg) [![CocoaPods compatible](https://img.shields.io/badge/CocoaPods-compatible-brightgreen.svg)](https://cocoapods.org) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-brightgreen.svg)](https://github.com/Carthage/Carthage) [![SwiftPM compatible](https://img.shields.io/badge/SwiftPM-compatible-brightgreen.svg)](https://github.com/apple/swift-package-manager)
![Swift Version](https://img.shields.io/badge/Swift-2.3-orange.svg) ![Plaforms](https://img.shields.io/badge/Platforms-macOS,iOS,tvOS-lightgrey.svg) ![License MIT](https://img.shields.io/badge/License-MIT-lightgrey.svg) ![Pod Version](https://img.shields.io/badge/Pod-2.3.0-blue.svg) [![Carthage compatible](https://img.shields.io/badge/Carthage-compatible-brightgreen.svg)](https://github.com/Carthage/Carthage)
## SlackKit: A Swift Slack Client Library
### Description
This is a Slack client library for OS X, iOS, and tvOS 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 to [bot users](https://api.slack.com/bot-users). SlackKit also supports Slacks [OAuth 2.0](https://api.slack.com/docs/oauth) flow including the [Add to Slack](https://api.slack.com/docs/slack-button) and [Sign in with Slack](https://api.slack.com/docs/sign-in-with-slack) buttons, [incoming webhooks](https://api.slack.com/incoming-webhooks), [slash commands](https://api.slack.com/slash-commands), and [message buttons](https://api.slack.com/docs/message-buttons).
This is the **Swift 3** branch of SlackKit. SlackKit also has support for [Swift 2.3](https://github.com/pvzig/SlackKit/tree/swift2.3) and [Linux](https://github.com/pvzig/SlackKit/tree/linux).
This is the Swift 2.3 branch of SlackKit. SlackKit also has support for [Swift 3](https://github.com/pvzig/SlackKit/tree/swift3) and [Linux](https://github.com/pvzig/SlackKit/tree/linux).
#### Building the SlackKit Framework
To build the SlackKit project directly, first build the dependencies using Carthage or CocoaPods. To use the framework in your application, install it in one of the following ways:
### Installation
#### CocoaPods
Add SlackKit to your pod file:
Add the pod to your podfile:
```
use_frameworks!
pod 'SlackKit'
```
and run
```
# Use CocoaPods version >= 1.1.0
pod install
```
@@ -30,29 +24,17 @@ pod install
Add SlackKit to your Cartfile:
```
github "pvzig/SlackKit"
github "pvzig/SlackKit" ~> 2.3
```
and run
```
carthage bootstrap
```
Drag the built `SlackKit.framework` into your Xcode project.
#### Swift Package Manager
Add SlackKit to your Package.swift
```swift
import PackageDescription
let package = Package(
dependencies: [
.Package(url: "https://github.com/pvzig/SlackKit.git", majorVersion: 3)
]
)
**Note:** SlackKit currently takes a _long_ time for the compiler to compile with optimizations turned on. I'm currently exploring a potential fix for this issue. In the meantime, you may want to skip the waiting and build it in the debug configuration instead:
```
Run `swift build` on your applications main directory.
carthage bootstrap --configuration "Debug"
```
Drag the built `SlackKit.framework` into your Xcode project.
To use the library in your project import it:
```
@@ -84,7 +66,7 @@ incoming.postMessage(message)
#### Slash Commands
After [configuring your slash command in Slack](https://my.slack.com/services/new/slash-commands) (you can also provide slash commands as part of a [Slack App](https://api.slack.com/slack-apps)), initialize a webhook server with the token for the slash command, a configured route, and a response.
```swift
let response = Response(text: "Hello, World!", responseType: .inChannel)
let response = Response(text: "Hello, World!", responseType: .InChannel)
let webhook = WebhookServer(token: "SLASH-COMMAND-TOKEN", route: "hello_world", response: response)
webhook.start()
```
@@ -107,7 +89,7 @@ let attachment = Attachment(fallback: "Hello World Attachment", title: "Attachme
To act on message actions, initialize an instance of the `MessageActionServer` using your apps verification token, your specified interactive messages request URL route, and a `MessageActionResponder`:
```swift
let action = Action(name: "hello_world", text: "Hello, World!")
let response = Response(text: "Hello, 🌎!", responseType: .inChannel)
let response = Response(text: "Hello, 🌎!", responseType: .InChannel)
let responder = MessageActionResponder(responses: [(action, response)])
let server = MessageActionServer(token: "SLACK-APP-VERIFICATION-TOKEN", route: "actions", responder: responder)
server.start()
@@ -148,7 +130,6 @@ SlackKit currently supports the a subset of the Slack Web APIs that are availabl
- channels.setPurpose
- channels.setTopic
- chat.delete
- chat.meMessage
- chat.postMessage
- chat.update
- emoji.list
@@ -196,22 +177,23 @@ SlackKit currently supports the a subset of the Slack Web APIs that are availabl
They can be accessed through a Client objects `webAPI` property:
```swift
client.webAPI.authenticationTest({(auth) in
print(auth)
}, failure: {(error) in
client.webAPI.authenticationTest({ (authenticated) -> Void in
print(authenticated)
}){(error) -> Void in
print(error)
})
}
```
#### Delegate methods
To receive delegate callbacks for events, register an object as the delegate for those events using the `onClientInitalization` block:
```swift
let bot = SlackKit(clientID: clientID, clientSecret: clientSecret)
let bot = SlackKit(clientID: "CLIENT_ID", clientSecret: "CLIENT_SECRET")
bot.onClientInitalization = { (client: Client) in
DispatchQueue.main.async(execute: {
dispatch_async(dispatch_get_main_queue(), {
client.connectionEventsDelegate = self
client.messageEventsDelegate = self
})
})
}
```
@@ -221,90 +203,90 @@ There are a number of delegates that you can set to receive callbacks for certai
##### ConnectionEventsDelegate
```swift
connected(_ client: Client)
disconnected(_ client: Client)
connectionFailed(_ client: Client, error: SlackError)
clientConnected(client: Client)
clientDisconnected(client: Client)
clientConnectionFailed(client: Client, error: SlackError)
```
##### MessageEventsDelegate
```swift
sent(_ message: Message, client: Client)
received(_ message: Message, client: Client)
changed(_ message: Message, client: Client)
deleted(_ message: Message?, client: Client)
messageSent(client: Client, message: Message)
messageReceived(client: Client, message: Message)
messageChanged(client: Client, message: Message)
messageDeleted(client: Client, message: Message?)
```
##### ChannelEventsDelegate
```swift
userTypingIn(_ channel: Channel, user: User, client: Client)
marked(_ channel: Channel, timestamp: String, client: Client)
created(_ channel: Channel, client: Client)
deleted(_ channel: Channel, client: Client)
renamed(_ channel: Channel, client: Client)
archived(_ channel: Channel, client: Client)
historyChanged(_ channel: Channel, client: Client)
joined(_ channel: Channel, client: Client)
left(_ channel: Channel, client: Client)
userTyping(client: Client, channel: Channel, user: User)
channelMarked(client: Client, channel: Channel, timestamp: String)
channelCreated(client: Client, channel: Channel)
channelDeleted(client: Client, channel: Channel)
channelRenamed(client: Client, channel: Channel)
channelArchived(client: Client, channel: Channel)
channelHistoryChanged(client: Client, channel: Channel)
channelJoined(client: Client, channel: Channel)
channelLeft(client: Client, channel: Channel)
```
##### DoNotDisturbEventsDelegate
```swift
updated(_ status: DoNotDisturbStatus, client: Client)
userUpdated(_ status: DoNotDisturbStatus, user: User, client: Client)
doNotDisturbUpdated(client: Client, dndStatus: DoNotDisturbStatus)
doNotDisturbUserUpdated(client: Client, dndStatus: DoNotDisturbStatus, user: User)
```
##### GroupEventsDelegate
```swift
opened(_ group: Channel, client: Client)
groupOpened(client: Client, group: Channel)
```
##### FileEventsDelegate
```swift
processed(_ file: File, client: Client)
madePrivate(_ file: File, client: Client)
deleted(_ file: File, client: Client)
commentAdded(_ file: File, comment: Comment, client: Client)
commentEdited(_ file: File, comment: Comment, client: Client)
commentDeleted(_ file: File, comment: Comment, client: Client)
fileProcessed(client: Client, file: File)
fileMadePrivate(client: Client, file: File)
fileDeleted(client: Client, file: File)
fileCommentAdded(client: Client, file: File, comment: Comment)
fileCommentEdited(client: Client, file: File, comment: Comment)
fileCommentDeleted(client: Client, file: File, comment: Comment)
```
##### PinEventsDelegate
```swift
pinned(_ item: Item, channel: Channel?, client: Client)
unpinned(_ item: Item, channel: Channel?, client: Client)
itemPinned(client: Client, item: Item, channel: Channel?)
itemUnpinned(client: Client, item: Item, channel: Channel?)
```
##### StarEventsDelegate
```swift
starred(_ item: Item, starred: Bool, _ client: Client)
itemStarred(client: Client, item: Item, star: Bool)
```
##### ReactionEventsDelegate
```swift
added(_ reaction: String, item: Item, itemUser: String, client: Client)
removed(_ reaction: String, item: Item, itemUser: String, client: Client)
reactionAdded(client: Client, reaction: String, item: Item, itemUser: String)
reactionRemoved(client: Client, reaction: String, item: Item, itemUser: String)
```
##### SlackEventsDelegate
```swift
preferenceChanged(_ preference: String, value: Any?, client: Client)
userChanged(_ user: User, client: Client)
presenceChanged(_ user: User, presence: String, client: Client)
manualPresenceChanged(_ user: User, presence: String, client: Client)
botEvent(_ bot: Bot, client: Client)
preferenceChanged(client: Client, preference: String, value: AnyObject?)
userChanged(client: Client, user: User)
presenceChanged(client: Client, user: User, presence: String)
manualPresenceChanged(client: Client, user: User, presence: String)
botEvent(client: Client, bot: Bot)
```
##### TeamEventsDelegate
```swift
userJoined(_ user: User, client: Client)
planChanged(_ plan: String, client: Client)
preferencesChanged(_ preference: String, value: Any?, client: Client)
nameChanged(_ name: String, client: Client)
domainChanged(_ domain: String, client: Client)
emailDomainChanged(_ domain: String, client: Client)
emojiChanged(_ client: Client)
teamJoined(client: Client, user: User)
teamPlanChanged(client: Client, plan: String)
teamPreferencesChanged(client: Client, preference: String, value: AnyObject?)
teamNameChanged(client: Client, name: String)
teamDomainChanged(client: Client, domain: String)
teamEmailDomainChanged(client: Client, domain: String)
teamEmojiChanged(client: Client)
```
##### SubteamEventsDelegate
```swift
event(_ userGroup: UserGroup, client: Client)
selfAdded(_ subteamID: String, client: Client)
selfRemoved(_ subteamID: String, client: Client)
subteamEvent(client: Client, userGroup: UserGroup)
subteamSelfAdded(client: Client, subteamID: String)
subteamSelfRemoved(client: Client, subteamID: String)
```
##### TeamProfileEventsDelegate
```swift
changed(_ profile: CustomProfile, client: Client)
deleted(_ profile: CustomProfile, client: Client)
reordered(_ profile: CustomProfile, client: Client)
teamProfileChanged(client: Client, profile: CustomProfile)
teamProfileDeleted(client: Client, profile: CustomProfile)
teamProfileReordered(client: Client, profile: CustomProfile)
```
### Examples
+4 -3
View File
@@ -1,6 +1,6 @@
Pod::Spec.new do |s|
s.name = "SlackKit"
s.version = "3.1.11"
s.version = "2.0.0"
s.summary = "a Slack client library for OS X, iOS, and tvOS written in Swift"
s.homepage = "https://github.com/pvzig/SlackKit"
s.license = 'MIT'
@@ -11,8 +11,9 @@ Pod::Spec.new do |s|
s.osx.deployment_target = '10.10'
s.tvos.deployment_target = '9.0'
s.requires_arc = true
s.source_files = 'Sources/SlackKit/**/*.swift'
s.source_files = 'SlackKit/Sources/*.swift'
s.frameworks = 'Foundation'
s.dependency 'Starscream'
s.dependency 'Starscream', '~> 1.1.3'
s.dependency 'Swifter'
end
File diff suppressed because it is too large Load Diff
@@ -2,6 +2,6 @@
<Workspace
version = "1.0">
<FileRef
location = "self:SlackKit.xcodeproj">
location = "self:SlackRTMKit.xcodeproj">
</FileRef>
</Workspace>
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0800"
LastUpgradeVersion = "0730"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0800"
LastUpgradeVersion = "0730"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -1,6 +1,6 @@
<?xml version="1.0" encoding="UTF-8"?>
<Scheme
LastUpgradeVersion = "0800"
LastUpgradeVersion = "0730"
version = "1.3">
<BuildAction
parallelizeBuildables = "YES"
@@ -30,16 +30,16 @@ public struct Action {
public let style: ActionStyle?
public let confirm: Confirm?
internal init(action:[String: Any]?) {
internal init(action:[String: AnyObject]?) {
name = action?["name"] as? String
text = action?["text"] as? String
type = action?["type"] as? String
value = action?["value"] as? String
style = ActionStyle(rawValue: action?["style"] as? String ?? "")
confirm = Confirm(confirm:action?["confirm"] as? [String: Any])
confirm = Confirm(confirm:action?["confirm"] as? [String: AnyObject])
}
public init(name: String, text: String, style: ActionStyle = .defaultStyle, value: String? = nil, confirm: Confirm? = nil) {
public init(name: String, text: String, style: ActionStyle = .Default, value: String? = nil, confirm: Confirm? = nil) {
self.type = "button"
self.name = name
self.text = text
@@ -48,14 +48,14 @@ public struct Action {
self.confirm = confirm
}
internal var dictionary: [String: Any] {
var dict = [String: Any]()
internal func dictionary() -> [String: AnyObject] {
var dict = [String: AnyObject]()
dict["name"] = name
dict["text"] = text
dict["type"] = type
dict["value"] = value
dict["style"] = style?.rawValue
dict["confirm"] = confirm?.dictionary
dict["confirm"] = confirm?.dictionary()
return dict
}
@@ -66,7 +66,7 @@ public struct Action {
public let okText: String?
public let dismissText: String?
internal init(confirm:[String: Any]?) {
internal init(confirm:[String: AnyObject]?) {
title = confirm?["title"] as? String
text = confirm?["text"] as? String
okText = confirm?["ok_text"] as? String
@@ -80,24 +80,26 @@ public struct Action {
self.dismissText = dismissText
}
internal var dictionary: [String: Any] {
var dict = [String: Any]()
internal func dictionary() -> [String: AnyObject] {
var dict = [String: AnyObject]()
dict["title"] = title
dict["text"] = text
dict["ok_text"] = okText
dict["dismiss_text"] = dismissText
return dict
}
}
}
public enum ActionStyle: String {
case defaultStyle = "default"
case primary = "primary"
case danger = "danger"
case Default = "default"
case Primary = "primary"
case Danger = "danger"
}
public enum ResponseType: String {
case inChannel = "in_channel"
case ephemeral = "ephemeral"
case InChannel = "in_channel"
case Ephemeral = "ephemeral"
}
@@ -41,10 +41,8 @@ public struct Attachment {
public let footer: String?
public let footerIcon: String?
public let ts: Int?
public let markdownEnabledFields: Set<AttachmentTextField>?
internal init(attachment: [String: Any]?) {
internal init(attachment: [String: AnyObject]?) {
fallback = attachment?["fallback"] as? String
callbackID = attachment?["callback_id"] as? String
type = attachment?["attachment_type"] as? String
@@ -61,13 +59,11 @@ public struct Attachment {
footer = attachment?["footer"] as? String
footerIcon = attachment?["footer_icon"] as? String
ts = attachment?["ts"] as? Int
fields = (attachment?["fields"] as? [[String: Any]])?.map { AttachmentField(field: $0) }
actions = (attachment?["actions"] as? [[String: Any]])?.map { Action(action: $0) }
markdownEnabledFields = (attachment?["mrkdwn_in"] as? [String]).map { Set($0.flatMap(AttachmentTextField.init)) }
fields = (attachment?["fields"] as? [[String: AnyObject]])?.map { AttachmentField(field: $0) }
actions = (attachment?["actions"] as? [[String: AnyObject]])?.map { Action(action: $0) }
}
public init(fallback: String, title: String?, callbackID: String? = nil, type: String? = nil, colorHex: String? = nil, pretext: String? = nil, authorName: String? = nil, authorLink: String? = nil, authorIcon: String? = nil, titleLink: String? = nil, text: String? = nil, fields: [AttachmentField]? = nil, actions: [Action]? = nil, imageURL: String? = nil, thumbURL: String? = nil, footer: String? = nil, footerIcon:String? = nil, ts:Int? = nil, markdownFields: Set<AttachmentTextField>? = nil) {
public init(fallback: String, title:String, callbackID: String? = nil, type: String? = nil, colorHex: String? = nil, pretext: String? = nil, authorName: String? = nil, authorLink: String? = nil, authorIcon: String? = nil, titleLink: String? = nil, text: String? = nil, fields: [AttachmentField]? = nil, actions: [Action]? = nil, imageURL: String? = nil, thumbURL: String? = nil, footer: String? = nil, footerIcon:String? = nil, ts:Int? = nil) {
self.fallback = fallback
self.callbackID = callbackID
self.type = type
@@ -86,46 +82,35 @@ public struct Attachment {
self.footer = footer
self.footerIcon = footerIcon
self.ts = ts
self.markdownEnabledFields = markdownFields
}
internal var dictionary: [String: Any] {
var attachment = [String: Any]()
internal func dictionary() -> [String: AnyObject] {
var attachment = [String: AnyObject]()
attachment["fallback"] = fallback
attachment["callback_id"] = callbackID
attachment["attachment_type"] = type
attachment["color"] = color
attachment["pretext"] = pretext
attachment["author_name"] = authorName
attachment["authorName"] = authorName
attachment["author_link"] = authorLink
attachment["author_icon"] = authorIcon
attachment["title"] = title
attachment["title_link"] = titleLink
attachment["text"] = text
attachment["fields"] = fields?.map{$0.dictionary}
attachment["actions"] = actions?.map{$0.dictionary}
attachment["fields"] = fields?.map{$0.dictionary()}
attachment["actions"] = actions?.map{$0.dictionary()}
attachment["image_url"] = imageURL
attachment["thumb_url"] = thumbURL
attachment["footer"] = footer
attachment["footer_icon"] = footerIcon
attachment["ts"] = ts
attachment["mrkdwn_in"] = markdownEnabledFields?.map { $0.rawValue }
return attachment
}
}
public enum AttachmentColor: String {
case good = "good"
case warning = "warning"
case danger = "danger"
}
public enum AttachmentTextField: String {
case fallback = "fallback"
case pretext = "pretext"
case authorName = "author_name"
case title = "title"
case text = "text"
case fields = "fields"
case footer = "footer"
case Good = "good"
case Warning = "warning"
case Danger = "danger"
}
@@ -27,23 +27,24 @@ public struct AttachmentField {
public let value: String?
public let short: Bool?
internal init(field: [String: Any]?) {
internal init(field: [String: AnyObject]?) {
title = field?["title"] as? String
value = field?["value"] as? String
short = field?["short"] as? Bool
}
public init(title: String?, value: String?, short: Bool? = nil) {
public init(title:String, value:String, short: Bool? = nil) {
self.title = title
self.value = value?.slackFormatEscaping
self.value = value.slackFormatEscaping()
self.short = short
}
internal var dictionary: [String: Any] {
var field = [String: Any]()
internal func dictionary() -> [String: AnyObject] {
var field = [String: AnyObject]()
field["title"] = title
field["value"] = value
field["short"] = short
return field
}
}
@@ -29,9 +29,9 @@ internal struct AuthorizeRequest {
let state: String
let team: String?
var parameters: [String: Any] {
var json = [String : Any]()
json["scope"] = scope.map({$0.rawValue}).joined(separator: ",")
var parameters: [String: AnyObject] {
var json = [String : AnyObject]()
json["scope"] = scope.map({$0.rawValue}).joinWithSeparator(",")
json["state"] = state
json["team"] = team
return json
@@ -44,4 +44,5 @@ internal struct AuthorizeRequest {
self.state = state
self.team = team
}
}
}
@@ -27,7 +27,7 @@ internal struct AuthorizeResponse {
let state: String
init?(queryParameters: [(String, String)]) {
guard let code = queryParameters.first?.1, let state = queryParameters.last?.1 else {
guard let code = queryParameters.first?.1, state = queryParameters.last?.1 else {
return nil
}
self.code = code
@@ -26,16 +26,17 @@ public struct Bot {
public let id: String?
internal(set) public var botToken: String?
internal(set) public var name: String?
internal(set) public var icons: [String: Any]?
internal(set) public var icons: [String: AnyObject]?
internal init(bot: [String: Any]?) {
internal init(bot: [String: AnyObject]?) {
id = bot?["id"] as? String
name = bot?["name"] as? String
icons = bot?["icons"] as? [String: Any]
icons = bot?["icons"] as? [String: AnyObject]
}
internal init(botUser: [String: Any]?) {
internal init(botUser: [String: AnyObject]?) {
id = botUser?["bot_user_id"] as? String
botToken = botUser?["bot_access_token"] as? String
}
}
@@ -49,7 +49,7 @@ public struct Channel {
internal(set) public var usersTyping = [String]()
internal(set) public var messages = [String: Message]()
internal init(channel: [String: Any]?) {
internal init(channel: [String: AnyObject]?) {
id = channel?["id"] as? String
name = channel?["name"] as? String
created = channel?["created"] as? Int
@@ -62,8 +62,8 @@ public struct Channel {
isUserDeleted = channel?["is_user_deleted"] as? Bool
user = channel?["user"] as? String
isOpen = channel?["is_open"] as? Bool
topic = Topic(topic: channel?["topic"] as? [String: Any])
purpose = Topic(topic: channel?["purpose"] as? [String: Any])
topic = Topic(topic: channel?["topic"] as? [String: AnyObject])
purpose = Topic(topic: channel?["purpose"] as? [String: AnyObject])
isMember = channel?["is_member"] as? Bool
lastRead = channel?["last_read"] as? String
unread = channel?["unread_count"] as? Int
@@ -71,8 +71,8 @@ public struct Channel {
hasPins = channel?["has_pins"] as? Bool
members = channel?["members"] as? [String]
if let latestMesssageDictionary = channel?["latest"] as? [String: Any] {
latest = Message(dictionary: latestMesssageDictionary)
if let latestMsgDictionary = channel?["latest"] as? [String: AnyObject] {
latest = Message(message: latestMsgDictionary)
} else {
latest = Message(ts: channel?["latest"] as? String)
}
@@ -23,153 +23,155 @@
internal extension Client {
func dispatch(_ anEvent: [String: Any]) {
let event = Event(anEvent)
let type = event.type ?? .unknown
func dispatch(event: [String: AnyObject]) {
let event = Event(event: event)
guard let type = event.type else {
return
}
switch type {
case .hello:
case .Hello:
connected = true
connectionEventsDelegate?.connected(self)
case .ok:
connectionEventsDelegate?.clientConnected(self)
case .Ok:
messageSent(event)
case .message:
case .Message:
if (event.subtype != nil) {
messageDispatcher(event)
} else {
messageReceived(event)
}
case .userTyping:
case .UserTyping:
userTyping(event)
case .channelMarked, .imMarked, .groupMarked:
case .ChannelMarked, .IMMarked, .GroupMarked:
channelMarked(event)
case .channelCreated, .imCreated:
case .ChannelCreated, .IMCreated:
channelCreated(event)
case .channelJoined, .groupJoined:
case .ChannelJoined, .GroupJoined:
channelJoined(event)
case .channelLeft, .groupLeft:
case .ChannelLeft, .GroupLeft:
channelLeft(event)
case .channelDeleted:
case .ChannelDeleted:
channelDeleted(event)
case .channelRenamed, .groupRename:
case .ChannelRenamed, .GroupRename:
channelRenamed(event)
case .channelArchive, .groupArchive:
case .ChannelArchive, .GroupArchive:
channelArchived(event, archived: true)
case .channelUnarchive, .groupUnarchive:
case .ChannelUnarchive, .GroupUnarchive:
channelArchived(event, archived: false)
case .channelHistoryChanged, .imHistoryChanged, .groupHistoryChanged:
case .ChannelHistoryChanged, .IMHistoryChanged, .GroupHistoryChanged:
channelHistoryChanged(event)
case .dndUpdated:
case .DNDUpdated:
doNotDisturbUpdated(event)
case .dndUpatedUser:
case .DNDUpatedUser:
doNotDisturbUserUpdated(event)
case .imOpen, .groupOpen:
case .IMOpen, .GroupOpen:
open(event, open: true)
case .imClose, .groupClose:
case .IMClose, .GroupClose:
open(event, open: false)
case .fileCreated:
case .FileCreated:
processFile(event)
case .fileShared:
case .FileShared:
processFile(event)
case .fileUnshared:
case .FileUnshared:
processFile(event)
case .filePublic:
case .FilePublic:
processFile(event)
case .filePrivate:
case .FilePrivate:
filePrivate(event)
case .fileChanged:
case .FileChanged:
processFile(event)
case .fileDeleted:
case .FileDeleted:
deleteFile(event)
case .fileCommentAdded:
case .FileCommentAdded:
fileCommentAdded(event)
case .fileCommentEdited:
case .FileCommentEdited:
fileCommentEdited(event)
case .fileCommentDeleted:
case .FileCommentDeleted:
fileCommentDeleted(event)
case .pinAdded:
case .PinAdded:
pinAdded(event)
case .pinRemoved:
case .PinRemoved:
pinRemoved(event)
case .pong:
case .Pong:
pong(event)
case .presenceChange:
case .PresenceChange:
presenceChange(event)
case .manualPresenceChange:
case .ManualPresenceChange:
manualPresenceChange(event)
case .prefChange:
case .PrefChange:
changePreference(event)
case .userChange:
case .UserChange:
userChange(event)
case .teamJoin:
case .TeamJoin:
teamJoin(event)
case .starAdded:
case .StarAdded:
itemStarred(event, star: true)
case .starRemoved:
case .StarRemoved:
itemStarred(event, star: false)
case .reactionAdded:
case .ReactionAdded:
addedReaction(event)
case .reactionRemoved:
case .ReactionRemoved:
removedReaction(event)
case .emojiChanged:
case .EmojiChanged:
emojiChanged(event)
case .commandsChanged:
case .CommandsChanged:
// This functionality is only used by our web client.
// The other APIs required to support slash command metadata are currently unstable.
// Until they are released other clients should ignore this event.
break
case .teamPlanChange:
case .TeamPlanChange:
teamPlanChange(event)
case .teamPrefChange:
case .TeamPrefChange:
teamPreferenceChange(event)
case .teamRename:
case .TeamRename:
teamNameChange(event)
case .teamDomainChange:
case .TeamDomainChange:
teamDomainChange(event)
case .emailDomainChange:
case .EmailDomainChange:
emailDomainChange(event)
case .teamProfileChange:
case .TeamProfileChange:
teamProfileChange(event)
case .teamProfileDelete:
case .TeamProfileDelete:
teamProfileDeleted(event)
case .teamProfileReorder:
case .TeamProfileReorder:
teamProfileReordered(event)
case .botAdded:
case .BotAdded:
bot(event)
case .botChanged:
case .BotChanged:
bot(event)
case .accountsChanged:
case .AccountsChanged:
// The accounts_changed event is used by our web client to maintain a list of logged-in accounts.
// Other clients should ignore this event.
break
case .teamMigrationStarted:
case .TeamMigrationStarted:
connect(options: options ?? ClientOptions())
case .reconnectURL:
case .ReconnectURL:
// The reconnect_url event is currently unsupported and experimental.
break
case .subteamCreated, .subteamUpdated:
case .SubteamCreated, .SubteamUpdated:
subteam(event)
case .subteamSelfAdded:
case .SubteamSelfAdded:
subteamAddedSelf(event)
case .subteamSelfRemoved:
case.SubteamSelfRemoved:
subteamRemovedSelf(event)
case .error:
print("Error: \(anEvent)")
case .unknown:
print("Unsupported event of type: \(anEvent["type"] ?? "No Type Information")")
case .Error:
print("Error: \(event)")
break
}
}
func messageDispatcher(_ event:Event) {
guard let value = event.subtype, let subtype = MessageSubtype(rawValue:value) else {
func messageDispatcher(event:Event) {
guard let value = event.subtype, subtype = MessageSubtype(rawValue:value) else {
return
}
switch subtype {
case .messageChanged:
case .MessageChanged:
messageChanged(event)
case .messageDeleted:
case .MessageDeleted:
messageDeleted(event)
default:
messageReceived(event)
}
}
}
+563
View File
@@ -0,0 +1,563 @@
//
// Client+EventHandling.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 extension Client {
//MARK: - Pong
func pong(event: Event) {
pong = event.replyTo
}
//MARK: - Messages
func messageSent(event: Event) {
guard let reply = event.replyTo, message = sentMessages[NSNumber(double: reply).stringValue], channel = message.channel, ts = message.ts else {
return
}
message.ts = event.ts
message.text = event.text
channels[channel]?.messages[ts] = message
messageEventsDelegate?.messageSent(self, message: message)
}
func messageReceived(event: Event) {
guard let channel = event.channel, message = event.message, id = channel.id, ts = message.ts else {
return
}
channels[id]?.messages[ts] = message
messageEventsDelegate?.messageReceived(self, message: message)
}
func messageChanged(event: Event) {
guard let id = event.channel?.id, nested = event.nestedMessage, ts = nested.ts else {
return
}
channels[id]?.messages[ts] = nested
messageEventsDelegate?.messageChanged(self, message: nested)
}
func messageDeleted(event: Event) {
guard let id = event.channel?.id, key = event.message?.deletedTs, message = channels[id]?.messages[key] else {
return
}
channels[id]?.messages.removeValueForKey(key)
messageEventsDelegate?.messageDeleted(self, message: message)
}
//MARK: - Channels
func userTyping(event: Event) {
guard let channel = event.channel, channelID = channel.id, user = event.user, userID = user.id where
channels.indexForKey(channelID) != nil && !channels[channelID]!.usersTyping.contains(userID) else {
return
}
channels[channelID]?.usersTyping.append(userID)
channelEventsDelegate?.userTyping(self, channel: channel, user: user)
let timeout = dispatch_time(DISPATCH_TIME_NOW, Int64(5.0 * Double(NSEC_PER_SEC)))
dispatch_after(timeout, dispatch_get_main_queue()) {
if let index = self.channels[channelID]?.usersTyping.indexOf(userID) {
self.channels[channelID]?.usersTyping.removeAtIndex(index)
}
}
}
func channelMarked(event: Event) {
guard let channel = event.channel, id = channel.id, timestamp = event.ts else {
return
}
channels[id]?.lastRead = event.ts
channelEventsDelegate?.channelMarked(self, channel: channel, timestamp: timestamp)
}
func channelCreated(event: Event) {
guard let channel = event.channel, id = channel.id else {
return
}
channels[id] = channel
channelEventsDelegate?.channelCreated(self, channel: channel)
}
func channelDeleted(event: Event) {
guard let channel = event.channel, id = channel.id else {
return
}
channels.removeValueForKey(id)
channelEventsDelegate?.channelDeleted(self, channel: channel)
}
func channelJoined(event: Event) {
guard let channel = event.channel, id = channel.id else {
return
}
channels[id] = event.channel
channelEventsDelegate?.channelJoined(self, channel: channel)
}
func channelLeft(event: Event) {
guard let channel = event.channel, id = channel.id else {
return
}
if let userID = authenticatedUser?.id, index = channels[id]?.members?.indexOf(userID) {
channels[id]?.members?.removeAtIndex(index)
}
channelEventsDelegate?.channelLeft(self, channel: channel)
}
func channelRenamed(event: Event) {
guard let channel = event.channel, id = channel.id else {
return
}
channels[id]?.name = channel.name
channelEventsDelegate?.channelRenamed(self, channel: channel)
}
func channelArchived(event: Event, archived: Bool) {
guard let channel = event.channel, id = channel.id else {
return
}
channels[id]?.isArchived = archived
channelEventsDelegate?.channelArchived(self, channel: channel)
}
func channelHistoryChanged(event: Event) {
guard let channel = event.channel else {
return
}
channelEventsDelegate?.channelHistoryChanged(self, channel: channel)
}
//MARK: - Do Not Disturb
func doNotDisturbUpdated(event: Event) {
guard let dndStatus = event.dndStatus else {
return
}
authenticatedUser?.doNotDisturbStatus = dndStatus
doNotDisturbEventsDelegate?.doNotDisturbUpdated(self, dndStatus: dndStatus)
}
func doNotDisturbUserUpdated(event: Event) {
guard let dndStatus = event.dndStatus, user = event.user, id = user.id else {
return
}
users[id]?.doNotDisturbStatus = dndStatus
doNotDisturbEventsDelegate?.doNotDisturbUserUpdated(self, dndStatus: dndStatus, user: user)
}
//MARK: - IM & Group Open/Close
func open(event: Event, open: Bool) {
guard let channel = event.channel, id = channel.id else {
return
}
channels[id]?.isOpen = open
groupEventsDelegate?.groupOpened(self, group: channel)
}
//MARK: - Files
func processFile(event: Event) {
guard let file = event.file, id = file.id else {
return
}
if let comment = file.initialComment, commentID = comment.id {
if files[id]?.comments[commentID] == nil {
files[id]?.comments[commentID] = comment
}
}
files[id] = file
fileEventsDelegate?.fileProcessed(self, file: file)
}
func filePrivate(event: Event) {
guard let file = event.file, id = file.id else {
return
}
files[id]?.isPublic = false
fileEventsDelegate?.fileMadePrivate(self, file: file)
}
func deleteFile(event: Event) {
guard let file = event.file, id = file.id else {
return
}
if files[id] != nil {
files.removeValueForKey(id)
}
fileEventsDelegate?.fileDeleted(self, file: file)
}
func fileCommentAdded(event: Event) {
guard let file = event.file, id = file.id, comment = event.comment, commentID = comment.id else {
return
}
files[id]?.comments[commentID] = comment
fileEventsDelegate?.fileCommentAdded(self, file: file, comment: comment)
}
func fileCommentEdited(event: Event) {
guard let file = event.file, id = file.id, comment = event.comment, commentID = comment.id else {
return
}
files[id]?.comments[commentID]?.comment = comment.comment
fileEventsDelegate?.fileCommentEdited(self, file: file, comment: comment)
}
func fileCommentDeleted(event: Event) {
guard let file = event.file, id = file.id, comment = event.comment, commentID = comment.id else {
return
}
files[id]?.comments.removeValueForKey(commentID)
fileEventsDelegate?.fileCommentDeleted(self, file: file, comment: comment)
}
//MARK: - Pins
func pinAdded(event: Event) {
guard let id = event.channelID, item = event.item else {
return
}
channels[id]?.pinnedItems.append(item)
pinEventsDelegate?.itemPinned(self, item: item, channel: channels[id])
}
func pinRemoved(event: Event) {
guard let id = event.channelID, item = event.item else {
return
}
if let pins = channels[id]?.pinnedItems.filter({$0 != item}) {
channels[id]?.pinnedItems = pins
}
pinEventsDelegate?.itemUnpinned(self, item: item, channel: channels[id])
}
//MARK: - Stars
func itemStarred(event: Event, star: Bool) {
guard let item = event.item, type = item.type else {
return
}
switch type {
case "message":
starMessage(item, star: star)
case "file":
starFile(item, star: star)
case "file_comment":
starComment(item)
default:
break
}
starEventsDelegate?.itemStarred(self, item: item, star: star)
}
func starMessage(item: Item, star: Bool) {
guard let message = item.message, ts = message.ts, channel = item.channel where channels[channel]?.messages[ts] != nil else {
return
}
channels[channel]?.messages[ts]?.isStarred = star
}
func starFile(item: Item, star: Bool) {
guard let file = item.file, id = file.id else {
return
}
files[id]?.isStarred = star
if let stars = files[id]?.stars {
if star == true {
files[id]?.stars = stars + 1
} else {
if stars > 0 {
files[id]?.stars = stars - 1
}
}
}
}
func starComment(item: Item) {
guard let file = item.file, id = file.id, comment = item.comment, commentID = comment.id else {
return
}
files[id]?.comments[commentID] = comment
}
//MARK: - Reactions
func addedReaction(event: Event) {
guard let item = event.item, type = item.type, reaction = event.reaction, userID = event.user?.id, itemUser = event.itemUser else {
return
}
switch type {
case "message":
guard let channel = item.channel, ts = item.ts, message = channels[channel]?.messages[ts] else {
return
}
message.reactions.append(Reaction(name: reaction, user: userID))
case "file":
guard let id = item.file?.id else {
return
}
files[id]?.reactions.append(Reaction(name: reaction, user: userID))
case "file_comment":
guard let id = item.file?.id, commentID = item.fileCommentID else {
return
}
files[id]?.comments[commentID]?.reactions.append(Reaction(name: reaction, user: userID))
default:
break
}
reactionEventsDelegate?.reactionAdded(self, reaction: reaction, item: item, itemUser: itemUser)
}
func removedReaction(event: Event) {
guard let item = event.item, type = item.type, key = event.reaction, userID = event.user?.id, itemUser = event.itemUser else {
return
}
switch type {
case "message":
guard let channel = item.channel, ts = item.ts, message = channels[channel]?.messages[ts] else {
return
}
message.reactions = message.reactions.filter({$0.name != key && $0.user != userID})
case "file":
guard let itemFile = item.file, id = itemFile.id else {
return
}
files[id]?.reactions = files[id]!.reactions.filter({$0.name != key && $0.user != userID})
case "file_comment":
guard let id = item.file?.id, commentID = item.fileCommentID else {
return
}
files[id]?.comments[commentID]?.reactions = files[id]!.comments[commentID]!.reactions.filter({$0.name != key && $0.user != userID})
default:
break
}
reactionEventsDelegate?.reactionRemoved(self, reaction: key, item: item, itemUser: itemUser)
}
//MARK: - Preferences
func changePreference(event: Event) {
guard let name = event.name else {
return
}
authenticatedUser?.preferences?[name] = event.value
slackEventsDelegate?.preferenceChanged(self, preference: name, value: event.value)
}
//Mark: - User Change
func userChange(event: Event) {
guard let user = event.user, id = user.id else {
return
}
let preferences = users[id]?.preferences
users[id] = user
users[id]?.preferences = preferences
slackEventsDelegate?.userChanged(self, user: user)
}
//MARK: - User Presence
func presenceChange(event: Event) {
guard let user = event.user, id = user.id, presence = event.presence else {
return
}
users[id]?.presence = event.presence
slackEventsDelegate?.presenceChanged(self, user: user, presence: presence)
}
//MARK: - Team
func teamJoin(event: Event) {
guard let user = event.user, id = user.id else {
return
}
users[id] = user
teamEventsDelegate?.teamJoined(self, user: user)
}
func teamPlanChange(event: Event) {
guard let plan = event.plan else {
return
}
team?.plan = plan
teamEventsDelegate?.teamPlanChanged(self, plan: plan)
}
func teamPreferenceChange(event: Event) {
guard let name = event.name else {
return
}
team?.prefs?[name] = event.value
teamEventsDelegate?.teamPreferencesChanged(self, preference: name, value: event.value)
}
func teamNameChange(event: Event) {
guard let name = event.name else {
return
}
team?.name = name
teamEventsDelegate?.teamNameChanged(self, name: name)
}
func teamDomainChange(event: Event) {
guard let domain = event.domain else {
return
}
team?.domain = domain
teamEventsDelegate?.teamDomainChanged(self, domain: domain)
}
func emailDomainChange(event: Event) {
guard let domain = event.emailDomain else {
return
}
team?.emailDomain = domain
teamEventsDelegate?.teamEmailDomainChanged(self, domain: domain)
}
func emojiChanged(event: Event) {
teamEventsDelegate?.teamEmojiChanged(self)
}
//MARK: - Bots
func bot(event: Event) {
guard let bot = event.bot, id = bot.id else {
return
}
bots[id] = bot
slackEventsDelegate?.botEvent(self, bot: bot)
}
//MARK: - Subteams
func subteam(event: Event) {
guard let subteam = event.subteam, id = subteam.id else {
return
}
userGroups[id] = subteam
subteamEventsDelegate?.subteamEvent(self, userGroup: subteam)
}
func subteamAddedSelf(event: Event) {
guard let subteamID = event.subteamID, _ = authenticatedUser?.userGroups else {
return
}
authenticatedUser?.userGroups![subteamID] = subteamID
subteamEventsDelegate?.subteamSelfAdded(self, subteamID: subteamID)
}
func subteamRemovedSelf(event: Event) {
guard let subteamID = event.subteamID else {
return
}
authenticatedUser?.userGroups?.removeValueForKey(subteamID)
subteamEventsDelegate?.subteamSelfRemoved(self, subteamID: subteamID)
}
//MARK: - Team Profiles
func teamProfileChange(event: Event) {
guard let profile = event.profile else {
return
}
for user in users {
for key in profile.fields.keys {
users[user.0]?.profile?.customProfile?.fields[key]?.updateProfileField(profile.fields[key])
}
}
teamProfileEventsDelegate?.teamProfileChanged(self, profile: profile)
}
func teamProfileDeleted(event: Event) {
guard let profile = event.profile else {
return
}
for user in users {
if let id = profile.fields.first?.0 {
users[user.0]?.profile?.customProfile?.fields[id] = nil
}
}
teamProfileEventsDelegate?.teamProfileDeleted(self, profile: profile)
}
func teamProfileReordered(event: Event) {
guard let profile = event.profile else {
return
}
for user in users {
for key in profile.fields.keys {
users[user.0]?.profile?.customProfile?.fields[key]?.ordering = profile.fields[key]?.ordering
}
}
teamProfileEventsDelegate?.teamProfileReordered(self, profile: profile)
}
//MARK: - Authenticated User
func manualPresenceChange(event: Event) {
guard let presence = event.presence, user = authenticatedUser else {
return
}
authenticatedUser?.presence = presence
slackEventsDelegate?.manualPresenceChanged(self, user: user, presence: presence)
}
}
@@ -21,43 +21,43 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
public enum ClientError: Error {
case channelDoesNotExist
case userDoesNotExist
public enum ClientError: ErrorType {
case ChannelDoesNotExist
case UserDoesNotExist
}
public extension Client {
//MARK: - User & Channel
public func getChannelIDWith(name: String) throws -> String {
guard let id = channels.filter({$0.1.name == strip(string:name)}).first?.0 else {
throw ClientError.channelDoesNotExist
public func getChannelIDByName(name: String) throws -> String {
guard let id = channels.filter({$0.1.name == stripString(name)}).first?.0 else {
throw ClientError.ChannelDoesNotExist
}
return id
}
public func getUserIDWith(name: String) throws -> String {
guard let id = users.filter({$0.1.name == strip(string:name)}).first?.0 else {
throw ClientError.userDoesNotExist
public func getUserIDByName(name: String) throws -> String {
guard let id = users.filter({$0.1.name == stripString(name)}).first?.0 else {
throw ClientError.UserDoesNotExist
}
return id
}
public func getImIDForUserWith(id: String, success: @escaping (_ imID: String?)->Void, failure: @escaping (SlackError)->Void) {
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(channel.0)
success(imID: channel.0)
} else {
webAPI.openIM(userID: id, success: success, failure: failure)
webAPI.openIM(id, success: success, failure: failure)
}
}
//MARK: - Utilities
internal func strip(string: String) -> String {
internal func stripString(string: String) -> String {
var strippedString = string
if string[string.startIndex] == "@" || string[string.startIndex] == "#" {
strippedString = string.substring(from: string.characters.index(string.startIndex, offsetBy: 1))
strippedString = string.substringFromIndex(string.startIndex.advancedBy(1))
}
return strippedString
}
@@ -44,7 +44,7 @@ public final class Client: WebSocketDelegate {
}
internal var webSocket: WebSocket?
private let pingPongQueue = DispatchQueue(label: "com.launchsoft.SlackKit")
private let pingPongQueue = dispatch_queue_create("com.launchsoft.SlackKit", DISPATCH_QUEUE_SERIAL)
internal var ping: Double?
internal var pong: Double?
internal var options: ClientOptions?
@@ -69,23 +69,24 @@ public final class Client: WebSocketDelegate {
self.token = apiToken
}
public func setAuthToken(_ token: String) {
public func setAuthToken(token: String) {
self.token = token
}
public func connect(options: ClientOptions = ClientOptions()) {
public func connect(options options: ClientOptions = ClientOptions()) {
self.options = options
webAPI.rtmStart(simpleLatest: options.simpleLatest, noUnreads: options.noUnreads, mpimAware: options.mpimAware, success: {(response) in
guard let socketURL = response["url"] as? String, let url = URL(string: socketURL) else {
webAPI.rtmStart(options.simpleLatest, noUnreads: options.noUnreads, mpimAware: options.mpimAware, success: {
(response) -> Void in
guard let socketURL = response["url"] as? String, url = NSURL(string: socketURL) else {
return
}
self.initialSetup(JSON: response)
self.initialSetup(response)
self.webSocket = WebSocket(url: url)
self.webSocket?.delegate = self
self.webSocket?.connect()
}, failure: {(error) in
self.connectionEventsDelegate?.connectionFailed(self, error: error)
})
}, failure: {(error) -> Void in
self.connectionEventsDelegate?.clientConnectionFailed(self, error: error)
})
}
public func disconnect() {
@@ -93,47 +94,48 @@ public final class Client: WebSocketDelegate {
}
//MARK: - RTM Message send
public func send(message: String, channelID: String) {
public func sendMessage(message: String, channelID: String) {
guard connected else { return }
if let data = try? format(message: message, channel: channelID), let string = String(data: data, encoding: String.Encoding.utf8) {
webSocket?.write(string: string)
if let data = try? formatMessageToSlackJsonString(msg: message, channel: channelID),
string = NSString(data: data, encoding: NSUTF8StringEncoding) as? String {
webSocket?.writeString(string)
}
}
private func format(message: String, channel: String) throws -> Data {
let json: [String: Any] = [
"id": Date().slackTimestamp,
private func formatMessageToSlackJsonString(message: (msg: String, channel: String)) throws -> NSData {
let json: [String: AnyObject] = [
"id": NSDate().slackTimestamp(),
"type": "message",
"channel": channel,
"text": message.slackFormatEscaping
"channel": message.channel,
"text": message.msg.slackFormatEscaping()
]
addSentMessage(json)
return try JSONSerialization.data(withJSONObject: json, options: [])
return try NSJSONSerialization.dataWithJSONObject(json, options: [])
}
private func addSentMessage(_ dictionary: [String: Any]) {
private func addSentMessage(dictionary: [String: AnyObject]) {
var message = dictionary
guard let id = message["id"] as? NSNumber else {
return
}
let ts = String(describing: id)
message.removeValue(forKey: "id")
let ts = String(id)
message.removeValueForKey("id")
message["ts"] = ts
message["user"] = self.authenticatedUser?.id
sentMessages[ts] = Message(dictionary: message)
sentMessages[ts] = Message(message: message)
}
//MARK: - RTM Ping
private func pingRTMServerAt(interval: TimeInterval) {
let delay = DispatchTime.now() + Double(Int64(interval * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)
pingPongQueue.asyncAfter(deadline: delay, execute: {
private func pingRTMServerAtInterval(interval: NSTimeInterval) {
let delay = dispatch_time(DISPATCH_TIME_NOW, Int64(interval * Double(NSEC_PER_SEC)))
dispatch_after(delay, pingPongQueue, {
guard self.connected && self.timeoutCheck() else {
self.disconnect()
return
}
self.sendRTMPing()
self.pingRTMServerAt(interval: interval)
self.pingRTMServerAtInterval(interval)
})
}
@@ -141,21 +143,22 @@ public final class Client: WebSocketDelegate {
guard connected else {
return
}
let json: [String: Any] = [
"id": Date().slackTimestamp,
"type": "ping"
let json: [String: AnyObject] = [
"id": NSDate().slackTimestamp(),
"type": "ping",
]
guard let data = try? JSONSerialization.data(withJSONObject: json, options: []) else {
guard let data = try? NSJSONSerialization.dataWithJSONObject(json, options: []) else {
return
}
if let string = String(data: data, encoding: String.Encoding.utf8) {
let string = NSString(data: data, encoding: NSUTF8StringEncoding)
if let writePing = string as? String {
ping = json["id"] as? Double
webSocket?.write(string: string)
webSocket?.writeString(writePing)
}
}
private func timeoutCheck() -> Bool {
if let pong = pong, let ping = ping, let timeout = options?.timeout {
if let pong = pong, ping = ping, timeout = options?.timeout {
if pong - ping < timeout {
return true
} else {
@@ -168,64 +171,62 @@ public final class Client: WebSocketDelegate {
}
//MARK: - Client setup
private func initialSetup(JSON: [String: Any]) {
team = Team(team: JSON["team"] as? [String: Any])
authenticatedUser = User(user: JSON["self"] as? [String: Any])
authenticatedUser?.doNotDisturbStatus = DoNotDisturbStatus(status: JSON["dnd"] as? [String: Any])
enumerateObjects(JSON["users"] as? Array) { (user) in self.addUser(user) }
enumerateObjects(JSON["channels"] as? Array) { (channel) in self.addChannel(channel) }
enumerateObjects(JSON["groups"] as? Array) { (group) in self.addChannel(group) }
enumerateObjects(JSON["mpims"] as? Array) { (mpim) in self.addChannel(mpim) }
enumerateObjects(JSON["ims"] as? Array) { (ims) in self.addChannel(ims) }
enumerateObjects(JSON["bots"] as? Array) { (bots) in self.addBot(bots) }
enumerateSubteams(JSON["subteams"] as? [String: Any])
private 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])
enumerateObjects(json["users"] as? Array) { (user) in self.addUser(user) }
enumerateObjects(json["channels"] as? Array) { (channel) in self.addChannel(channel) }
enumerateObjects(json["groups"] as? Array) { (group) in self.addChannel(group) }
enumerateObjects(json["mpims"] as? Array) { (mpim) in self.addChannel(mpim) }
enumerateObjects(json["ims"] as? Array) { (ims) in self.addChannel(ims) }
enumerateObjects(json["bots"] as? Array) { (bots) in self.addBot(bots) }
enumerateSubteams(json["subteams"] as? [String: AnyObject])
}
private func addUser(_ aUser: [String: Any]) {
private func addUser(aUser: [String: AnyObject]) {
let user = User(user: aUser)
if let id = user.id {
users[id] = user
}
}
private func addChannel(_ aChannel: [String: Any]) {
private func addChannel(aChannel: [String: AnyObject]) {
let channel = Channel(channel: aChannel)
if let id = channel.id {
channels[id] = channel
}
}
private func addBot(_ aBot: [String: Any]) {
private func addBot(aBot: [String: AnyObject]) {
let bot = Bot(bot: aBot)
if let id = bot.id {
bots[id] = bot
}
}
private func enumerateSubteams(_ subteams: [String: Any]?) {
private func enumerateSubteams(subteams: [String: AnyObject]?) {
if let subteams = subteams {
if let all = subteams["all"] as? [[String: Any]] {
if let all = subteams["all"] as? [[String: AnyObject]] {
for item in all {
let u = UserGroup(userGroup: item)
if let id = u.id {
self.userGroups[id] = u
}
self.userGroups[u.id!] = u
}
}
if let auth = subteams["self"] as? [String] {
for item in auth {
authenticatedUser?.userGroups = [String: String]()
authenticatedUser?.userGroups?[item] = item
authenticatedUser?.userGroups![item] = item
}
}
}
}
// MARK: - Utilities
private func enumerateObjects(_ array: [Any]?, initalizer: ([String: Any])-> Void) {
private func enumerateObjects(array: [AnyObject]?, initalizer: ([String: AnyObject])-> Void) {
if let array = array {
for object in array {
if let dictionary = object as? [String: Any] {
if let dictionary = object as? [String: AnyObject] {
initalizer(dictionary)
}
}
@@ -235,7 +236,7 @@ public final class Client: WebSocketDelegate {
// MARK: - WebSocketDelegate
public func websocketDidConnect(socket: WebSocket) {
if let pingInterval = options?.pingInterval {
pingRTMServerAt(interval: pingInterval)
pingRTMServerAtInterval(pingInterval)
}
}
@@ -243,21 +244,22 @@ public final class Client: WebSocketDelegate {
connected = false
webSocket = nil
authenticatedUser = nil
connectionEventsDelegate?.disconnected(self)
if let options = options, options.reconnect == true {
connectionEventsDelegate?.clientDisconnected(self)
if let options = options where options.reconnect == true {
connect(options: options)
}
}
public func websocketDidReceiveMessage(socket: WebSocket, text: String) {
guard let data = text.data(using: String.Encoding.utf8) else {
guard let data = text.dataUsingEncoding(NSUTF8StringEncoding) else {
return
}
if let json = (try? JSONSerialization.jsonObject(with: data, options: JSONSerialization.ReadingOptions.allowFragments)) as? [String: Any] {
if let json = (try? NSJSONSerialization.JSONObjectWithData(data, options: NSJSONReadingOptions.AllowFragments)) as? [String: AnyObject] {
dispatch(json)
}
}
public func websocketDidReceiveData(socket: WebSocket, data: Data) {}
public func websocketDidReceiveData(socket: WebSocket, data: NSData) {}
}
@@ -28,11 +28,11 @@ public struct ClientOptions {
let simpleLatest: Bool?
let noUnreads: Bool?
let mpimAware: Bool?
let pingInterval: TimeInterval?
let timeout: TimeInterval?
let pingInterval: NSTimeInterval?
let timeout: NSTimeInterval?
let reconnect: Bool?
public init(simpleLatest: Bool? = nil, noUnreads: Bool? = nil, mpimAware: Bool? = nil, pingInterval: TimeInterval? = nil, timeout: TimeInterval? = nil, reconnect: Bool? = nil) {
public init(simpleLatest: Bool? = nil, noUnreads: Bool? = nil, mpimAware: Bool? = nil, pingInterval: NSTimeInterval? = nil, timeout: NSTimeInterval? = nil, reconnect: Bool? = nil) {
self.simpleLatest = simpleLatest
self.noUnreads = noUnreads
self.mpimAware = mpimAware
@@ -40,4 +40,5 @@ public struct ClientOptions {
self.timeout = timeout
self.reconnect = reconnect
}
}
@@ -21,8 +21,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
public struct Comment: Equatable {
public struct Comment {
public let id: String?
public let user: String?
internal(set) public var created: Int?
@@ -31,7 +30,7 @@ public struct Comment: Equatable {
internal(set) public var stars: Int?
internal(set) public var reactions = [Reaction]()
internal init(comment:[String: Any]?) {
internal init(comment:[String: AnyObject]?) {
id = comment?["id"] as? String
created = comment?["created"] as? Int
user = comment?["user"] as? String
@@ -44,8 +43,10 @@ public struct Comment: Equatable {
self.id = id
self.user = nil
}
public static func ==(lhs: Comment, rhs: Comment) -> Bool {
return lhs.id == rhs.id
}
}
extension Comment: Equatable {}
public func ==(lhs: Comment, rhs: Comment) -> Bool {
return lhs.id == rhs.id
}
@@ -22,14 +22,13 @@
// THE SOFTWARE.
public struct CustomProfile {
internal(set) public var fields = [String: CustomProfileField]()
internal init(profile: [String: Any]?) {
if let eventFields = profile?["fields"] as? [Any] {
internal init(profile: [String: AnyObject]?) {
if let eventFields = profile?["fields"] as? [AnyObject] {
for field in eventFields {
var cpf: CustomProfileField?
if let fieldDictionary = field as? [String: Any] {
if let fieldDictionary = field as? [String: AnyObject] {
cpf = CustomProfileField(field: fieldDictionary)
} else {
cpf = CustomProfileField(id: field as? String)
@@ -39,12 +38,13 @@ public struct CustomProfile {
}
}
internal init(customFields: [String: Any]?) {
internal init(customFields: [String: AnyObject]?) {
if let customFields = customFields {
for key in customFields.keys {
let cpf = CustomProfileField(field: customFields[key] as? [String: Any])
let cpf = CustomProfileField(field: customFields[key] as? [String: AnyObject])
self.fields[key] = cpf
}
}
}
}
@@ -22,7 +22,6 @@
// THE SOFTWARE.
public struct CustomProfileField {
internal(set) public var id: String?
internal(set) public var alt: String?
internal(set) public var value: String?
@@ -34,7 +33,7 @@ public struct CustomProfileField {
internal(set) public var possibleValues: [String]?
internal(set) public var type: String?
internal init(field: [String: Any]?) {
internal init(field: [String: AnyObject]?) {
id = field?["id"] as? String
alt = field?["alt"] as? String
value = field?["value"] as? String
@@ -51,7 +50,7 @@ public struct CustomProfileField {
self.id = id
}
internal mutating func updateProfileField(_ profile: CustomProfileField?) {
internal mutating func updateProfileField(profile: CustomProfileField?) {
id = profile?.id != nil ? profile?.id : id
alt = profile?.alt != nil ? profile?.alt : alt
value = profile?.value != nil ? profile?.value : value
@@ -22,18 +22,18 @@
// THE SOFTWARE.
public struct DoNotDisturbStatus {
internal(set) public var enabled: Bool?
internal(set) public var nextDoNotDisturbStart: Int?
internal(set) public var nextDoNotDisturbEnd: Int?
internal(set) public var snoozeEnabled: Bool?
internal(set) public var snoozeEndtime: Int?
internal init(status: [String: Any]?) {
internal init(status: [String: AnyObject]?) {
enabled = status?["dnd_enabled"] as? Bool
nextDoNotDisturbStart = status?["next_dnd_start_ts"] as? Int
nextDoNotDisturbEnd = status?["next_dnd_end_ts"] as? Int
snoozeEnabled = status?["snooze_enabled"] as? Bool
snoozeEndtime = status?["snooze_endtime"] as? Int
}
}
@@ -22,11 +22,10 @@
// THE SOFTWARE.
public struct Edited {
public let user: String?
public let ts: String?
internal init(edited:[String: Any]?) {
internal init(edited:[String: AnyObject]?) {
user = edited?["user"] as? String
ts = edited?["ts"] as? String
}
+219
View File
@@ -0,0 +1,219 @@
//
// Event.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.
internal enum EventType: String {
case Hello = "hello"
case Message = "message"
case UserTyping = "user_typing"
case ChannelMarked = "channel_marked"
case ChannelCreated = "channel_created"
case ChannelJoined = "channel_joined"
case ChannelLeft = "channel_left"
case ChannelDeleted = "channel_deleted"
case ChannelRenamed = "channel_rename"
case ChannelArchive = "channel_archive"
case ChannelUnarchive = "channel_unarchive"
case ChannelHistoryChanged = "channel_history_changed"
case DNDUpdated = "dnd_updated"
case DNDUpatedUser = "dnd_updated_user"
case IMCreated = "im_created"
case IMOpen = "im_open"
case IMClose = "im_close"
case IMMarked = "im_marked"
case IMHistoryChanged = "im_history_changed"
case GroupJoined = "group_joined"
case GroupLeft = "group_left"
case GroupOpen = "group_open"
case GroupClose = "group_close"
case GroupArchive = "group_archive"
case GroupUnarchive = "group_unarchive"
case GroupRename = "group_rename"
case GroupMarked = "group_marked"
case GroupHistoryChanged = "group_history_changed"
case FileCreated = "file_created"
case FileShared = "file_shared"
case FileUnshared = "file_unshared"
case FilePublic = "file_public"
case FilePrivate = "file_private"
case FileChanged = "file_change"
case FileDeleted = "file_deleted"
case FileCommentAdded = "file_comment_added"
case FileCommentEdited = "file_comment_edited"
case FileCommentDeleted = "file_comment_deleted"
case PinAdded = "pin_added"
case PinRemoved = "pin_removed"
case Pong = "pong"
case PresenceChange = "presence_change"
case ManualPresenceChange = "manual_presence_change"
case PrefChange = "pref_change"
case UserChange = "user_change"
case TeamJoin = "team_join"
case StarAdded = "star_added"
case StarRemoved = "star_removed"
case ReactionAdded = "reaction_added"
case ReactionRemoved = "reaction_removed"
case EmojiChanged = "emoji_changed"
case CommandsChanged = "commands_changed"
case TeamPlanChange = "team_plan_change"
case TeamPrefChange = "team_pref_change"
case TeamRename = "team_rename"
case TeamDomainChange = "team_domain_change"
case EmailDomainChange = "email_domain_change"
case TeamProfileChange = "team_profile_change"
case TeamProfileDelete = "team_profile_delete"
case TeamProfileReorder = "team_profile_reorder"
case BotAdded = "bot_added"
case BotChanged = "bot_changed"
case AccountsChanged = "accounts_changed"
case TeamMigrationStarted = "team_migration_started"
case ReconnectURL = "reconnect_url"
case SubteamCreated = "subteam_created"
case SubteamUpdated = "subteam_updated"
case SubteamSelfAdded = "subteam_self_added"
case SubteamSelfRemoved = "subteam_self_removed"
case Ok = "ok"
case Error = "error"
}
internal enum MessageSubtype: String {
case BotMessage = "bot_message"
case MeMessage = "me_message"
case MessageChanged = "message_changed"
case MessageDeleted = "message_deleted"
case ChannelJoin = "channel_join"
case ChannelLeave = "channel_leave"
case ChannelTopic = "channel_topic"
case ChannelPurpose = "channel_purpose"
case ChannelName = "channel_name"
case ChannelArchive = "channel_archive"
case ChannelUnarchive = "channel_unarchive"
case GroupJoin = "group_join"
case GroupLeave = "group_leave"
case GroupTopic = "group_topic"
case GroupPurpose = "group_purpose"
case GroupName = "group_name"
case GroupArchive = "group_archive"
case GroupUnarchive = "group_unarchive"
case FileShare = "file_share"
case FileComment = "file_comment"
case FileMention = "file_mention"
case PinnedItem = "pinned_item"
case UnpinnedItem = "unpinned_item"
}
internal struct Event {
let type: EventType?
let ts: String?
let subtype: String?
let channelID: String?
let text: String?
let eventTs: String?
let latest: String?
let hidden: Bool?
let isStarred: Bool?
let hasPins: Bool?
let pinnedTo: [String]?
let fileID: String?
let presence: String?
let name: String?
let value: AnyObject?
let plan: String?
let url: String?
let domain: String?
let emailDomain: String?
let reaction: String?
let replyTo: Double?
let reactions: [[String: AnyObject]]?
let edited: Edited?
let bot: Bot?
let channel: Channel?
let comment: Comment?
let user: User?
let file: File?
let message: Message?
let nestedMessage: Message?
let itemUser: String?
let item: Item?
let dndStatus: DoNotDisturbStatus?
let subteam: UserGroup?
let subteamID: String?
var profile: CustomProfile?
init(event:[String: AnyObject]) {
type = EventType(rawValue: event["type"] as? String ?? "ok")
ts = event["ts"] as? String
subtype = event["subtype"] as? String
channelID = event["channel_id"] as? String
text = event["text"] as? String
eventTs = event["event_ts"] as? String
latest = event["latest"] as? String
hidden = event["hidden"] as? Bool
isStarred = event["is_starred"] as? Bool
hasPins = event["has_pins"] as? Bool
pinnedTo = event["pinned_top"] as? [String]
fileID = event["file_id"] as? String
presence = event["presence"] as? String
name = event["name"] as? String
value = event["value"]
plan = event["plan"] as? String
url = event["url"] as? String
domain = event["domain"] as? String
emailDomain = event["email_domain"] as? String
reaction = event["reaction"] as? String
replyTo = event["reply_to"] as? Double
reactions = event["reactions"] as? [[String: AnyObject]]
bot = Bot(bot: event["bot"] as? [String: AnyObject])
edited = Edited(edited:event["edited"] as? [String: AnyObject])
dndStatus = DoNotDisturbStatus(status: event["dnd_status"] as? [String: AnyObject])
itemUser = event["item_user"] as? String
item = Item(item: event["item"] as? [String: AnyObject])
subteam = UserGroup(userGroup: event["subteam"] as? [String: AnyObject])
subteamID = event["subteam_id"] as? String
message = Message(message: event)
nestedMessage = Message(message: event["message"] as? [String: AnyObject])
profile = CustomProfile(profile: event["profile"] as? [String: AnyObject])
file = File(id: event["file"] as? String)
// Comment, Channel, and User can come across as Strings or Dictionaries
if let commentDictionary = event["comment"] as? [String: AnyObject] {
comment = Comment(comment: commentDictionary)
} else {
comment = Comment(id: event["comment"] as? String)
}
if let userDictionary = event["user"] as? [String: AnyObject] {
user = User(user: userDictionary)
} else {
user = User(id: event["user"] as? String)
}
if let channelDictionary = event["channel"] as? [String: AnyObject] {
channel = Channel(channel: channelDictionary)
} else {
channel = Channel(id: event["channel"] as? String)
}
}
}
+109
View File
@@ -0,0 +1,109 @@
//
// EventDelegate.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.
public protocol ConnectionEventsDelegate: class {
func clientConnected(client: Client)
func clientDisconnected(client: Client)
func clientConnectionFailed(client: Client, error: SlackError)
}
public protocol MessageEventsDelegate: class {
func messageSent(client: Client, message: Message)
func messageReceived(client: Client, message: Message)
func messageChanged(client: Client, message: Message)
func messageDeleted(client: Client, message: Message?)
}
public protocol ChannelEventsDelegate: class {
func userTyping(client: Client, channel: Channel, user: User)
func channelMarked(client: Client, channel: Channel, timestamp: String)
func channelCreated(client: Client, channel: Channel)
func channelDeleted(client: Client, channel: Channel)
func channelRenamed(client: Client, channel: Channel)
func channelArchived(client: Client, channel: Channel)
func channelHistoryChanged(client: Client, channel: Channel)
func channelJoined(client: Client, channel: Channel)
func channelLeft(client: Client, channel: Channel)
}
public protocol DoNotDisturbEventsDelegate: class {
func doNotDisturbUpdated(client: Client, dndStatus: DoNotDisturbStatus)
func doNotDisturbUserUpdated(client: Client, dndStatus: DoNotDisturbStatus, user: User)
}
public protocol GroupEventsDelegate: class {
func groupOpened(client: Client, group: Channel)
}
public protocol FileEventsDelegate: class {
func fileProcessed(client: Client, file: File)
func fileMadePrivate(client: Client, file: File)
func fileDeleted(client: Client, file: File)
func fileCommentAdded(client: Client, file: File, comment: Comment)
func fileCommentEdited(client: Client, file: File, comment: Comment)
func fileCommentDeleted(client: Client, file: File, comment: Comment)
}
public protocol PinEventsDelegate: class {
func itemPinned(client: Client, item: Item, channel: Channel?)
func itemUnpinned(client: Client, item: Item, channel: Channel?)
}
public protocol StarEventsDelegate: class {
func itemStarred(client: Client, item: Item, star: Bool)
}
public protocol ReactionEventsDelegate: class {
func reactionAdded(client: Client, reaction: String, item: Item, itemUser: String)
func reactionRemoved(client: Client, reaction: String, item: Item, itemUser: String)
}
public protocol SlackEventsDelegate: class {
func preferenceChanged(client: Client, preference: String, value: AnyObject?)
func userChanged(client: Client, user: User)
func presenceChanged(client: Client, user: User, presence: String)
func manualPresenceChanged(client: Client, user: User, presence: String)
func botEvent(client: Client, bot: Bot)
}
public protocol TeamEventsDelegate: class {
func teamJoined(client: Client, user: User)
func teamPlanChanged(client: Client, plan: String)
func teamPreferencesChanged(client: Client, preference: String, value: AnyObject?)
func teamNameChanged(client: Client, name: String)
func teamDomainChanged(client: Client, domain: String)
func teamEmailDomainChanged(client: Client, domain: String)
func teamEmojiChanged(client: Client)
}
public protocol SubteamEventsDelegate: class {
func subteamEvent(client: Client, userGroup: UserGroup)
func subteamSelfAdded(client: Client, subteamID: String)
func subteamSelfRemoved(client: Client, subteamID: String)
}
public protocol TeamProfileEventsDelegate: class {
func teamProfileChanged(client: Client, profile: CustomProfile)
func teamProfileDeleted(client: Client, profile: CustomProfile)
func teamProfileReordered(client: Client, profile: CustomProfile)
}
@@ -23,19 +23,39 @@
import Foundation
public extension Date {
public extension NSDate {
var slackTimestamp: Double {
return NSNumber(value: timeIntervalSince1970).doubleValue
func slackTimestamp() -> Double {
return NSNumber(double: timeIntervalSince1970).doubleValue
}
}
internal extension String {
var slackFormatEscaping: String {
var escapedString = replacingOccurrences(of: "&", with: "&amp;")
escapedString = replacingOccurrences(of: "<", with: "&lt;")
escapedString = replacingOccurrences(of: ">", with: "&gt;")
func slackFormatEscaping() -> String {
var escapedString = stringByReplacingOccurrencesOfString("&", withString: "&amp;")
escapedString = stringByReplacingOccurrencesOfString("<", withString: "&lt;")
escapedString = stringByReplacingOccurrencesOfString(">", withString: "&gt;")
return escapedString
}
}
internal extension Dictionary where Key: StringLiteralConvertible, Value: AnyObject {
var requestStringFromParameters: String {
var requestString = ""
for key in self.keys {
if let value = self[key] as? String, encodedValue = value.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLHostAllowedCharacterSet()) {
requestString += "&\(key)=\(encodedValue)"
} else if let value = self[key] as? Int {
requestString += "&\(key)=\(value)"
}
}
return requestString
}
}
@@ -21,7 +21,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
public struct File: Equatable {
public struct File {
public let id: String?
public let created: Int?
@@ -78,7 +78,7 @@ public struct File: Equatable {
internal(set) public var comments = [String: Comment]()
internal(set) public var reactions = [Reaction]()
public init(file:[String: Any]?) {
public init(file:[String: AnyObject]?) {
id = file?["id"] as? String
created = file?["created"] as? Int
name = file?["name"] as? String
@@ -127,11 +127,11 @@ public struct File: Equatable {
channels = file?["channels"] as? [String]
groups = file?["groups"] as? [String]
ims = file?["ims"] as? [String]
initialComment = Comment(comment: file?["initial_comment"] as? [String: Any])
initialComment = Comment(comment: file?["initial_comment"] as? [String: AnyObject])
stars = file?["num_stars"] as? Int
isStarred = file?["is_starred"] as? Bool
pinnedTo = file?["pinned_to"] as? [String]
reactions = Reaction.reactionsFromArray(file?["reactions"] as? [[String: Any]])
reactions = Reaction.reactionsFromArray(file?["reactions"] as? [[String: AnyObject]])
}
internal init(id:String?) {
@@ -179,8 +179,11 @@ public struct File: Equatable {
linesMore = nil
initialComment = nil
}
public static func ==(lhs: File, rhs: File) -> Bool {
return lhs.id == rhs.id
}
}
extension File: Equatable {}
public func ==(lhs: File, rhs: File) -> Bool {
return lhs.id == rhs.id
}
@@ -25,17 +25,17 @@ import Foundation
public struct History {
internal(set) public var latest: Date?
internal(set) public var latest: NSDate?
internal(set) public var messages = [Message]()
public let hasMore: Bool?
internal init(history: [String: Any]?) {
if let latestStr = history?["latest"] as? String, let latestDouble = Double(latestStr) {
latest = Date(timeIntervalSince1970: TimeInterval(latestDouble))
internal init(history: [String: AnyObject]?) {
if let latestStr = history?["latest"] as? String, latestDouble = Double(latestStr) {
latest = NSDate(timeIntervalSince1970: NSTimeInterval(latestDouble))
}
if let msgs = history?["messages"] as? [[String: Any]] {
if let msgs = history?["messages"] as? [[String: AnyObject]] {
for message in msgs {
messages.append(Message(dictionary: message))
messages.append(Message(message: message))
}
}
hasMore = history?["has_more"] as? Bool
@@ -32,7 +32,7 @@ public struct IncomingWebhook {
public let iconEmoji: String?
public let iconURL: String?
internal init(webhook: [String: Any]?) {
internal init(webhook: [String: AnyObject]?) {
url = webhook?["url"] as? String
channel = webhook?["channel"] as? String
configurationURL = webhook?["configuration_url"] as? String
@@ -50,8 +50,8 @@ public struct IncomingWebhook {
self.configurationURL = nil
}
public func postMessage(_ response: Response, success: ((Bool)->Void)? = nil, failure: ((SlackError)->Void)? = nil) {
if let url = self.url, let data = try? JSONSerialization.data(withJSONObject: jsonBody(response.json), options: []) {
public func postMessage(response: Response, success: ((Bool)->Void)? = nil, failure: ((SlackError)->Void)? = nil) {
if let url = self.url, data = try? NSJSONSerialization.dataWithJSONObject(jsonBody(response.json()), options: []) {
NetworkInterface().customRequest(url, data: data, success: { _ in
success?(true)
}, errorClosure: {(error) in
@@ -60,7 +60,7 @@ public struct IncomingWebhook {
}
}
private func jsonBody(_ response: [String: Any]) -> [String: Any] {
private func jsonBody(response: [String: AnyObject]) -> [String: AnyObject] {
var json = response
json["channel"] = channel
json["username"] = username
@@ -68,4 +68,5 @@ public struct IncomingWebhook {
json["icon_url"] = iconURL
return json
}
}
@@ -21,8 +21,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
public struct Item: Equatable {
public struct Item {
public let type: String?
public let ts: String?
public let channel: String?
@@ -31,20 +30,21 @@ public struct Item: Equatable {
public let comment: Comment?
public let fileCommentID: String?
internal init(item:[String: Any]?) {
internal init(item:[String: AnyObject]?) {
type = item?["type"] as? String
ts = item?["ts"] as? String
channel = item?["channel"] as? String
message = Message(dictionary: item?["message"] as? [String: Any])
message = Message(message: item?["message"] as? [String: AnyObject])
// Comment and File can come across as Strings or Dictionaries
if let commentDictionary = item?["comment"] as? [String: Any] {
if let commentDictionary = item?["comment"] as? [String: AnyObject] {
comment = Comment(comment: commentDictionary)
} else {
comment = Comment(id: item?["comment"] as? String)
}
if let fileDictionary = item?["file"] as? [String: Any] {
if let fileDictionary = item?["file"] as? [String: AnyObject] {
file = File(file: fileDictionary)
} else {
file = File(id: item?["file"] as? String)
@@ -52,8 +52,10 @@ public struct Item: Equatable {
fileCommentID = item?["file_comment"] as? String
}
public static func ==(lhs: Item, rhs: Item) -> Bool {
return lhs.type == rhs.type && lhs.channel == rhs.channel && lhs.file == rhs.file && lhs.comment == rhs.comment && lhs.message == rhs.message
}
}
extension Item: Equatable {}
public func ==(lhs: Item, rhs: Item) -> Bool {
return lhs.type == rhs.type && lhs.channel == rhs.channel && lhs.file == rhs.file && lhs.comment == rhs.comment && lhs.message == rhs.message
}
@@ -21,7 +21,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
public final class Message: Equatable {
public final class Message {
public let type = "message"
public let subtype: String?
@@ -32,7 +32,7 @@ public final class Message: Equatable {
internal(set) public var text: String?
public let botID: String?
public let username: String?
public let icons: [String: Any]?
public let icons: [String: AnyObject]?
public let deletedTs: String?
internal(set) var purpose: String?
internal(set) var topic: String?
@@ -51,33 +51,33 @@ public final class Message: Equatable {
internal(set) public var replaceOriginal: Bool?
internal(set) public var deleteOriginal: Bool?
public init(dictionary: [String: Any]?) {
subtype = dictionary?["subtype"] as? String
ts = dictionary?["ts"] as? String
user = dictionary?["user"] as? String
channel = dictionary?["channel"] as? String
hidden = dictionary?["hidden"] as? Bool
text = dictionary?["text"] as? String
botID = dictionary?["bot_id"] as? String
username = dictionary?["username"] as? String
icons = dictionary?["icons"] as? [String: Any]
deletedTs = dictionary?["deleted_ts"] as? String
purpose = dictionary?["purpose"] as? String
topic = dictionary?["topic"] as? String
name = dictionary?["name"] as? String
members = dictionary?["members"] as? [String]
oldName = dictionary?["old_name"] as? String
upload = dictionary?["upload"] as? Bool
itemType = dictionary?["item_type"] as? String
isStarred = dictionary?["is_starred"] as? Bool
pinnedTo = dictionary?["pinned_to"] as? [String]
comment = Comment(comment: dictionary?["comment"] as? [String: Any])
file = File(file: dictionary?["file"] as? [String: Any])
reactions = Reaction.reactionsFromArray(dictionary?["reactions"] as? [[String: Any]])
attachments = (dictionary?["attachments"] as? [[String: Any]])?.map{Attachment(attachment: $0)}
responseType = ResponseType(rawValue: dictionary?["response_type"] as? String ?? "")
replaceOriginal = dictionary?["replace_original"] as? Bool
deleteOriginal = dictionary?["delete_original"] as? Bool
public init(message: [String: AnyObject]?) {
subtype = message?["subtype"] as? String
ts = message?["ts"] as? String
user = message?["user"] as? String
channel = message?["channel"] as? String
hidden = message?["hidden"] as? Bool
text = message?["text"] as? String
botID = message?["bot_id"] as? String
username = message?["username"] as? String
icons = message?["icons"] as? [String: AnyObject]
deletedTs = message?["deleted_ts"] as? String
purpose = message?["purpose"] as? String
topic = message?["topic"] as? String
name = message?["name"] as? String
members = message?["members"] as? [String]
oldName = message?["old_name"] as? String
upload = message?["upload"] as? Bool
itemType = message?["item_type"] as? String
isStarred = message?["is_starred"] as? Bool
pinnedTo = message?["pinned_to"] as? [String]
comment = Comment(comment: message?["comment"] as? [String: AnyObject])
file = File(file: message?["file"] as? [String: AnyObject])
reactions = Reaction.reactionsFromArray(message?["reactions"] as? [[String: AnyObject]])
attachments = (message?["attachments"] as? [[String: AnyObject]])?.map{Attachment(attachment: $0)}
responseType = ResponseType(rawValue: message?["response_type"] as? String ?? "")
replaceOriginal = message?["replace_original"] as? Bool
deleteOriginal = message?["delete_original"] as? Bool
}
internal init(ts:String?) {
@@ -94,8 +94,11 @@ public final class Message: Equatable {
comment = nil
file = nil
}
public static func ==(lhs: Message, rhs: Message) -> Bool {
return lhs.ts == rhs.ts && lhs.user == rhs.user && lhs.text == rhs.text
}
}
extension Message: Equatable {}
public func ==(lhs: Message, rhs: Message) -> Bool {
return lhs.ts == rhs.ts && lhs.user == rhs.user && lhs.text == rhs.text
}
@@ -35,17 +35,18 @@ internal struct MessageActionRequest: Request {
let originalMessage: Message?
let responseURL: String
init(response: [String: Any]?) {
action = (response?["actions"] as? [[String:Any]])?.map({Action(action: $0)}).first
init(response: [String: AnyObject]?) {
action = (response?["actions"] as? [[String:AnyObject]])?.map({Action(action: $0)}).first
callbackID = response?["callback_id"] as? String
team = Team(team: response?["team"] as? [String: Any])
channel = Channel(channel: response?["channel"] as? [String: Any])
user = User(user: response?["channel"] as? [String: Any])
team = Team(team: response?["team"] as? [String: AnyObject])
channel = Channel(channel: response?["channel"] as? [String: AnyObject])
user = User(user: response?["channel"] as? [String: AnyObject])
actionTS = response?["action_ts"] as? String
messageTS = response?["message_ts"] as? String
attachmentID = response?["attachment_id"] as? String
token = response?["token"] as? String
originalMessage = Message(dictionary: response?["original_message"] as? [String: Any])
originalMessage = Message(message: response?["original_message"] as? [String: AnyObject])
responseURL = response?["response_url"] as? String ?? ""
}
}
@@ -29,11 +29,12 @@ public struct MessageActionResponder {
self.responses = responses
}
internal func responseForRequest(_ request:MessageActionRequest) -> Reply? {
internal func responseForRequest(request:MessageActionRequest) -> Reply? {
if let response = responses.filter({$0.0.name == request.action?.name}).first?.1 {
return Reply.json(response: response)
return Reply.JSON(response: response)
} else {
return nil
}
}
}
@@ -21,7 +21,7 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
open class MessageActionServer: Server {
public class MessageActionServer: Server {
internal let responder: MessageActionResponder
@@ -31,15 +31,16 @@ open class MessageActionServer: Server {
addRoute(route)
}
internal func addRoute(_ route: String) {
internal func addRoute(route: String) {
http.POST["/\(route)"] = { request in
let payload = request.parseUrlencodedForm()
let actionRequest = MessageActionRequest(response: self.jsonFromRequest(payload[0].1))
if let reply = self.responder.responseForRequest(actionRequest), actionRequest.token == self.token {
if let reply = self.responder.responseForRequest(actionRequest) where actionRequest.token == self.token {
return self.request(actionRequest, reply: reply)
} else {
return .badRequest(.text("Bad request."))
return .BadRequest(.Text("Bad request."))
}
}
}
}
}
+153
View File
@@ -0,0 +1,153 @@
//
// 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: Endpoint, token: String? = nil, parameters: [String: AnyObject]?, successClosure: ([String: AnyObject])->Void, errorClosure: (SlackError)->Void) {
var requestString = "\(apiUrl)\(endpoint.rawValue)?"
if let token = token {
requestString += "token=\(token)"
}
if let params = parameters {
requestString += params.requestStringFromParameters
}
guard let url = NSURL(string: requestString) else {
errorClosure(SlackError.ClientNetworkError)
return
}
let request = NSURLRequest(URL:url)
NSURLSession.sharedSession().dataTaskWithRequest(request) {
(data, response, internalError) -> Void in
self.handleResponse(data, response: response, internalError: internalError, successClosure: {(json) in
successClosure(json)
}, errorClosure: {(error) in
errorClosure(error)
})
}.resume()
}
internal func customRequest(url: String, data: NSData, success: (Bool)->Void, errorClosure: (SlackError)->Void) {
guard let requestString = url.stringByRemovingPercentEncoding, url = NSURL(string: requestString) else {
errorClosure(SlackError.ClientNetworkError)
return
}
let request = NSMutableURLRequest(URL:url)
request.HTTPMethod = "POST"
let contentType = "application/json"
request.setValue(contentType, forHTTPHeaderField: "Content-Type")
request.HTTPBody = data
NSURLSession.sharedSession().dataTaskWithRequest(request) {
(data, response, internalError) -> Void in
if internalError == nil {
success(true)
} else {
errorClosure(SlackError.ClientNetworkError)
}
}.resume()
}
internal func uploadRequest(token: String, data: NSData, parameters: [String: AnyObject]?, successClosure: ([String: AnyObject])->Void, errorClosure: (SlackError)->Void) {
var requestString = "\(apiUrl)\(Endpoint.FilesUpload.rawValue)?token=\(token)"
if let params = parameters {
requestString = requestString + params.requestStringFromParameters
}
guard let url = NSURL(string: requestString) else {
errorClosure(SlackError.ClientNetworkError)
return
}
let request = NSMutableURLRequest(URL:url)
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
self.handleResponse(data, response: response, internalError: internalError, successClosure: {(json) in
successClosure(json)
}, errorClosure: {(error) in
errorClosure(error)
})
}.resume()
}
private func handleResponse(data: NSData?, response:NSURLResponse?, internalError:NSError?, successClosure: ([String: AnyObject])->Void, errorClosure: (SlackError)->Void) {
guard let data = data, response = response as? NSHTTPURLResponse else {
errorClosure(SlackError.ClientNetworkError)
return
}
do {
guard let json = try NSJSONSerialization.JSONObjectWithData(data, options: []) as? [String: AnyObject] else {
errorClosure(SlackError.ClientJSONError)
return
}
switch response.statusCode {
case 200:
if (json["ok"] as! Bool == true) {
successClosure(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 {
errorClosure(slackError)
} else {
errorClosure(SlackError.UnknownError)
}
}
}
private func randomBoundary() -> String {
return String(format: "slackkit.boundary.%08x%08x", arc4random(), arc4random())
}
}
@@ -31,14 +31,14 @@ internal struct OAuthResponse {
let incomingWebhook: IncomingWebhook?
let bot: Bot?
init(response: [String: Any]?) {
init(response: [String: AnyObject]?) {
accessToken = response?["access_token"] as? String
scope = (response?["scope"] as? String)?.components(separatedBy: ",").flatMap{Scope(rawValue:$0)}
scope = (response?["scope"] as? String)?.componentsSeparatedByString(",").flatMap{Scope(rawValue:$0)}
userID = response?["user_id"] as? String
teamName = response?["team_name"] as? String
teamID = response?["team_id"] as? String
incomingWebhook = IncomingWebhook(webhook: response?["incoming_webhook"] as? [String: Any])
bot = Bot(botUser: response?["bot"] as? [String: Any])
incomingWebhook = IncomingWebhook(webhook: response?["incoming_webhook"] as? [String: AnyObject])
bot = Bot(botUser: response?["bot"] as? [String: AnyObject])
}
}
@@ -25,13 +25,13 @@ import Foundation
import Swifter
internal protocol OAuthDelegate {
func userAuthed(_ response: OAuthResponse)
mutating func userAuthed(response: OAuthResponse)
}
public struct OAuthServer {
private let oauthURL = "https://slack.com/oauth/authorize"
private let http = HttpServer()
private let clientID: String
private let clientSecret: String
@@ -49,7 +49,7 @@ public struct OAuthServer {
start(port, forceIPV4: forceIPV4)
}
public func start(_ port: in_port_t = 8080, forceIPV4: Bool = false) {
public func start(port: in_port_t = 8080, forceIPV4: Bool = false) {
do {
try http.start(port, forceIPv4: forceIPV4)
} catch let error as NSError {
@@ -61,37 +61,39 @@ public struct OAuthServer {
http.stop()
}
private func oauthRoute() {
private mutating func oauthRoute() {
http["/oauth"] = { request in
guard let response = AuthorizeResponse(queryParameters: request.queryParams), response.state == self.state else {
return .badRequest(.text("Bad request."))
guard let response = AuthorizeResponse(queryParameters: request.queryParams) where response.state == self.state else {
return .BadRequest(.Text("Bad request."))
}
WebAPI.oauthAccess(clientID: self.clientID, clientSecret: self.clientSecret, code: response.code, redirectURI: self.redirectURI, success: {(response) in
self.delegate?.userAuthed(OAuthResponse(response: response))
}, failure: {(error) in
print("Authorization failed")
})
self.oauthRequest(response)
if let redirect = self.redirectURI {
return .movedPermanently(redirect)
return .MovedPermanently(redirect)
}
return .ok(.text("Authentication successful."))
return .OK(.Text("Authentication successful."))
}
}
private func oauthURLRequest(_ authorize: AuthorizeRequest) -> URLRequest? {
var components = URLComponents(string: "\(oauthURL)")
components?.queryItems = [
URLQueryItem(name: "client_id", value: "\(authorize.clientID)"),
URLQueryItem(name: "scope", value: "\(authorize.scope)"),
]
guard let url = components?.url else {
private mutating func oauthRequest(auth: AuthorizeResponse) {
WebAPI.oauthAccess(self.clientID, clientSecret: self.clientSecret, code: auth.code, redirectURI: self.redirectURI, success: {(response) in
self.delegate?.userAuthed(OAuthResponse(response: response))
}, failure: {(error) in
})
}
private func oauthURLRequest(authorize: AuthorizeRequest) -> NSURLRequest? {
var requestString = "\(oauthURL)?client_id=\(authorize.clientID)"
requestString += authorize.parameters.requestStringFromParameters
guard let url = NSURL(string: requestString) else {
return nil
}
return URLRequest(url: url)
return NSURLRequest(URL:url)
}
public func authorizeRequest(_ scope:[Scope], redirectURI: String, state: String = "slackkit", team: String? = nil) -> URLRequest? {
public func authorizeRequest(scope:[Scope], redirectURI: String, state: String = "slackkit", team: String? = nil) -> NSURLRequest? {
let request = AuthorizeRequest(clientID: clientID, scope: scope, redirectURI: redirectURI, state: state, team: team)
return oauthURLRequest(request)
}
}
@@ -21,12 +21,11 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
public struct Reaction: Equatable {
public struct Reaction {
public let name: String?
internal(set) public var user: String?
internal init(reaction:[String: Any]?) {
internal init(reaction:[String: AnyObject]?) {
name = reaction?["name"] as? String
}
@@ -35,11 +34,11 @@ public struct Reaction: Equatable {
self.user = user
}
static func reactionsFromArray(_ array: [[String: Any]]?) -> [Reaction] {
static func reactionsFromArray(array: [[String: AnyObject]]?) -> [Reaction] {
var reactions = [Reaction]()
if let array = array {
for reaction in array {
if let users = reaction["users"] as? [String], let name = reaction["name"] as? String {
if let users = reaction["users"] as? [String], name = reaction["name"] as? String {
for user in users {
reactions.append(Reaction(name: name, user: user))
}
@@ -49,7 +48,10 @@ public struct Reaction: Equatable {
return reactions
}
public static func ==(lhs: Reaction, rhs: Reaction) -> Bool {
return lhs.name == rhs.name
}
}
extension Reaction: Equatable {}
public func ==(lhs: Reaction, rhs: Reaction) -> Bool {
return lhs.name == rhs.name
}
@@ -33,11 +33,12 @@ public struct Response {
self.attachments = attachments
}
internal var json: [String: Any] {
var json = [String : Any]()
internal func json() -> [String: AnyObject] {
var json = [String : AnyObject]()
json["text"] = text
json["response_type"] = responseType?.rawValue
json["attachments"] = attachments?.map({$0.dictionary})
json["attachments"] = attachments?.map({$0.dictionary()})
return json
}
}
@@ -25,16 +25,16 @@ import Foundation
import Swifter
internal enum Reply {
case json(response: Response)
case text(body: String)
case badRequest
case JSON(response: Response)
case Text(body: String)
case BadRequest
}
internal protocol Request {
var responseURL: String { get }
}
open class Server {
public class Server {
internal let http = HttpServer()
internal let token: String
@@ -43,7 +43,7 @@ open class Server {
self.token = token
}
open func start(_ port: in_port_t = 8080, forceIPV4: Bool = false) {
public func start(port: in_port_t = 8080, forceIPV4: Bool = false) {
do {
try http.start(port, forceIPv4: forceIPV4)
} catch let error as NSError {
@@ -51,29 +51,29 @@ open class Server {
}
}
open func stop() {
public func stop() {
http.stop()
}
internal func request(_ request:Request, reply: Reply) -> HttpResponse {
internal func request(request:Request, reply: Reply) -> HttpResponse {
switch reply {
case .text(let body):
return .ok(.text(body))
case .json(let response):
return .ok(.json(response.json as AnyObject))
case .badRequest:
return .badRequest(.text("Bad request."))
case .Text(let body):
return .OK(.Text(body))
case .JSON(let response):
return .OK(.Json(response.json()))
case .BadRequest:
return .BadRequest(.Text("Bad request."))
}
}
internal func dictionaryFromRequest(_ body: [UInt8]) -> [String: Any]? {
let string = String(data: Data(bytes: UnsafePointer<UInt8>(body), count: body.count), encoding: String.Encoding.utf8)
if let body = string?.components(separatedBy: "&") {
var dict: [String: Any] = [:]
internal func dictionaryFromRequest(body: [UInt8]) -> [String: AnyObject]? {
let string = NSString(data: NSData(bytes: body, length: body.count), encoding: NSUTF8StringEncoding)
if let body = string?.componentsSeparatedByString("&") {
var dict: [String: AnyObject] = [:]
for argument in body {
let kv = argument.components(separatedBy: "=")
if let key = kv.first, let value = kv.last {
dict[key] = value as Any?
let kv = argument.componentsSeparatedByString("=")
if let key = kv.first, value = kv.last {
dict[key] = value
}
}
return dict
@@ -81,10 +81,11 @@ open class Server {
return nil
}
internal func jsonFromRequest(_ string: String) -> [String: Any]? {
guard let data = string.data(using: String.Encoding.utf8) else {
internal func jsonFromRequest(string: String) -> [String: AnyObject]? {
guard let data = string.dataUsingEncoding(NSUTF8StringEncoding) else {
return nil
}
return (try? JSONSerialization.jsonObject(with: data, options: [])) as? [String: Any] ?? nil
return (try? NSJSONSerialization.JSONObjectWithData(data, options: [])) as? [String: AnyObject] ?? nil
}
}
+121
View File
@@ -0,0 +1,121 @@
//
// SlackError.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.
public enum SlackError: String, ErrorType {
case AccountInactive = "account_inactive"
case AlreadyArchived = "already_archived"
case AlreadyInChannel = "already_in_channel"
case AlreadyPinned = "already_pinned"
case AlreadyReacted = "already_reacted"
case AlreadyStarred = "already_starred"
case BadClientSecret = "bad_client_secret"
case BadRedirectURI = "bad_redirect_uri"
case BadTimeStamp = "bad_timestamp"
case CantArchiveGeneral = "cant_archive_general"
case CantDelete = "cant_delete"
case CantDeleteFile = "cant_delete_file"
case CantDeleteMessage = "cant_delete_message"
case CantInvite = "cant_invite"
case CantInviteSelf = "cant_invite_self"
case CantKickFromGeneral = "cant_kick_from_general"
case CantKickFromLastChannel = "cant_kick_from_last_channel"
case CantKickSelf = "cant_kick_self"
case CantLeaveGeneral = "cant_leave_general"
case CantLeaveLastChannel = "cant_leave_last_channel"
case CantUpdateMessage = "cant_update_message"
case ChannelNotFound = "channel_not_found"
case ComplianceExportsPreventDeletion = "compliance_exports_prevent_deletion"
case EditWindowClosed = "edit_window_closed"
case FileCommentNotFound = "file_comment_not_found"
case FileDeleted = "file_deleted"
case FileNotFound = "file_not_found"
case FileNotShared = "file_not_shared"
case GroupContainsOthers = "group_contains_others"
case InvalidArgName = "invalid_arg_name"
case InvalidArrayArg = "invalid_array_arg"
case InvalidAuth = "invalid_auth"
case InvalidChannel = "invalid_channel"
case InvalidCharSet = "invalid_charset"
case InvalidClientID = "invalid_client_id"
case InvalidCode = "invalid_code"
case InvalidFormData = "invalid_form_data"
case InvalidName = "invalid_name"
case InvalidPostType = "invalid_post_type"
case InvalidPresence = "invalid_presence"
case InvalidTS = "invalid_timestamp"
case InvalidTSLatest = "invalid_ts_latest"
case InvalidTSOldest = "invalid_ts_oldest"
case IsArchived = "is_archived"
case LastMember = "last_member"
case LastRAChannel = "last_ra_channel"
case MessageNotFound = "message_not_found"
case MessageTooLong = "msg_too_long"
case MigrationInProgress = "migration_in_progress"
case MissingDuration = "missing_duration"
case MissingPostType = "missing_post_type"
case MissingScope = "missing_scope"
case NameTaken = "name_taken"
case NoChannel = "no_channel"
case NoComment = "no_comment"
case NoItemSpecified = "no_item_specified"
case NoReaction = "no_reaction"
case NoText = "no_text"
case NotArchived = "not_archived"
case NotAuthed = "not_authed"
case NotEnoughUsers = "not_enough_users"
case NotInChannel = "not_in_channel"
case NotInGroup = "not_in_group"
case NotPinned = "not_pinned"
case NotStarred = "not_starred"
case OverPaginationLimit = "over_pagination_limit"
case PaidOnly = "paid_only"
case PermissionDenied = "perimssion_denied"
case PostingToGeneralChannelDenied = "posting_to_general_channel_denied"
case RateLimited = "rate_limited"
case RequestTimeout = "request_timeout"
case RestrictedAction = "restricted_action"
case SnoozeEndFailed = "snooze_end_failed"
case SnoozeFailed = "snooze_failed"
case SnoozeNotActive = "snooze_not_active"
case TooLong = "too_long"
case TooManyEmoji = "too_many_emoji"
case TooManyReactions = "too_many_reactions"
case TooManyUsers = "too_many_users"
case UnknownError
case UnknownType = "unknown_type"
case UserDisabled = "user_disabled"
case UserDoesNotOwnChannel = "user_does_not_own_channel"
case UserIsBot = "user_is_bot"
case UserIsRestricted = "user_is_restricted"
case UserIsUltraRestricted = "user_is_ultra_restricted"
case UserListNotSupplied = "user_list_not_supplied"
case UserNotFound = "user_not_found"
case UserNotVisible = "user_not_visible"
// Client
case ClientNetworkError
case ClientJSONError
case ClientOAuthError
// HTTP
case TooManyRequests
case UnknownHTTPError
}
@@ -35,7 +35,7 @@ public final class SlackKit: OAuthDelegate {
public init(withAPIToken token: String, clientOptions: ClientOptions = ClientOptions()) {
self.clientOptions = clientOptions
let client = Client(apiToken: token)
DispatchQueue.main.async(execute: {
dispatch_async(dispatch_get_main_queue(), {
self.onClientInitalization?(client)
})
clients[token] = client
@@ -48,7 +48,7 @@ public final class SlackKit: OAuthDelegate {
oauth = try? OAuthServer(clientID: clientID, clientSecret: clientSecret, state: state, redirectURI: redirectURI, port: port, forceIPV4: forceIPV4, delegate: self)
}
internal func userAuthed(_ response: OAuthResponse) {
internal func userAuthed(response: OAuthResponse) {
// User auth
if let token = response.accessToken {
let client = Client(apiToken: token)
@@ -63,4 +63,5 @@ public final class SlackKit: OAuthDelegate {
client.connect(options: self.clientOptions)
}
}
}
@@ -29,19 +29,19 @@ public struct Team {
internal(set) public var emailDomain: String?
internal(set) public var messageEditWindowMinutes: Int?
internal(set) public var overStorageLimit: Bool?
internal(set) public var prefs: [String: Any]?
internal(set) public var prefs: [String: AnyObject]?
internal(set) public var plan: String?
internal(set) public var icon: TeamIcon?
internal init(team: [String: Any]?) {
internal init(team: [String: AnyObject]?) {
id = team?["id"] as? String
name = team?["name"] as? String
domain = team?["domain"] as? String
emailDomain = team?["email_domain"] as? String
messageEditWindowMinutes = team?["msg_edit_window_mins"] as? Int
overStorageLimit = team?["over_storage_limit"] as? Bool
prefs = team?["prefs"] as? [String: Any]
prefs = team?["prefs"] as? [String: AnyObject]
plan = team?["plan"] as? String
icon = TeamIcon(icon: team?["icon"] as? [String: Any])
icon = TeamIcon(icon: team?["icon"] as? [String: AnyObject])
}
}
@@ -22,7 +22,6 @@
// THE SOFTWARE.
public struct TeamIcon {
internal(set) public var image34: String?
internal(set) public var image44: String?
internal(set) public var image68: String?
@@ -32,7 +31,7 @@ public struct TeamIcon {
internal(set) public var imageOriginal: String?
internal(set) public var imageDefault: Bool?
internal init(icon: [String: Any]?) {
internal init(icon: [String: AnyObject]?) {
image34 = icon?["image_34"] as? String
image44 = icon?["image_44"] as? String
image68 = icon?["image_68"] as? String
@@ -22,12 +22,11 @@
// THE SOFTWARE.
public struct Topic {
public let value: String?
public let creator: String?
public let lastSet: Int?
internal init(topic: [String: Any]?) {
internal init(topic: [String: AnyObject]?) {
value = topic?["value"] as? String
creator = topic?["creator"] as? String
lastSet = topic?["last_set"] as? Int
@@ -24,7 +24,6 @@
public struct User {
public struct Profile {
internal(set) public var firstName: String?
internal(set) public var lastName: String?
internal(set) public var realName: String?
@@ -38,7 +37,7 @@ public struct User {
internal(set) public var image192: String?
internal(set) public var customProfile: CustomProfile?
internal init(profile: [String: Any]?) {
internal init(profile: [String: AnyObject]?) {
firstName = profile?["first_name"] as? String
lastName = profile?["last_name"] as? String
realName = profile?["real_name"] as? String
@@ -50,10 +49,11 @@ public struct User {
image48 = profile?["image_48"] as? String
image72 = profile?["image_72"] as? String
image192 = profile?["image_192"] as? String
customProfile = CustomProfile(customFields: profile?["fields"] as? [String: Any])
customProfile = CustomProfile(customFields: profile?["fields"] as? [String: AnyObject])
}
}
public let id: String?
internal(set) public var name: String?
internal(set) public var deleted: Bool?
@@ -73,15 +73,15 @@ public struct User {
internal(set) public var timeZone: String?
internal(set) public var timeZoneLabel: String?
internal(set) public var timeZoneOffSet: Int?
internal(set) public var preferences: [String: Any]?
internal(set) public var preferences: [String: AnyObject]?
// Client properties
internal(set) public var userGroups: [String: String]?
internal init(user: [String: Any]?) {
internal init(user: [String: AnyObject]?) {
id = user?["id"] as? String
name = user?["name"] as? String
deleted = user?["deleted"] as? Bool
profile = Profile(profile: user?["profile"] as? [String: Any])
profile = Profile(profile: user?["profile"] as? [String: AnyObject])
color = user?["color"] as? String
isAdmin = user?["is_admin"] as? Bool
isOwner = user?["is_owner"] as? Bool
@@ -96,11 +96,11 @@ public struct User {
timeZone = user?["tz"] as? String
timeZoneLabel = user?["tz_label"] as? String
timeZoneOffSet = user?["tz_offset"] as? Int
preferences = user?["prefs"] as? [String: Any]
preferences = user?["prefs"] as? [String: AnyObject]
}
internal init(id: String?) {
self.id = id
self.isBot = nil
}
}
}
@@ -37,11 +37,11 @@ public struct UserGroup {
public let createdBy: String?
internal(set) public var updatedBy: String?
internal(set) public var deletedBy: String?
internal(set) public var preferences: [String: Any]?
internal(set) public var preferences: [String: AnyObject]?
internal(set) public var users: [String]?
internal(set) public var userCount: Int?
internal init(userGroup: [String: Any]?) {
internal init(userGroup: [String: AnyObject]?) {
id = userGroup?["id"] as? String
teamID = userGroup?["team_id"] as? String
isUserGroup = userGroup?["is_usergroup"] as? Bool
@@ -56,10 +56,11 @@ public struct UserGroup {
createdBy = userGroup?["created_by"] as? String
updatedBy = userGroup?["updated_by"] as? String
deletedBy = userGroup?["deleted_by"] as? String
preferences = userGroup?["prefs"] as? [String: Any]
preferences = userGroup?["prefs"] as? [String: AnyObject]
users = userGroup?["users"] as? [String]
if let count = userGroup?["user_count"] as? String {
userCount = Int(count)
}
}
}
+770
View File
@@ -0,0 +1,770 @@
//
// WebAPI.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 Endpoint: String {
case APITest = "api.test"
case AuthRevoke = "auth.revoke"
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 DNDInfo = "dnd.info"
case DNDTeamInfo = "dnd.teamInfo"
case EmojiList = "emoji.list"
case FilesCommentsAdd = "files.comments.add"
case FilesCommentsEdit = "files.comments.edit"
case FilesCommentsDelete = "files.comments.delete"
case FilesDelete = "files.delete"
case FilesInfo = "files.info"
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 OAuthAccess = "oauth.access"
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 final class WebAPI {
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 networkInterface: NetworkInterface
private let token: String
public init(token: String) {
self.networkInterface = NetworkInterface()
self.token = token
}
//MARK: - RTM
public func rtmStart(simpleLatest: Bool? = nil, noUnreads: Bool? = nil, mpimAware: Bool? = nil, success: ((response: [String: AnyObject])->Void)?, failure: FailureClosure?) {
let parameters: [String: AnyObject?] = ["simple_latest": simpleLatest, "no_unreads": noUnreads, "mpim_aware": mpimAware]
networkInterface.request(.RTMStart, token: token, parameters: WebAPI.filterNilParameters(parameters), successClosure: {
(response) -> Void in
success?(response: response)
}) {(error) -> Void in
failure?(error: error)
}
}
//MARK: - Auth
public func authenticationTest(success: ((authenticated: Bool)->Void)?, failure: FailureClosure?) {
networkInterface.request(.AuthTest, token: token, parameters: nil, successClosure: {
(response) -> Void in
success?(authenticated: true)
}) {(error) -> Void in
failure?(error: error)
}
}
public static func oauthAccess(clientID: String, clientSecret: String, code: String, redirectURI: String? = nil, success: ((response: [String: AnyObject])->Void)?, failure: ((SlackError)->Void)?) {
let parameters: [String: AnyObject?] = ["client_id": clientID, "client_secret": clientSecret, "code": code, "redirect_uri": redirectURI]
NetworkInterface().request(.OAuthAccess, parameters: filterNilParameters(parameters), successClosure: {
(response) -> Void in
success?(response:response)
}) {(error) -> Void in
failure?(error)
}
}
public static func oauthRevoke(token: String, test: Bool? = nil, success: ((revoked:Bool)->Void)?, failure: ((SlackError)->Void)?) {
let parameters: [String: AnyObject?] = ["token": token, "test": test]
NetworkInterface().request(.AuthRevoke, parameters: filterNilParameters(parameters), successClosure: {
(response) -> Void in
success?(revoked:true)
}) {(error) -> Void in
failure?(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: History)->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]
networkInterface.request(.ChatDelete, token: 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? = nil, parse: ParseMode? = nil, linkNames: Bool? = nil, attachments: [Attachment?]? = nil, unfurlLinks: Bool? = nil, unfurlMedia: Bool? = nil, 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":encodeAttachments(attachments), "icon_url":iconURL, "icon_emoji":iconEmoji]
networkInterface.request(.ChatPostMessage, token: token, parameters: WebAPI.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: [Attachment?]? = 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":encodeAttachments(attachments)]
networkInterface.request(.ChatUpdate, token: token, parameters: WebAPI.filterNilParameters(parameters), successClosure: {
(response) -> Void in
success?(updated: true)
}) {(error) -> Void in
failure?(error: error)
}
}
//MARK: - Do Not Disturb
public func dndInfo(user: String? = nil, success: ((status: DoNotDisturbStatus)->Void)?, failure: FailureClosure?) {
let parameters: [String: AnyObject?] = ["user": user]
networkInterface.request(.DNDInfo, token: token, parameters: WebAPI.filterNilParameters(parameters), successClosure: {
(response) -> Void in
success?(status: DoNotDisturbStatus(status: response))
}) {(error) -> Void in
failure?(error: error)
}
}
public func dndTeamInfo(users: [String]? = nil, success: ((statuses: [String: DoNotDisturbStatus])->Void)?, failure: FailureClosure?) {
let parameters: [String: AnyObject?] = ["users":users?.joinWithSeparator(",")]
networkInterface.request(.DNDTeamInfo, token: token, parameters: WebAPI.filterNilParameters(parameters), successClosure: {
(response) -> Void in
guard let usersDictionary = response["users"] as? [String: AnyObject] else {
success?(statuses: [:])
return
}
success?(statuses: self.enumerateDNDStatuses(usersDictionary))
}) {(error) -> Void in
failure?(error: error)
}
}
//MARK: - Emoji
public func emojiList(success: ((emojiList: [String: AnyObject]?)->Void)?, failure: FailureClosure?) {
networkInterface.request(.EmojiList, token: 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]
networkInterface.request(.FilesDelete, token: token, parameters: parameters, successClosure: {
(response) -> Void in
success?(deleted: true)
}) {(error) -> Void in
failure?(error: error)
}
}
public func fileInfo(fileID: String, commentCount: Int = 100, totalPages: Int = 1, success: ((file: File)->Void)?, failure: FailureClosure?) {
let parameters: [String: AnyObject] = ["file":fileID, "count": commentCount, "totalPages":totalPages]
networkInterface.request(.FilesInfo, token: token, parameters: parameters, successClosure: {
(response) in
var file = File(file: response["file"] as? [String: AnyObject])
(response["comments"] as? [[String: AnyObject]])?.forEach { comment in
let comment = Comment(comment: comment)
if let id = comment.id {
file.comments[id] = comment
}
}
success?(file: file)
}) {(error) 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(",")]
networkInterface.uploadRequest(token, data: file, parameters: WebAPI.filterNilParameters(parameters), successClosure: {
(response) -> Void in
success?(file: File(file: response["file"] as? [String: AnyObject]))
}) {(error) -> Void in
failure?(error: error)
}
}
//MARK: - File Comments
public func addFileComment(fileID: String, comment: String, success: ((comment: Comment)->Void)?, failure: FailureClosure?) {
let parameters: [String: AnyObject] = ["file":fileID, "comment":comment.slackFormatEscaping()]
networkInterface.request(.FilesCommentsAdd, token: token, parameters: parameters, successClosure: {
(response) -> Void in
success?(comment: Comment(comment: response["comment"] as? [String: AnyObject]))
}) {(error) -> Void in
failure?(error: error)
}
}
public func editFileComment(fileID: String, commentID: String, comment: String, success: ((comment: Comment)->Void)?, failure: FailureClosure?) {
let parameters: [String: AnyObject] = ["file":fileID, "id":commentID, "comment":comment.slackFormatEscaping()]
networkInterface.request(.FilesCommentsEdit, token: token, parameters: parameters, successClosure: {
(response) -> Void in
success?(comment: Comment(comment: response["comment"] as? [String: AnyObject]))
}) {(error) -> Void in
failure?(error: error)
}
}
public func deleteFileComment(fileID: String, commentID: String, success: ((deleted: Bool?)->Void)?, failure: FailureClosure?) {
let parameters: [String: AnyObject] = ["file":fileID, "id": commentID]
networkInterface.request(.FilesCommentsDelete, token: token, parameters: parameters, successClosure: {
(response) -> Void in
success?(deleted: true)
}) {(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: History)->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]
networkInterface.request(.GroupsOpen, token: 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: History)->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]
networkInterface.request(.IMOpen, token: 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: History)->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(",")]
networkInterface.request(.MPIMOpen, token: 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: Endpoint, 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]
networkInterface.request(endpoint, token: token, parameters: WebAPI.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: Endpoint, 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]
networkInterface.request(endpoint, token: token, parameters: WebAPI.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: Endpoint, 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]
networkInterface.request(endpoint, token: token, parameters: WebAPI.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?) {
networkInterface.request(.TeamInfo, token: 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]
networkInterface.request(.UsersGetPresence, token: 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]
networkInterface.request(.UsersInfo, token: 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]
networkInterface.request(.UsersList, token: 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?) {
networkInterface.request(.UsersSetActive, token: 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]
networkInterface.request(.UsersSetPresence, token: token, parameters: parameters, successClosure: {
(response) -> Void in
success?(success:true)
}) {(error) -> Void in
failure?(error: error)
}
}
//MARK: - Channel Utilities
private func close(endpoint: Endpoint, channelID: String, success: ((closed: Bool)->Void)?, failure: FailureClosure?) {
let parameters: [String: AnyObject] = ["channel":channelID]
networkInterface.request(endpoint, token: token, parameters: parameters, successClosure: {
(response) -> Void in
success?(closed: true)
}) {(error) -> Void in
failure?(error: error)
}
}
private func history(endpoint: Endpoint, id: String, latest: String = "\(NSDate().timeIntervalSince1970)", oldest: String = "0", inclusive: Bool = false, count: Int = 100, unreads: Bool = false, success: ((history: History)->Void)?, failure: FailureClosure?) {
let parameters: [String: AnyObject] = ["channel": id, "latest": latest, "oldest": oldest, "inclusive":inclusive, "count":count, "unreads":unreads]
networkInterface.request(endpoint, token: token, parameters: parameters, successClosure: {
(response) -> Void in
success?(history: History(history: response))
}) {(error) -> Void in
failure?(error: error)
}
}
private func info(endpoint: Endpoint, type: ChannelType, id: String, success: ((channel: Channel)->Void)?, failure: FailureClosure?) {
let parameters: [String: AnyObject] = ["channel": id]
networkInterface.request(endpoint, token: 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: Endpoint, type: ChannelType, excludeArchived: Bool = false, success: ((channels: [[String: AnyObject]]?)->Void)?, failure: FailureClosure?) {
let parameters: [String: AnyObject] = ["exclude_archived": excludeArchived]
networkInterface.request(endpoint, token: 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: Endpoint, channel: String, timestamp: String, success: ((ts: String)->Void)?, failure: FailureClosure?) {
let parameters: [String: AnyObject] = ["channel": channel, "ts": timestamp]
networkInterface.request(endpoint, token: token, parameters: parameters, successClosure: {
(response) -> Void in
success?(ts: timestamp)
}) {(error) -> Void in
failure?(error: error)
}
}
private func setInfo(endpoint: Endpoint, type: InfoType, channel: String, text: String, success: ((success: Bool)->Void)?, failure: FailureClosure?) {
let parameters: [String: AnyObject] = ["channel": channel, type.rawValue: text]
networkInterface.request(endpoint, token: token, parameters: parameters, successClosure: {
(response) -> Void in
success?(success: true)
}) {(error) -> Void in
failure?(error: error)
}
}
//MARK: - Encode Attachments
private func encodeAttachments(attachments: [Attachment?]?) -> NSString? {
if let attachments = attachments {
var attachmentArray: [[String: AnyObject]] = []
for attachment in attachments {
if let attachment = attachment {
attachmentArray.append(attachment.dictionary())
}
}
do {
let data = try NSJSONSerialization.dataWithJSONObject(attachmentArray, options: [])
let string = NSString(data: data, encoding: NSUTF8StringEncoding)
return string
} catch _ {
}
}
return nil
}
//MARK: - Filter Nil Parameters
internal static func filterNilParameters(parameters: [String: AnyObject?]) -> [String: AnyObject] {
var finalParameters = [String: AnyObject]()
for (key, value) in parameters {
if let unwrapped = value {
finalParameters[key] = unwrapped
}
}
return finalParameters
}
//MARK: - Enumerate Do Not Disturb Status
private func enumerateDNDStatuses(statuses: [String: AnyObject]) -> [String: DoNotDisturbStatus] {
var retVal = [String: DoNotDisturbStatus]()
for key in statuses.keys {
retVal[key] = DoNotDisturbStatus(status: statuses[key] as? [String: AnyObject])
}
return retVal
}
}
@@ -34,7 +34,7 @@ internal struct WebhookRequest: Request {
let text: String
let responseURL: String
init(request: [String: Any]?) {
init(request: [String: AnyObject]?) {
token = request?["token"] as? String
teamID = request?["team_id"] as? String ?? ""
teamDomain = request?["team_domain"] as? String ?? ""
@@ -46,4 +46,5 @@ internal struct WebhookRequest: Request {
text = request?["text"] as? String ?? ""
responseURL = request?["response_url"] as? String ?? ""
}
}
@@ -21,29 +21,30 @@
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
open class WebhookServer: Server {
public class WebhookServer: Server {
public init(token: String, route: String, response: Response) {
super.init(token: token)
addRoute(route, response: response)
}
open func addRoute(_ route: String, response: Response) {
public func addRoute(route: String, response: Response) {
http["/\(route)"] = { request in
let webhookRequest = WebhookRequest(request: self.dictionaryFromRequest(request.body))
if webhookRequest.token == self.token {
return self.request(webhookRequest, reply: self.replyForResponse(response))
} else {
return .badRequest(.text("Bad request."))
return .BadRequest(.Text("Bad request."))
}
}
}
private func replyForResponse(_ response: Response) -> Reply {
private func replyForResponse(response: Response) -> Reply {
if response.attachments == nil && response.responseType == nil {
return Reply.text(body: response.text)
return Reply.Text(body: response.text)
} else {
return Reply.json(response: response)
return Reply.JSON(response: response)
}
}
}
+28
View File
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>2.3.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2016 Peter Zignego. All rights reserved.</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>
+28
View File
@@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>
<string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
<key>CFBundleInfoDictionaryVersion</key>
<string>6.0</string>
<key>CFBundleName</key>
<string>$(PRODUCT_NAME)</string>
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>2.3.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
<string>$(CURRENT_PROJECT_VERSION)</string>
<key>NSHumanReadableCopyright</key>
<string>Copyright © 2016 Peter Zignego. All rights reserved.</string>
<key>NSPrincipalClass</key>
<string></string>
</dict>
</plist>
@@ -15,7 +15,7 @@
<key>CFBundlePackageType</key>
<string>FMWK</string>
<key>CFBundleShortVersionString</key>
<string>3.1.12</string>
<string>2.3.0</string>
<key>CFBundleSignature</key>
<string>????</string>
<key>CFBundleVersion</key>
-562
View File
@@ -1,562 +0,0 @@
//
// Client+EventHandling.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 extension Client {
//MARK: - Pong
func pong(_ event: Event) {
pong = event.replyTo
}
//MARK: - Messages
func messageSent(_ event: Event) {
guard let reply = event.replyTo, let message = sentMessages[NSNumber(value: reply).stringValue], let channel = message.channel, let ts = message.ts else {
return
}
message.ts = event.ts
message.text = event.text
channels[channel]?.messages[ts] = message
messageEventsDelegate?.sent(message, client: self)
}
func messageReceived(_ event: Event) {
guard let channel = event.channel, let message = event.message, let id = channel.id, let ts = message.ts else {
return
}
channels[id]?.messages[ts] = message
messageEventsDelegate?.received(message, client:self)
}
func messageChanged(_ event: Event) {
guard let id = event.channel?.id, let nested = event.nestedMessage, let ts = nested.ts else {
return
}
channels[id]?.messages[ts] = nested
messageEventsDelegate?.changed(nested, client:self)
}
func messageDeleted(_ event: Event) {
guard let id = event.channel?.id, let key = event.message?.deletedTs, let message = channels[id]?.messages[key] else {
return
}
_ = channels[id]?.messages.removeValue(forKey: key)
messageEventsDelegate?.deleted(message, client:self)
}
//MARK: - Channels
func userTyping(_ event: Event) {
guard let channel = event.channel, let channelID = channel.id, let user = event.user, let userID = user.id ,
channels.index(forKey: channelID) != nil && !channels[channelID]!.usersTyping.contains(userID) else {
return
}
channels[channelID]?.usersTyping.append(userID)
channelEventsDelegate?.userTypingIn(channel, user: user, client: self)
let timeout = DispatchTime.now() + Double(Int64(5.0 * Double(NSEC_PER_SEC))) / Double(NSEC_PER_SEC)
DispatchQueue.main.asyncAfter(deadline: timeout, execute: {
if let index = self.channels[channelID]?.usersTyping.index(of: userID) {
self.channels[channelID]?.usersTyping.remove(at: index)
}
})
}
func channelMarked(_ event: Event) {
guard let channel = event.channel, let id = channel.id, let timestamp = event.ts else {
return
}
channels[id]?.lastRead = event.ts
channelEventsDelegate?.marked(channel, timestamp: timestamp, client: self)
}
func channelCreated(_ event: Event) {
guard let channel = event.channel, let id = channel.id else {
return
}
channels[id] = channel
channelEventsDelegate?.created(channel, client: self)
}
func channelDeleted(_ event: Event) {
guard let channel = event.channel, let id = channel.id else {
return
}
channels.removeValue(forKey: id)
channelEventsDelegate?.deleted(channel, client: self)
}
func channelJoined(_ event: Event) {
guard let channel = event.channel, let id = channel.id else {
return
}
channels[id] = event.channel
channelEventsDelegate?.joined(channel, client: self)
}
func channelLeft(_ event: Event) {
guard let channel = event.channel, let id = channel.id else {
return
}
if let userID = authenticatedUser?.id, let index = channels[id]?.members?.index(of: userID) {
channels[id]?.members?.remove(at: index)
}
channelEventsDelegate?.left(channel, client: self)
}
func channelRenamed(_ event: Event) {
guard let channel = event.channel, let id = channel.id else {
return
}
channels[id]?.name = channel.name
channelEventsDelegate?.renamed(channel, client: self)
}
func channelArchived(_ event: Event, archived: Bool) {
guard let channel = event.channel, let id = channel.id else {
return
}
channels[id]?.isArchived = archived
channelEventsDelegate?.archived(channel, client: self)
}
func channelHistoryChanged(_ event: Event) {
guard let channel = event.channel else {
return
}
channelEventsDelegate?.historyChanged(channel, client: self)
}
//MARK: - Do Not Disturb
func doNotDisturbUpdated(_ event: Event) {
guard let dndStatus = event.dndStatus else {
return
}
authenticatedUser?.doNotDisturbStatus = dndStatus
doNotDisturbEventsDelegate?.updated(dndStatus, client: self)
}
func doNotDisturbUserUpdated(_ event: Event) {
guard let dndStatus = event.dndStatus, let user = event.user, let id = user.id else {
return
}
users[id]?.doNotDisturbStatus = dndStatus
doNotDisturbEventsDelegate?.userUpdated(dndStatus, user: user, client: self)
}
//MARK: - IM & Group Open/Close
func open(_ event: Event, open: Bool) {
guard let channel = event.channel, let id = channel.id else {
return
}
channels[id]?.isOpen = open
groupEventsDelegate?.opened(channel, client: self)
}
//MARK: - Files
func processFile(_ event: Event) {
guard let file = event.file, let id = file.id else {
return
}
if let comment = file.initialComment, let commentID = comment.id {
if files[id]?.comments[commentID] == nil {
files[id]?.comments[commentID] = comment
}
}
files[id] = file
fileEventsDelegate?.processed(file, client: self)
}
func filePrivate(_ event: Event) {
guard let file = event.file, let id = file.id else {
return
}
files[id]?.isPublic = false
fileEventsDelegate?.madePrivate(file, client: self)
}
func deleteFile(_ event: Event) {
guard let file = event.file, let id = file.id else {
return
}
if files[id] != nil {
files.removeValue(forKey: id)
}
fileEventsDelegate?.deleted(file, client: self)
}
func fileCommentAdded(_ event: Event) {
guard let file = event.file, let id = file.id, let comment = event.comment, let commentID = comment.id else {
return
}
files[id]?.comments[commentID] = comment
fileEventsDelegate?.commentAdded(file, comment: comment, client: self)
}
func fileCommentEdited(_ event: Event) {
guard let file = event.file, let id = file.id, let comment = event.comment, let commentID = comment.id else {
return
}
files[id]?.comments[commentID]?.comment = comment.comment
fileEventsDelegate?.commentAdded(file, comment: comment, client: self)
}
func fileCommentDeleted(_ event: Event) {
guard let file = event.file, let id = file.id, let comment = event.comment, let commentID = comment.id else {
return
}
_ = files[id]?.comments.removeValue(forKey: commentID)
fileEventsDelegate?.commentDeleted(file, comment: comment, client: self)
}
//MARK: - Pins
func pinAdded(_ event: Event) {
guard let id = event.channelID, let item = event.item else {
return
}
channels[id]?.pinnedItems.append(item)
pinEventsDelegate?.pinned(item, channel: channels[id], client: self)
}
func pinRemoved(_ event: Event) {
guard let id = event.channelID, let item = event.item else {
return
}
if let pins = channels[id]?.pinnedItems.filter({$0 != item}) {
channels[id]?.pinnedItems = pins
}
pinEventsDelegate?.unpinned(item, channel: channels[id], client: self)
}
//MARK: - Stars
func itemStarred(_ event: Event, star: Bool) {
guard let item = event.item, let type = item.type else {
return
}
switch type {
case "message":
starMessage(item, star: star)
case "file":
starFile(item, star: star)
case "file_comment":
starComment(item)
default:
break
}
starEventsDelegate?.starred(item, starred: star, self)
}
func starMessage(_ item: Item, star: Bool) {
guard let message = item.message, let ts = message.ts, let channel = item.channel , channels[channel]?.messages[ts] != nil else {
return
}
channels[channel]?.messages[ts]?.isStarred = star
}
func starFile(_ item: Item, star: Bool) {
guard let file = item.file, let id = file.id else {
return
}
files[id]?.isStarred = star
if let stars = files[id]?.stars {
if star == true {
files[id]?.stars = stars + 1
} else {
if stars > 0 {
files[id]?.stars = stars - 1
}
}
}
}
func starComment(_ item: Item) {
guard let file = item.file, let id = file.id, let comment = item.comment, let commentID = comment.id else {
return
}
files[id]?.comments[commentID] = comment
}
//MARK: - Reactions
func addedReaction(_ event: Event) {
guard let item = event.item, let type = item.type, let reaction = event.reaction, let userID = event.user?.id, let itemUser = event.itemUser else {
return
}
switch type {
case "message":
guard let channel = item.channel, let ts = item.ts, let message = channels[channel]?.messages[ts] else {
return
}
message.reactions.append(Reaction(name: reaction, user: userID))
case "file":
guard let id = item.file?.id else {
return
}
files[id]?.reactions.append(Reaction(name: reaction, user: userID))
case "file_comment":
guard let id = item.file?.id, let commentID = item.fileCommentID else {
return
}
files[id]?.comments[commentID]?.reactions.append(Reaction(name: reaction, user: userID))
default:
break
}
reactionEventsDelegate?.added(reaction, item: item, itemUser: itemUser, client: self)
}
func removedReaction(_ event: Event) {
guard let item = event.item, let type = item.type, let key = event.reaction, let userID = event.user?.id, let itemUser = event.itemUser else {
return
}
switch type {
case "message":
guard let channel = item.channel, let ts = item.ts, let message = channels[channel]?.messages[ts] else {
return
}
message.reactions = message.reactions.filter({$0.name != key && $0.user != userID})
case "file":
guard let itemFile = item.file, let id = itemFile.id else {
return
}
files[id]?.reactions = files[id]!.reactions.filter({$0.name != key && $0.user != userID})
case "file_comment":
guard let id = item.file?.id, let commentID = item.fileCommentID else {
return
}
files[id]?.comments[commentID]?.reactions = files[id]!.comments[commentID]!.reactions.filter({$0.name != key && $0.user != userID})
default:
break
}
reactionEventsDelegate?.removed(key, item: item, itemUser: itemUser, client: self)
}
//MARK: - Preferences
func changePreference(_ event: Event) {
guard let name = event.name else {
return
}
authenticatedUser?.preferences?[name] = event.value
slackEventsDelegate?.preferenceChanged(name, value: event.value, client: self)
}
//Mark: - User Change
func userChange(_ event: Event) {
guard let user = event.user, let id = user.id else {
return
}
let preferences = users[id]?.preferences
users[id] = user
users[id]?.preferences = preferences
slackEventsDelegate?.userChanged(user, client: self)
}
//MARK: - User Presence
func presenceChange(_ event: Event) {
guard let user = event.user, let id = user.id, let presence = event.presence else {
return
}
users[id]?.presence = event.presence
slackEventsDelegate?.presenceChanged(user, presence: presence, client: self)
}
//MARK: - Team
func teamJoin(_ event: Event) {
guard let user = event.user, let id = user.id else {
return
}
users[id] = user
teamEventsDelegate?.userJoined(user, client: self)
}
func teamPlanChange(_ event: Event) {
guard let plan = event.plan else {
return
}
team?.plan = plan
teamEventsDelegate?.planChanged(plan, client: self)
}
func teamPreferenceChange(_ event: Event) {
guard let name = event.name else {
return
}
team?.prefs?[name] = event.value
teamEventsDelegate?.preferencesChanged(name, value: event.value, client: self)
}
func teamNameChange(_ event: Event) {
guard let name = event.name else {
return
}
team?.name = name
teamEventsDelegate?.nameChanged(name, client: self)
}
func teamDomainChange(_ event: Event) {
guard let domain = event.domain else {
return
}
team?.domain = domain
teamEventsDelegate?.domainChanged(domain, client: self)
}
func emailDomainChange(_ event: Event) {
guard let domain = event.emailDomain else {
return
}
team?.emailDomain = domain
teamEventsDelegate?.emailDomainChanged(domain, client: self)
}
func emojiChanged(_ event: Event) {
teamEventsDelegate?.emojiChanged(self)
}
//MARK: - Bots
func bot(_ event: Event) {
guard let bot = event.bot, let id = bot.id else {
return
}
bots[id] = bot
slackEventsDelegate?.botEvent(bot, client: self)
}
//MARK: - Subteams
func subteam(_ event: Event) {
guard let subteam = event.subteam, let id = subteam.id else {
return
}
userGroups[id] = subteam
subteamEventsDelegate?.event(subteam, client: self)
}
func subteamAddedSelf(_ event: Event) {
guard let subteamID = event.subteamID, let _ = authenticatedUser?.userGroups else {
return
}
authenticatedUser?.userGroups![subteamID] = subteamID
subteamEventsDelegate?.selfAdded(subteamID, client: self)
}
func subteamRemovedSelf(_ event: Event) {
guard let subteamID = event.subteamID else {
return
}
_ = authenticatedUser?.userGroups?.removeValue(forKey: subteamID)
subteamEventsDelegate?.selfRemoved(subteamID, client: self)
}
//MARK: - Team Profiles
func teamProfileChange(_ event: Event) {
guard let profile = event.profile else {
return
}
for user in users {
for key in profile.fields.keys {
users[user.0]?.profile?.customProfile?.fields[key]?.updateProfileField(profile.fields[key])
}
}
teamProfileEventsDelegate?.changed(profile, client: self)
}
func teamProfileDeleted(_ event: Event) {
guard let profile = event.profile else {
return
}
for user in users {
if let id = profile.fields.first?.0 {
users[user.0]?.profile?.customProfile?.fields[id] = nil
}
}
teamProfileEventsDelegate?.deleted(profile, client: self)
}
func teamProfileReordered(_ event: Event) {
guard let profile = event.profile else {
return
}
for user in users {
for key in profile.fields.keys {
users[user.0]?.profile?.customProfile?.fields[key]?.ordering = profile.fields[key]?.ordering
}
}
teamProfileEventsDelegate?.reordered(profile, client: self)
}
//MARK: - Authenticated User
func manualPresenceChange(_ event: Event) {
guard let presence = event.presence, let user = authenticatedUser else {
return
}
authenticatedUser?.presence = presence
slackEventsDelegate?.manualPresenceChanged(user, presence: presence, client: self)
}
}
-109
View File
@@ -1,109 +0,0 @@
//
// EventDelegate.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.
public protocol ConnectionEventsDelegate: class {
func connected(_ client: Client)
func disconnected(_ client: Client)
func connectionFailed(_ client: Client, error: SlackError)
}
public protocol MessageEventsDelegate: class {
func sent(_ message: Message, client: Client)
func received(_ message: Message, client: Client)
func changed(_ message: Message, client: Client)
func deleted(_ message: Message?, client: Client)
}
public protocol ChannelEventsDelegate: class {
func userTypingIn(_ channel: Channel, user: User, client: Client)
func marked(_ channel: Channel, timestamp: String, client: Client)
func created(_ channel: Channel, client: Client)
func deleted(_ channel: Channel, client: Client)
func renamed(_ channel: Channel, client: Client)
func archived(_ channel: Channel, client: Client)
func historyChanged(_ channel: Channel, client: Client)
func joined(_ channel: Channel, client: Client)
func left(_ channel: Channel, client: Client)
}
public protocol DoNotDisturbEventsDelegate: class {
func updated(_ status: DoNotDisturbStatus, client: Client)
func userUpdated(_ status: DoNotDisturbStatus, user: User, client: Client)
}
public protocol GroupEventsDelegate: class {
func opened(_ group: Channel, client: Client)
}
public protocol FileEventsDelegate: class {
func processed(_ file: File, client: Client)
func madePrivate(_ file: File, client: Client)
func deleted(_ file: File, client: Client)
func commentAdded(_ file: File, comment: Comment, client: Client)
func commentEdited(_ file: File, comment: Comment, client: Client)
func commentDeleted(_ file: File, comment: Comment, client: Client)
}
public protocol PinEventsDelegate: class {
func pinned(_ item: Item, channel: Channel?, client: Client)
func unpinned(_ item: Item, channel: Channel?, client: Client)
}
public protocol StarEventsDelegate: class {
func starred(_ item: Item, starred: Bool, _ client: Client)
}
public protocol ReactionEventsDelegate: class {
func added(_ reaction: String, item: Item, itemUser: String, client: Client)
func removed(_ reaction: String, item: Item, itemUser: String, client: Client)
}
public protocol SlackEventsDelegate: class {
func preferenceChanged(_ preference: String, value: Any?, client: Client)
func userChanged(_ user: User, client: Client)
func presenceChanged(_ user: User, presence: String, client: Client)
func manualPresenceChanged(_ user: User, presence: String, client: Client)
func botEvent(_ bot: Bot, client: Client)
}
public protocol TeamEventsDelegate: class {
func userJoined(_ user: User, client: Client)
func planChanged(_ plan: String, client: Client)
func preferencesChanged(_ preference: String, value: Any?, client: Client)
func nameChanged(_ name: String, client: Client)
func domainChanged(_ domain: String, client: Client)
func emailDomainChanged(_ domain: String, client: Client)
func emojiChanged(_ client: Client)
}
public protocol SubteamEventsDelegate: class {
func event(_ userGroup: UserGroup, client: Client)
func selfAdded(_ subteamID: String, client: Client)
func selfRemoved(_ subteamID: String, client: Client)
}
public protocol TeamProfileEventsDelegate: class {
func changed(_ profile: CustomProfile, client: Client)
func deleted(_ profile: CustomProfile, client: Client)
func reordered(_ profile: CustomProfile, client: Client)
}
-217
View File
@@ -1,217 +0,0 @@
//
// Event.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.
internal enum EventType: String {
case hello = "hello"
case message = "message"
case userTyping = "user_typing"
case channelMarked = "channel_marked"
case channelCreated = "channel_created"
case channelJoined = "channel_joined"
case channelLeft = "channel_left"
case channelDeleted = "channel_deleted"
case channelRenamed = "channel_rename"
case channelArchive = "channel_archive"
case channelUnarchive = "channel_unarchive"
case channelHistoryChanged = "channel_history_changed"
case dndUpdated = "dnd_updated"
case dndUpatedUser = "dnd_updated_user"
case imCreated = "im_created"
case imOpen = "im_open"
case imClose = "im_close"
case imMarked = "im_marked"
case imHistoryChanged = "im_history_changed"
case groupJoined = "group_joined"
case groupLeft = "group_left"
case groupOpen = "group_open"
case groupClose = "group_close"
case groupArchive = "group_archive"
case groupUnarchive = "group_unarchive"
case groupRename = "group_rename"
case groupMarked = "group_marked"
case groupHistoryChanged = "group_history_changed"
case fileCreated = "file_created"
case fileShared = "file_shared"
case fileUnshared = "file_unshared"
case filePublic = "file_public"
case filePrivate = "file_private"
case fileChanged = "file_change"
case fileDeleted = "file_deleted"
case fileCommentAdded = "file_comment_added"
case fileCommentEdited = "file_comment_edited"
case fileCommentDeleted = "file_comment_deleted"
case pinAdded = "pin_added"
case pinRemoved = "pin_removed"
case pong = "pong"
case presenceChange = "presence_change"
case manualPresenceChange = "manual_presence_change"
case prefChange = "pref_change"
case userChange = "user_change"
case teamJoin = "team_join"
case starAdded = "star_added"
case starRemoved = "star_removed"
case reactionAdded = "reaction_added"
case reactionRemoved = "reaction_removed"
case emojiChanged = "emoji_changed"
case commandsChanged = "commands_changed"
case teamPlanChange = "team_plan_change"
case teamPrefChange = "team_pref_change"
case teamRename = "team_rename"
case teamDomainChange = "team_domain_change"
case emailDomainChange = "email_domain_change"
case teamProfileChange = "team_profile_change"
case teamProfileDelete = "team_profile_delete"
case teamProfileReorder = "team_profile_reorder"
case botAdded = "bot_added"
case botChanged = "bot_changed"
case accountsChanged = "accounts_changed"
case teamMigrationStarted = "team_migration_started"
case reconnectURL = "reconnect_url"
case subteamCreated = "subteam_created"
case subteamUpdated = "subteam_updated"
case subteamSelfAdded = "subteam_self_added"
case subteamSelfRemoved = "subteam_self_removed"
case ok = "ok"
case error = "error"
case unknown = "unknown"
}
internal enum MessageSubtype: String {
case botMessage = "bot_message"
case meMessage = "me_message"
case messageChanged = "message_changed"
case messageDeleted = "message_deleted"
case channelJoin = "channel_join"
case channelLeave = "channel_leave"
case channelTopic = "channel_topic"
case channelPurpose = "channel_purpose"
case channelName = "channel_name"
case channelArchive = "channel_archive"
case channelUnarchive = "channel_unarchive"
case groupJoin = "group_join"
case groupLeave = "group_leave"
case groupTopic = "group_topic"
case groupPurpose = "group_purpose"
case groupName = "group_name"
case groupArchive = "group_archive"
case groupUnarchive = "group_unarchive"
case fileShare = "file_share"
case fileComment = "file_comment"
case fileMention = "file_mention"
case pinnedItem = "pinned_item"
case unpinnedItem = "unpinned_item"
}
internal class Event {
let type: EventType?
let ts: String?
let subtype: String?
let channelID: String?
let text: String?
let eventTs: String?
let latest: String?
let hidden: Bool?
let isStarred: Bool?
let hasPins: Bool?
let pinnedTo: [String]?
let fileID: String?
let presence: String?
let name: String?
let value: Any?
let plan: String?
let url: String?
let domain: String?
let emailDomain: String?
let reaction: String?
let replyTo: Double?
let reactions: [[String: Any]]?
let edited: Edited?
let bot: Bot?
let channel: Channel?
let comment: Comment?
let user: User?
let file: File?
let message: Message?
let nestedMessage: Message?
let itemUser: String?
let item: Item?
let dndStatus: DoNotDisturbStatus?
let subteam: UserGroup?
let subteamID: String?
var profile: CustomProfile?
init(_ event:[String: Any]) {
type = EventType(rawValue: event["type"] as? String ?? "ok")
ts = event["ts"] as? String
subtype = event["subtype"] as? String
channelID = event["channel_id"] as? String
text = event["text"] as? String
eventTs = event["event_ts"] as? String
latest = event["latest"] as? String
hidden = event["hidden"] as? Bool
isStarred = event["is_starred"] as? Bool
hasPins = event["has_pins"] as? Bool
pinnedTo = event["pinned_top"] as? [String]
fileID = event["file_id"] as? String
presence = event["presence"] as? String
name = event["name"] as? String
value = event["value"]
plan = event["plan"] as? String
url = event["url"] as? String
domain = event["domain"] as? String
emailDomain = event["email_domain"] as? String
reaction = event["reaction"] as? String
replyTo = event["reply_to"] as? Double
reactions = event["reactions"] as? [[String: Any]]
bot = Bot(bot: event["bot"] as? [String: Any])
edited = Edited(edited:event["edited"] as? [String: Any])
dndStatus = DoNotDisturbStatus(status: event["dnd_status"] as? [String: Any])
itemUser = event["item_user"] as? String
item = Item(item: event["item"] as? [String: Any])
subteam = UserGroup(userGroup: event["subteam"] as? [String: Any])
subteamID = event["subteam_id"] as? String
message = Message(dictionary: event)
nestedMessage = Message(dictionary: event["message"] as? [String: Any])
profile = CustomProfile(profile: event["profile"] as? [String: Any])
file = File(id: event["file"] as? String)
// Comment, Channel, and User can come across as Strings or Dictionaries
if let commentDictionary = event["comment"] as? [String: Any] {
comment = Comment(comment: commentDictionary)
} else {
comment = Comment(id: event["comment"] as? String)
}
if let userDictionary = event["user"] as? [String: Any] {
user = User(user: userDictionary)
} else {
user = User(id: event["user"] as? String)
}
if let channelDictionary = event["channel"] as? [String: Any] {
channel = Channel(channel: channelDictionary)
} else {
channel = Channel(id: event["channel"] as? String)
}
}
}
-121
View File
@@ -1,121 +0,0 @@
//
// SlackError.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.
public enum SlackError: String, Error {
case accountInactive = "account_inactive"
case alreadyArchived = "already_archived"
case alreadyInChannel = "already_in_channel"
case alreadyPinned = "already_pinned"
case alreadyReacted = "already_reacted"
case alreadyStarred = "already_starred"
case badClientSecret = "bad_client_secret"
case badRedirectURI = "bad_redirect_uri"
case badTimeStamp = "bad_timestamp"
case cantArchiveGeneral = "cant_archive_general"
case cantDelete = "cant_delete"
case cantDeleteFile = "cant_delete_file"
case cantDeleteMessage = "cant_delete_message"
case cantInvite = "cant_invite"
case cantInviteSelf = "cant_invite_self"
case cantKickFromGeneral = "cant_kick_from_general"
case cantKickFromLastChannel = "cant_kick_from_last_channel"
case cantKickSelf = "cant_kick_self"
case cantLeaveGeneral = "cant_leave_general"
case cantLeaveLastChannel = "cant_leave_last_channel"
case cantUpdateMessage = "cant_update_message"
case channelNotFound = "channel_not_found"
case complianceExportsPreventDeletion = "compliance_exports_prevent_deletion"
case editWindowClosed = "edit_window_closed"
case fileCommentNotFound = "file_comment_not_found"
case fileDeleted = "file_deleted"
case fileNotFound = "file_not_found"
case fileNotShared = "file_not_shared"
case groupContainsOthers = "group_contains_others"
case invalidArgName = "invalid_arg_name"
case invalidArrayArg = "invalid_array_arg"
case invalidAuth = "invalid_auth"
case invalidChannel = "invalid_channel"
case invalidCharSet = "invalid_charset"
case invalidClientID = "invalid_client_id"
case invalidCode = "invalid_code"
case invalidFormData = "invalid_form_data"
case invalidName = "invalid_name"
case invalidPostType = "invalid_post_type"
case invalidPresence = "invalid_presence"
case invalidTS = "invalid_timestamp"
case invalidTSLatest = "invalid_ts_latest"
case invalidTSOldest = "invalid_ts_oldest"
case isArchived = "is_archived"
case lastMember = "last_member"
case lastRAChannel = "last_ra_channel"
case messageNotFound = "message_not_found"
case messageTooLong = "msg_too_long"
case migrationInProgress = "migration_in_progress"
case missingDuration = "missing_duration"
case missingPostType = "missing_post_type"
case missingScope = "missing_scope"
case nameTaken = "name_taken"
case noChannel = "no_channel"
case noComment = "no_comment"
case noItemSpecified = "no_item_specified"
case noReaction = "no_reaction"
case noText = "no_text"
case notArchived = "not_archived"
case notAuthed = "not_authed"
case notEnoughUsers = "not_enough_users"
case notInChannel = "not_in_channel"
case notInGroup = "not_in_group"
case notPinned = "not_pinned"
case notStarred = "not_starred"
case overPaginationLimit = "over_pagination_limit"
case paidOnly = "paid_only"
case permissionDenied = "perimssion_denied"
case postingToGeneralChannelDenied = "posting_to_general_channel_denied"
case rateLimited = "rate_limited"
case requestTimeout = "request_timeout"
case restrictedAction = "restricted_action"
case snoozeEndFailed = "snooze_end_failed"
case snoozeFailed = "snooze_failed"
case snoozeNotActive = "snooze_not_active"
case tooLong = "too_long"
case tooManyEmoji = "too_many_emoji"
case tooManyReactions = "too_many_reactions"
case tooManyUsers = "too_many_users"
case unknownError
case unknownType = "unknown_type"
case userDisabled = "user_disabled"
case userDoesNotOwnChannel = "user_does_not_own_channel"
case userIsBot = "user_is_bot"
case userIsRestricted = "user_is_restricted"
case userIsUltraRestricted = "user_is_ultra_restricted"
case userListNotSupplied = "user_list_not_supplied"
case userNotFound = "user_not_found"
case userNotVisible = "user_not_visible"
// Client
case clientNetworkError
case clientJSONError
case clientOAuthError
// HTTP
case tooManyRequests
case unknownHTTPError
}
-156
View File
@@ -1,156 +0,0 @@
//
// 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: Endpoint, parameters: [String: Any?], successClosure: @escaping ([String: Any])->Void, errorClosure: @escaping (SlackError)->Void) {
var components = URLComponents(string: "\(apiUrl)\(endpoint.rawValue)")
if parameters.count > 0 {
components?.queryItems = filterNilParameters(parameters).map { URLQueryItem(name: $0.0, value: "\($0.1)") }
}
guard let url = components?.url else {
errorClosure(SlackError.clientNetworkError)
return
}
let request = URLRequest(url: url)
URLSession.shared.dataTask(with: request) {(data, response, internalError) in
do {
successClosure(try self.handleResponse(data, response: response, internalError: internalError))
} catch let error {
errorClosure(error as? SlackError ?? SlackError.unknownError)
}
}.resume()
}
internal func customRequest(_ url: 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"
request.setValue(contentType, forHTTPHeaderField: "Content-Type")
request.httpBody = data
URLSession.shared.dataTask(with: request) {(data, response, internalError) in
if internalError == nil {
success(true)
} else {
errorClosure(SlackError.clientNetworkError)
}
}.resume()
}
internal func uploadRequest(data: Data, parameters: [String: Any?], successClosure: @escaping ([String: Any])->Void, errorClosure: @escaping (SlackError)->Void) {
var components = URLComponents(string: "\(apiUrl)\(Endpoint.filesUpload.rawValue)")
if parameters.count > 0 {
components?.queryItems = filterNilParameters(parameters).map { URLQueryItem(name: $0.0, value: "\($0.1)") }
}
guard let url = components?.url, 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
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"
var requestBodyData: Data = Data()
requestBodyData.append(boundaryStart.data(using: String.Encoding.utf8)!)
requestBodyData.append(contentDispositionString.data(using: String.Encoding.utf8)!)
requestBodyData.append(contentTypeString.data(using: String.Encoding.utf8)!)
requestBodyData.append(data)
requestBodyData.append("\r\n".data(using: String.Encoding.utf8)!)
requestBodyData.append(boundaryEnd.data(using: String.Encoding.utf8)!)
request.setValue(contentType, forHTTPHeaderField: "Content-Type")
request.httpBody = requestBodyData as Data
URLSession.shared.dataTask(with: request) {(data, response, internalError) in
do {
successClosure(try self.handleResponse(data, response: response, internalError: internalError))
} catch let error {
errorClosure(error as? SlackError ?? SlackError.unknownError)
}
}.resume()
}
private func handleResponse(_ data: Data?, response:URLResponse?, internalError: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 randomBoundary() -> String {
return String(format: "slackkit.boundary.%08x%08x", arc4random(), arc4random())
}
//MARK: - Filter Nil Parameters
private func filterNilParameters(_ parameters: [String: Any?]) -> [String: Any] {
var finalParameters = [String: Any]()
for (key, value) in parameters {
if let unwrapped = value {
finalParameters[key] = unwrapped
}
}
return finalParameters
}
}
-707
View File
@@ -1,707 +0,0 @@
//
// WebAPI.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 Endpoint: String {
case apiTest = "api.test"
case authRevoke = "auth.revoke"
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 chatMeMessage = "chat.meMessage"
case chatUpdate = "chat.update"
case dndInfo = "dnd.info"
case dndTeamInfo = "dnd.teamInfo"
case emojiList = "emoji.list"
case filesCommentsAdd = "files.comments.add"
case filesCommentsEdit = "files.comments.edit"
case filesCommentsDelete = "files.comments.delete"
case filesDelete = "files.delete"
case filesInfo = "files.info"
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 oauthAccess = "oauth.access"
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 final class WebAPI {
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 networkInterface: NetworkInterface
private let token: String
public init(token: String) {
self.networkInterface = NetworkInterface()
self.token = token
}
//MARK: - RTM
public func rtmStart(simpleLatest: Bool? = nil, noUnreads: Bool? = nil, mpimAware: Bool? = nil, success: ((_ response: [String: Any])->Void)?, failure: FailureClosure?) {
let parameters: [String: Any?] = ["token": token, "simple_latest": simpleLatest, "no_unreads": noUnreads, "mpim_aware": mpimAware]
networkInterface.request(.rtmStart, parameters: parameters, successClosure: {(response) in
success?(response)
}) {(error) in
failure?(error)
}
}
//MARK: - Auth
public func authenticationTest(success: ((_ authenticated: Bool)->Void)?, failure: FailureClosure?) {
networkInterface.request(.authTest, parameters: ["token": token], successClosure: {(response) in
success?(true)
}) {(error) in
failure?(error)
}
}
public static func oauthAccess(clientID: String, clientSecret: String, code: String, redirectURI: String? = nil, success: ((_ response: [String: Any])->Void)?, failure: ((SlackError)->Void)?) {
let parameters: [String: Any?] = ["client_id": clientID, "client_secret": clientSecret, "code": code, "redirect_uri": redirectURI]
NetworkInterface().request(.oauthAccess, parameters: parameters, successClosure: {(response) in
success?(response)
}) {(error) in
failure?(error)
}
}
public static func oauthRevoke(token: String, test: Bool? = nil, success: ((_ revoked:Bool)->Void)?, failure: ((SlackError)->Void)?) {
let parameters: [String: Any?] = ["token": token, "test": test]
NetworkInterface().request(.authRevoke, parameters: parameters, successClosure: {(response) in
success?(true)
}) {(error) in
failure?(error)
}
}
//MARK: - Channels
public func channelHistory(id: String, latest: String = "\(Date().timeIntervalSince1970)", oldest: String = "0", inclusive: Bool = false, count: Int = 100, unreads: Bool = false, success: ((_ history: History)->Void)?, failure: FailureClosure?) {
history(.channelsHistory, id: id, latest: latest, oldest: oldest, inclusive: inclusive, count: count, unreads: unreads, success: {(history) in
success?(history)
}) {(error) in
failure?(error)
}
}
public func channelInfo(id: String, success: ((_ channel: Channel)->Void)?, failure: FailureClosure?) {
info(.channelsInfo, type:.channel, id: id, success: {(channel) in
success?(channel)
}) {(error) in
failure?(error)
}
}
public func channelsList(excludeArchived: Bool = false, success: ((_ channels: [[String: Any]]?)->Void)?, failure: FailureClosure?) {
list(.channelsList, type:.channel, excludeArchived: excludeArchived, success: {(channels) in
success?(channels)
}) {(error) in
failure?(error)
}
}
public func markChannel(channel: String, timestamp: String, success: ((_ ts: String)->Void)?, failure: FailureClosure?) {
mark(.channelsMark, channel: channel, timestamp: timestamp, success: {(ts) in
success?(timestamp)
}) {(error) in
failure?(error)
}
}
public func setChannelPurpose(channel: String, purpose: String, success: ((_ purposeSet: Bool)->Void)?, failure: FailureClosure?) {
setInfo(.channelsSetPurpose, type: .purpose, channel: channel, text: purpose, success: {(purposeSet) in
success?(purposeSet)
}) {(error) in
failure?(error)
}
}
public func setChannelTopic(channel: String, topic: String, success: ((_ topicSet: Bool)->Void)?, failure: FailureClosure?) {
setInfo(.channelsSetTopic, type: .topic, channel: channel, text: topic, success: {(topicSet) in
success?(topicSet)
}) {(error) in
failure?(error)
}
}
//MARK: - Messaging
public func deleteMessage(channel: String, ts: String, success: ((_ deleted: Bool)->Void)?, failure: FailureClosure?) {
let parameters: [String: Any] = ["token": token, "channel": channel, "ts": ts]
networkInterface.request(.chatDelete, parameters: parameters, successClosure: {(response) in
success?(true)
}) {(error) in
failure?(error)
}
}
public func sendMessage(channel: String, text: String, username: String? = nil, asUser: Bool? = nil, parse: ParseMode? = nil, linkNames: Bool? = nil, attachments: [Attachment?]? = nil, unfurlLinks: Bool? = nil, unfurlMedia: Bool? = nil, iconURL: String? = nil, iconEmoji: String? = nil, success: (((ts: String?, channel: String?))->Void)?, failure: FailureClosure?) {
let parameters: [String: Any?] = ["token": token, "channel": channel, "text": text.slackFormatEscaping, "as_user": asUser, "parse": parse?.rawValue, "link_names": linkNames, "unfurl_links": unfurlLinks, "unfurlMedia": unfurlMedia, "username": username, "icon_url": iconURL, "icon_emoji": iconEmoji, "attachments": encodeAttachments(attachments)]
networkInterface.request(.chatPostMessage, parameters: parameters, successClosure: {(response) in
success?((ts: response["ts"] as? String, response["channel"] as? String))
}) {(error) in
failure?(error)
}
}
public func sendMeMessage(channel: String, text: String, success: (((ts: String?, channel: String?))->Void)?, failure: FailureClosure?) {
let parameters: [String: Any?] = ["token": token, "channel": channel, "text": text.slackFormatEscaping]
networkInterface.request(.chatMeMessage, parameters: parameters, successClosure: {(response) in
success?((ts: response["ts"] as? String, response["channel"] as? String))
}) {(error) in
failure?(error)
}
}
public func updateMessage(channel: String, ts: String, message: String, attachments: [Attachment?]? = nil, parse:ParseMode = .none, linkNames: Bool = false, success: ((_ updated: Bool)->Void)?, failure: FailureClosure?) {
let parameters: [String: Any?] = ["token": token, "channel": channel, "ts": ts, "text": message.slackFormatEscaping, "parse": parse.rawValue, "link_names": linkNames, "attachments": encodeAttachments(attachments)]
networkInterface.request(.chatUpdate, parameters: parameters, successClosure: {(response) in
success?(true)
}) {(error) in
failure?(error)
}
}
//MARK: - Do Not Disturb
public func dndInfo(user: String? = nil, success: ((_ status: DoNotDisturbStatus)->Void)?, failure: FailureClosure?) {
let parameters: [String: Any?] = ["token": token, "user": user]
networkInterface.request(.dndInfo, parameters: parameters, successClosure: {(response) in
success?(DoNotDisturbStatus(status: response))
}) {(error) in
failure?(error)
}
}
public func dndTeamInfo(users: [String]? = nil, success: ((_ statuses: [String: DoNotDisturbStatus])->Void)?, failure: FailureClosure?) {
let parameters: [String: Any?] = ["token": token, "users": users?.joined(separator: ",")]
networkInterface.request(.dndTeamInfo, parameters: parameters, successClosure: {(response) in
guard let usersDictionary = response["users"] as? [String: Any] else {
success?([:])
return
}
success?(self.enumerateDNDStatuses(usersDictionary))
}) {(error) in
failure?(error)
}
}
//MARK: - Emoji
public func emojiList(success: ((_ emojiList: [String: Any]?)->Void)?, failure: FailureClosure?) {
networkInterface.request(.emojiList, parameters: ["token": token], successClosure: {(response) in
success?(response["emoji"] as? [String: Any])
}) {(error) in
failure?(error)
}
}
//MARK: - Files
public func deleteFile(fileID: String, success: ((_ deleted: Bool)->Void)?, failure: FailureClosure?) {
let parameters = ["token": token, "file": fileID]
networkInterface.request(.filesDelete, parameters: parameters, successClosure: {(response) in
success?(true)
}) {(error) in
failure?(error)
}
}
public func fileInfo(fileID: String, commentCount: Int = 100, totalPages: Int = 1, success: ((_ file: File)->Void)?, failure: FailureClosure?) {
let parameters: [String: Any] = ["token": token, "file": fileID, "count": commentCount, "totalPages": totalPages]
networkInterface.request(.filesInfo, parameters: parameters, successClosure: {(response) in
var file = File(file: response["file"] as? [String: Any])
(response["comments"] as? [[String: Any]])?.forEach { comment in
let comment = Comment(comment: comment)
if let id = comment.id {
file.comments[id] = comment
}
}
success?(file)
}) {(error) in
failure?(error)
}
}
public func uploadFile(file: Data, filename: String, filetype: String = "auto", title: String? = nil, initialComment: String? = nil, channels: [String]? = nil, success: ((_ file: File)->Void)?, failure: FailureClosure?) {
let parameters: [String: Any?] = ["token": token, "filename": filename, "filetype": filetype, "title": title, "initial_comment": initialComment, "channels": channels?.joined(separator: ",")]
networkInterface.uploadRequest(data: file, parameters: parameters, successClosure: {(response) in
success?(File(file: response["file"] as? [String: Any]))
}) {(error) in
failure?(error)
}
}
//MARK: - File Comments
public func addFileComment(fileID: String, comment: String, success: ((_ comment: Comment)->Void)?, failure: FailureClosure?) {
let parameters: [String: Any] = ["token": token, "file": fileID, "comment": comment.slackFormatEscaping]
networkInterface.request(.filesCommentsAdd, parameters: parameters, successClosure: {(response) in
success?(Comment(comment: response["comment"] as? [String: Any]))
}) {(error) in
failure?(error)
}
}
public func editFileComment(fileID: String, commentID: String, comment: String, success: ((_ comment: Comment)->Void)?, failure: FailureClosure?) {
let parameters: [String: Any] = ["token": token, "file": fileID, "id": commentID, "comment": comment.slackFormatEscaping]
networkInterface.request(.filesCommentsEdit, parameters: parameters, successClosure: {(response) in
success?(Comment(comment: response["comment"] as? [String: Any]))
}) {(error) in
failure?(error)
}
}
public func deleteFileComment(fileID: String, commentID: String, success: ((_ deleted: Bool?)->Void)?, failure: FailureClosure?) {
let parameters: [String: Any] = ["token": token, "file": fileID, "id": commentID]
networkInterface.request(.filesCommentsDelete, parameters: parameters, successClosure: {(response) in
success?(true)
}) {(error) in
failure?(error)
}
}
//MARK: - Groups
public func closeGroup(groupID: String, success: ((_ closed: Bool)->Void)?, failure: FailureClosure?) {
close(.groupsClose, channelID: groupID, success: {(closed) in
success?(closed)
}) {(error) in
failure?(error)
}
}
public func groupHistory(id: String, latest: String = "\(Date().timeIntervalSince1970)", oldest: String = "0", inclusive: Bool = false, count: Int = 100, unreads: Bool = false, success: ((_ history: History)->Void)?, failure: FailureClosure?) {
history(.groupsHistory, id: id, latest: latest, oldest: oldest, inclusive: inclusive, count: count, unreads: unreads, success: {(history) in
success?(history)
}) {(error) in
failure?(error)
}
}
public func groupInfo(id: String, success: ((_ channel: Channel)->Void)?, failure: FailureClosure?) {
info(.groupsInfo, type:.group, id: id, success: {(channel) in
success?(channel)
}) {(error) in
failure?(error)
}
}
public func groupsList(excludeArchived: Bool = false, success: ((_ channels: [[String: Any]]?)->Void)?, failure: FailureClosure?) {
list(.groupsList, type:.group, excludeArchived: excludeArchived, success: {(channels) in
success?(channels)
}) {(error) in
failure?(error)
}
}
public func markGroup(channel: String, timestamp: String, success: ((_ ts: String)->Void)?, failure: FailureClosure?) {
mark(.groupsMark, channel: channel, timestamp: timestamp, success: {(ts) in
success?(timestamp)
}) {(error) in
failure?(error)
}
}
public func openGroup(channel: String, success: ((_ opened: Bool)->Void)?, failure: FailureClosure?) {
let parameters = ["token": token, "channel":channel]
networkInterface.request(.groupsOpen, parameters: parameters, successClosure: {(response) in
success?(true)
}) {(error) in
failure?(error)
}
}
public func setGroupPurpose(channel: String, purpose: String, success: ((_ purposeSet: Bool)->Void)?, failure: FailureClosure?) {
setInfo(.groupsSetPurpose, type: .purpose, channel: channel, text: purpose, success: {(purposeSet) in
success?(purposeSet)
}) {(error) in
failure?(error)
}
}
public func setGroupTopic(channel: String, topic: String, success: ((_ topicSet: Bool)->Void)?, failure: FailureClosure?) {
setInfo(.groupsSetTopic, type: .topic, channel: channel, text: topic, success: {(topicSet) in
success?(topicSet)
}) {(error) in
failure?(error)
}
}
//MARK: - IM
public func closeIM(channel: String, success: ((_ closed: Bool)->Void)?, failure: FailureClosure?) {
close(.imClose, channelID: channel, success: {(closed) in
success?(closed)
}) {(error) in
failure?(error)
}
}
public func imHistory(id: String, latest: String = "\(Date().timeIntervalSince1970)", oldest: String = "0", inclusive: Bool = false, count: Int = 100, unreads: Bool = false, success: ((_ history: History)->Void)?, failure: FailureClosure?) {
history(.imHistory, id: id, latest: latest, oldest: oldest, inclusive: inclusive, count: count, unreads: unreads, success: {(history) in
success?(history)
}) {(error) in
failure?(error)
}
}
public func imsList(excludeArchived: Bool = false, success: ((_ channels: [[String: Any]]?)->Void)?, failure: FailureClosure?) {
list(.imList, type:.im, excludeArchived: excludeArchived, success: {(channels) in
success?(channels)
}) {(error) in
failure?(error)
}
}
public func markIM(channel: String, timestamp: String, success: ((_ ts: String)->Void)?, failure: FailureClosure?) {
mark(.imMark, channel: channel, timestamp: timestamp, success: {(ts) in
success?(timestamp)
}) {(error) in
failure?(error)
}
}
public func openIM(userID: String, success: ((_ imID: String?)->Void)?, failure: FailureClosure?) {
let parameters = ["token": token, "user": userID]
networkInterface.request(.imOpen, parameters: parameters, successClosure: {(response) in
let group = response["channel"] as? [String: Any]
success?(group?["id"] as? String)
}) {(error) in
failure?(error)
}
}
//MARK: - MPIM
public func closeMPIM(channel: String, success: ((_ closed: Bool)->Void)?, failure: FailureClosure?) {
close(.mpimClose, channelID: channel, success: {(closed) in
success?(closed)
}) {(error) in
failure?(error)
}
}
public func mpimHistory(id: String, latest: String = "\(Date().timeIntervalSince1970)", oldest: String = "0", inclusive: Bool = false, count: Int = 100, unreads: Bool = false, success: ((_ history: History)->Void)?, failure: FailureClosure?) {
history(.mpimHistory, id: id, latest: latest, oldest: oldest, inclusive: inclusive, count: count, unreads: unreads, success: {(history) in
success?(history)
}) {(error) in
failure?(error)
}
}
public func mpimsList(excludeArchived: Bool = false, success: ((_ channels: [[String: Any]]?)->Void)?, failure: FailureClosure?) {
list(.mpimList, type:.group, excludeArchived: excludeArchived, success: {(channels) in
success?(channels)
}) {(error) in
failure?(error)
}
}
public func markMPIM(channel: String, timestamp: String, success: ((_ ts: String)->Void)?, failure: FailureClosure?) {
mark(.mpimMark, channel: channel, timestamp: timestamp, success: {(ts) in
success?(timestamp)
}) {(error) in
failure?(error)
}
}
public func openMPIM(userIDs: [String], success: ((_ mpimID: String?)->Void)?, failure: FailureClosure?) {
let parameters = ["token": token, "users": userIDs.joined(separator: ",")]
networkInterface.request(.mpimOpen, parameters: parameters, successClosure: {(response) in
let group = response["group"] as? [String: Any]
success?(group?["id"] as? String)
}) {(error) in
failure?(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) in
success?(ok)
}) {(error) in
failure?(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) in
success?(ok)
}) {(error) in
failure?(error)
}
}
private func pin(_ endpoint: Endpoint, channel: String, file: String? = nil, fileComment: String? = nil, timestamp: String? = nil, success: ((_ ok: Bool)->Void)?, failure: FailureClosure?) {
let parameters: [String: Any?] = ["token": token, "channel": channel, "file": file, "file_comment": fileComment, "timestamp": timestamp]
networkInterface.request(endpoint, parameters: parameters, successClosure: {(response) in
success?(true)
}) {(error) in
failure?(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) in
success?(ok)
}) {(error) in
failure?(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) in
success?(ok)
}) {(error) in
failure?(error)
}
}
private func react(_ endpoint: Endpoint, name: String, file: String? = nil, fileComment: String? = nil, channel: String? = nil, timestamp: String? = nil, success: ((_ ok: Bool)->Void)?, failure: FailureClosure?) {
let parameters: [String: Any?] = ["token": token, "name": name, "file": file, "file_comment": fileComment, "channel": channel, "timestamp": timestamp]
networkInterface.request(endpoint, parameters: parameters, successClosure: {(response) in
success?(true)
}) {(error) in
failure?(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) in
success?(ok)
}) {(error) in
failure?(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) in
success?(ok)
}) {(error) in
failure?(error)
}
}
private func star(_ endpoint: Endpoint, file: String?, fileComment: String?, channel: String?, timestamp: String?, success: ((_ ok: Bool)->Void)?, failure: FailureClosure?) {
let parameters: [String: Any?] = ["token": token, "file": file, "file_comment": fileComment, "channel": channel, "timestamp": timestamp]
networkInterface.request(endpoint, parameters: parameters, successClosure: {(response) in
success?(true)
}) {(error) in
failure?(error)
}
}
//MARK: - Team
public func teamInfo(success: ((_ info: [String: Any]?)->Void)?, failure: FailureClosure?) {
networkInterface.request(.teamInfo, parameters: ["token": token], successClosure: {(response) in
success?(response["team"] as? [String: Any])
}) {(error) in
failure?(error)
}
}
//MARK: - Users
public func userPresence(user: String, success: ((_ presence: String?)->Void)?, failure: FailureClosure?) {
let parameters: [String: Any] = ["token": token, "user": user]
networkInterface.request(.usersGetPresence, parameters: parameters, successClosure: {(response) in
success?(response["presence"] as? String)
}) {(error) in
failure?(error)
}
}
public func userInfo(id: String, success: ((_ user: User)->Void)?, failure: FailureClosure?) {
let parameters: [String: Any] = ["token": token, "user": id]
networkInterface.request(.usersInfo, parameters: parameters, successClosure: {(response) in
success?(User(user: response["user"] as? [String: Any]))
}) {(error) in
failure?(error)
}
}
public func usersList(includePresence: Bool = false, success: ((_ userList: [[String: Any]]?)->Void)?, failure: FailureClosure?) {
let parameters: [String: Any] = ["token": token, "presence": includePresence]
networkInterface.request(.usersList, parameters: parameters, successClosure: {(response) in
success?(response["members"] as? [[String: Any]])
}) {(error) in
failure?(error)
}
}
public func setUserActive(success: ((_ success: Bool)->Void)?, failure: FailureClosure?) {
networkInterface.request(.usersSetActive, parameters: ["token": token], successClosure: {(response) in
success?(true)
}) {(error) in
failure?(error)
}
}
public func setUserPresence(presence: Presence, success: ((_ success: Bool)->Void)?, failure: FailureClosure?) {
let parameters: [String: Any] = ["token": token, "presence": presence.rawValue]
networkInterface.request(.usersSetPresence, parameters: parameters, successClosure: {(response) in
success?(true)
}) {(error) in
failure?(error)
}
}
//MARK: - Channel Utilities
private func close(_ endpoint: Endpoint, channelID: String, success: ((_ closed: Bool)->Void)?, failure: FailureClosure?) {
let parameters: [String: Any] = ["token": token, "channel": channelID]
networkInterface.request(endpoint, parameters: parameters, successClosure: {(response) in
success?(true)
}) {(error) in
failure?(error)
}
}
private func history(_ endpoint: Endpoint, id: String, latest: String = "\(Date().timeIntervalSince1970)", oldest: String = "0", inclusive: Bool = false, count: Int = 100, unreads: Bool = false, success: ((_ history: History)->Void)?, failure: FailureClosure?) {
let parameters: [String: Any] = ["token": token, "channel": id, "latest": latest, "oldest": oldest, "inclusive": inclusive, "count": count, "unreads": unreads]
networkInterface.request(endpoint, parameters: parameters, successClosure: {(response) in
success?(History(history: response))
}) {(error) in
failure?(error)
}
}
private func info(_ endpoint: Endpoint, type: ChannelType, id: String, success: ((_ channel: Channel)->Void)?, failure: FailureClosure?) {
let parameters: [String: Any] = ["token": token, "channel": id]
networkInterface.request(endpoint, parameters: parameters, successClosure: {(response) in
success?(Channel(channel: response[type.rawValue] as? [String: Any]))
}) {(error) in
failure?(error)
}
}
private func list(_ endpoint: Endpoint, type: ChannelType, excludeArchived: Bool = false, success: ((_ channels: [[String: Any]]?)->Void)?, failure: FailureClosure?) {
let parameters: [String: Any] = ["token": token, "exclude_archived": excludeArchived]
networkInterface.request(endpoint, parameters: parameters, successClosure: {(response) in
success?(response[type.rawValue+"s"] as? [[String: Any]])
}) {(error) in
failure?(error)
}
}
private func mark(_ endpoint: Endpoint, channel: String, timestamp: String, success: ((_ ts: String)->Void)?, failure: FailureClosure?) {
let parameters: [String: Any] = ["token": token, "channel": channel, "ts": timestamp]
networkInterface.request(endpoint, parameters: parameters, successClosure: {(response) in
success?(timestamp)
}) {(error) in
failure?(error)
}
}
fileprivate func setInfo(_ endpoint: Endpoint, type: InfoType, channel: String, text: String, success: ((_ success: Bool)->Void)?, failure: FailureClosure?) {
let parameters: [String: Any] = ["token": token, "channel": channel, type.rawValue: text]
networkInterface.request(endpoint, parameters: parameters, successClosure: {(response) in
success?(true)
}) {(error) in
failure?(error)
}
}
//MARK: - Encode Attachments
private func encodeAttachments(_ attachments: [Attachment?]?) -> String? {
if let attachments = attachments {
var attachmentArray: [[String: Any]] = []
for attachment in attachments {
if let attachment = attachment {
attachmentArray.append(attachment.dictionary)
}
}
do {
let data = try JSONSerialization.data(withJSONObject: attachmentArray, options: [])
return String(data: data, encoding: String.Encoding.utf8)
} catch _ {
}
}
return nil
}
//MARK: - Enumerate Do Not Disturb Status
private func enumerateDNDStatuses(_ statuses: [String: Any]) -> [String: DoNotDisturbStatus] {
var retVal = [String: DoNotDisturbStatus]()
for key in statuses.keys {
retVal[key] = DoNotDisturbStatus(status: statuses[key] as? [String: Any])
}
return retVal
}
}