Hey all { }>
In case you are familiar with Slack and what is it used for, then you can skip the introduction.
For the ones who have no idea what Slack is:
Everybody uses Facebook and Workplace in their daily routine. Thus, majority of you must know how both these platforms work. In case, you are working in a software development company, then there might be a few more platforms that you use on daily basis. Some of these can be Github, Bitbucket, Zoho, InVision, etc. Now let’s say when you post something on Zoho and then posted something different on Workplace. To keep track of these posts, you have to keep visiting these platforms.
What if all posts and notifications of Github, Zoho, InVision etc. can be seen at one place?
Well, yes that is possible with Slack. Slack lets you create various channels for communication and integrate Github, Zoho, InVision etc. to your account and integrate slack apps of Github, Zoho, InVision etc.
Let us understand this with an example. To integrate Github account with slack channel, select the repositories. Once you have integrated it, you will start receiving notifications about activities like push, pull, clone etc. happening in your repositories.
Let’s start by creating a new Xcode project and then install the following libraries.
And we are done with setting up with our project. Now, go to https://slack.com/ . Sign in using your workplace account YOUR_WORKPLACE_NAME.slack.com or create a new workplace account on Slack and invite members on Slack via emails.
Now Create a simple UI like the one given below. We are not focusing on the UI because this blog is about Slack Integration.
Now that we are done with the UI, let’s start dragging.
i.e. Ctrl + Click + Drag = IBOutlets or IBActions
CAUTION: Applicable only for iOS Developers.
Here is the small code snippet for UI explained above.
import UIKit class ViewController: UIViewController { @IBOutlet weak var textField: UITextField! @IBOutlet weak var responseView: UITextView! override func viewDidLoad() { super.viewDidLoad() textField.delegate = self } override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) { textField.resignFirstResponder() } } //MARK:- UITextFieldDelegate extension ViewController: UITextFieldDelegate { func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.resignFirstResponder() return true } }
We will access Slack channel IDs of Slack account and then create a bot that will send message to channels as ANONYMOUS user.
Login to your account YOUR_WORKPLACE_NAME.slack.com and then click on your USER_NAME on top left corner and then click on MANAGE_APPS.
A new window will open where you can search for bots and then click on Bots.
On the next screen click on Add Configuration.
Write your chat bot name and click on Add bot integration.
Make sure you remember the bot name and API token of bot which is generated after bot integration and click on Save Integration. Now, create a file named SlackConstants.swift in your project and create an enum for slack IDs that are required later in this file.
enum SlackID: String { case botUser = "dummy-bot" case botToken = "xoxb-SLACK_GENERATED_NUMBER" }
Now we also need a user token to get this while being logged into Slack. Go to https://api.slack.com/apps and click on Create an APP. You will see the following screen.
Now, write the name of app that you want to create and select the workplace team, my app name is SlackAppTest and workplace team name is codeTest.Click on Create App, and you will get Client ID, Client Secret and Verification Token.
Add each of the above details to your enum named SlackID. You will come to know about the reason later. Now we need to install this app to workplace but before installation we need to add some permissions so click on Permissions.
After that if you need to install app to Slack you also need to add a redirect URL. Add any dummy URL like https://example.com
Now you have to add some permissions to this app before installation that decide the purpose behind making this app.
We are going to add two permissions to this app; one for accessing user public channel info so that we can send message from iOS app to any public channel of slack account and one for modifying public channel so that we can create our own public channel for communication.
Now finally you can install your app to Slack account.
After that, click on Authorize on next screen.
Now, you have got your OAuth ID of your app that starts with xoxp-ANY_ALPHANUMERIC_DIGITS also save it to your SlackID enum in your project as you all know again that we will need it later.
Now, our SlackID enum would look like following code snippet.
enum SlackID: String { case botUser = "dummy-bot" case botToken = "xoxb-SLACK_GENERATED_ID" case slackExampleClientID = "SLACK_GENERATED_ID" case slackExampleSecret = "SLACK_GENERATED_SECRET" case slackExampleVerificationToken = "SLACK_GENERATED_VERIFICATION_TOKEN" case slackExampleID = "xoxp-SLACK_GENERATED_ID" }
Now just copy and paste the following code that I’ll provide in SlackConstants.swift file. It will contain the slack APIs and its parameter keys that we need to hit these API’s.
enum SlackID: String { case botUser = "dummy-bot" case botToken = "xoxb-SLACK_GENERATED_ID" case slackExampleClientID = "SLACK_GENERATED_ID" case slackExampleSecret = "SLACK_GENERATED_SECRET" case slackExampleVerificationToken = "SLACK_GENERATED_VERIFICATION_TOKEN" case slackExampleID = "xoxp-SLACK_GENERATED_ID" } struct Slack { /// Slack HTTPS API URLs struct URL { struct rtm { static let start = "https://slack.com/api/rtm.start"; } struct channels { static let create = "https://slack.com/api/channels.create"; static let join = "https://slack.com/api/channels.join"; static let invite = "https://slack.com/api/channels.invite"; static let channelList = "https://slack.com/api/channels.list" } } /// Parameter constants as found in Slack data struct param { static let token = "token"; static let ok = "ok"; static let url = "url"; static let channels = "channels"; static let name = "name"; static let channel = "channel"; static let id = "id"; static let type = "type"; static let text = "text"; static let users = "users"; static let user = "user"; static let profile = "profile"; static let image_32 = "image_32"; static let color = "color"; static let connect = "connect" static let image = "image"; static let image_data = "image_data"; } // Message types as found in Slack data struct type { static let message = "message"; static let user_typing = "user_typing"; } // User tokens for the Slack bot and user. Note: storing these in a production app is very unsafe and not secure !!! struct token { static let bot = SlackID.botToken.rawValue static let admin = SlackID.slackExampleID.rawValue } // Misc. constants, the username of the Slack bot, and don't forget your towel. struct misc { static let usernames = ["arthur", "ford", "trillian", "zaphod", "marvin", "eddie", "hamma-kavula", "slartibartfast", "deep-thought", "agrajag", "vogon-jeltz"]; static let bot_name = SlackID.botUser.rawValue } } struct MessageCenter { // Dictionary keys for NSUserDefaults struct prefs { static let channelID = "channel_id"; } // Notification types as used in the Message Center struct notification { static let newMessage = "new_message"; static let userTyping = "user_typing"; } }
If you want to explore more Slack APIs, visit https://api.slack.com/ and enlighten yourself.
Now, we will create a new Swift file named SlackAPI.swift in which we will create a singleton SlackAPI class for hitting our slack APIs using Alamofire and mapping its data to get required values from received JSON using SwiftyJSON, so again copy and paste. CAUTION First understand then copy.
import Foundation import Alamofire import SwiftyJSON class SlackAPI: NSObject { /// Singleton instance of `RSSlackAPI` static let sharedInstance = SlackAPI(); /** Send "rtm_start" HTTPS API request to Slack. Returns info about users, channels, and the websocket RTM URL. This method also searches the user data for the ID of the user bot `Slack.misc.bot_name`, and stores all relevant user data (ID, profile, color and image URL). Finally, it calls the `completion` closure when the request is finished. It's recommended you connect to the websocket, because it closes in 30 seconds after "rtm_start". :param: completion Closure that's called upon completion of this method. */ func rtm_start(completion: @escaping (String, String) -> Void) { var botID: String? Alamofire.request(Slack.URL.rtm.start, method: .get, parameters: [Slack.param.token: Slack.token.bot] as [String : Any], encoding: URLEncoding.default).responseJSON { response in if response.error != nil { print(response.error?.localizedDescription ?? ""); return; } let json = JSON(response.data ?? Data()); print(json); if let users = json[Slack.param.users].array { for user in users { // Figure out user ID of bot if user[Slack.param.name].string != nil && user[Slack.param.name].stringValue == Slack.misc.bot_name { botID = user[Slack.param.id].string ?? "" print("BotID: ", user[Slack.param.id].string ?? "") } // Store user data in RSMessageCenterAPI for later reference var user_data = [String: AnyObject](); if let id = user[Slack.param.id].string, let profile = user[Slack.param.profile].dictionary, let color = user[Slack.param.color].string, let image = profile[Slack.param.image_32]?.string { user_data[Slack.param.color] = color as AnyObject; user_data[Slack.param.image] = image as AnyObject; print("UserData: ", user_data) } } } // Get websocket URL and call completion closure if let url = json[Slack.param.url].string { completion(url, botID ?? ""); } } } /** Send "channels_join" HTTPS API request to Slack. Uses the admin token (i.e. the admin user) to join a new channel with `channel_name`. To Slack, joining a channel creates a channel when it doesn't exist yet. The `completion` closure is executed when the request finishes, if the returned data is OK. :param: channel_name String with the name of the channel. :param: completion Closure with `channelID` parameter. */ func channels_join(channel_name:String, completion: @escaping (_ channelID: String) -> Void) { Alamofire.request(Slack.URL.channels.join, method: .get ,parameters: [Slack.param.token: Slack.token.admin, Slack.param.name: channel_name]).responseJSON { response in if response.error != nil { print(response.error?.localizedDescription ?? ""); return; } let json = JSON(response.data!); print(json) if let channel = json[Slack.param.channel].dictionary, let channelID = channel[Slack.param.id]?.string { completion(channelID); } } } /** Send "channels_invite" HTTPS API request to Slack. Used to invite a user with `userID` to channel with `channelID`. Calls a closure upon completion. In the example project, this is used to invite the bot user to the new message center channel. The admin user is already invited, because it created/joined the channel. :param: channelID The channel to invite to. :param: userID The user ID of the user to invite to the channel. :param: completion Optional closure to be called when the request finishes. */ func getChannelList() { Alamofire.request(Slack.URL.channels.channelList, method: .get, parameters: [Slack.param.token : Slack.token.admin], encoding: URLEncoding.default).responseJSON { (response) in if response.error != nil { print(response.error?.localizedDescription ?? ""); return; } let json = JSON(response.data ?? Data()); print(json); } } func channels_invite(channelID:String, userID:String, completion: (() -> Void)?) { Alamofire.request(Slack.URL.channels.invite, method: .get, parameters: [Slack.param.token: Slack.token.admin, Slack.param.channel: channelID, Slack.param.user: userID]).responseJSON { response in if response.error != nil { print(response.error?.localizedDescription ?? ""); return; } let json = JSON(response.data ?? Data()); print(json); if(completion != nil) { completion!(); } } } }
One Socket singleton class is also required to send messages from iOS app to workplace and receive messages from workplace to app. So again create a new file named SocketAPI.swift and copy and paste following code in it.
import Foundation import Starscream import SwiftyJSON protocol SocketDelegate: class { func message(_ messageDict: String) } class SocketAPI: NSObject { static let shared = SocketAPI() var socket: WebSocket? var delegate: SocketDelegate? var isConnected: Bool? { return self.socket?.isConnected ?? false } func connect(url: URL){ self.socket = WebSocket(url: url) socket?.delegate = self socket?.connect() } func disConnect() { socket?.disconnect() socket = nil } func sendMessage(id: Int, type: String, channelID: String, text: String) { let json: JSON = [Slack.param.id : id, Slack.param.type : type, Slack.param.channel : channelID, Slack.param.text : text] if let string = json.rawString() { self.send(message: string) } } func send(message: String) { if let socket = self.socket { if socket.isConnected { socket.write(string: message) } else { return } } } } extension SocketAPI: WebSocketDelegate { func websocketDidReceiveMessage(socket: WebSocket, text: String) { delegate?.message(text) print(text) } func websocketDidConnect(socket: WebSocket) { print("Connected") } func websocketDidDisconnect(socket: WebSocket, error: NSError?) { print("Disconnected") } func websocketDidReceiveData(socket: WebSocket, data: Data) { print("Recieve Data") } }x
Now create the following variables in your view controller class
var socketURL: String?
var userName: String?
var botID: String?
var channelID: String?
Now in viewDidLoad() we are going to hit the Slack real time messaging API from which we can get botID and socketURL. botID is used as ID of sender and socketURL is to send and receive message. After success of RTM API we connect the socket using socket url provided by RTM API to send and receive messages, set socket delegate to receive responses and calling a function named setupChannel() that is used to create a random channel for communication. Write the following code inside viewDidLoad().
override func viewDidLoad() { super.viewDidLoad() SlackAPI.sharedInstance.rtm_start { (webSocketURl, botID) in self.socketURL = webSocketURl self.botID = botID SocketAPI.shared.connect(url: URL(string: webSocketURl)!) SocketAPI.shared.delegate = self self.setupChannel() } textField.delegate = self }
Now we are going to create the following functions:
Here is the code snippet that you will need to implement the above written functions.
extension ViewController { func setupChannel() { if let channelID = UserDefaults.standard.value(forKey: "ChannelID") { self.channelID = channelID as? String self.inviteBotToChannel() } else { let channel_name = getRandomChannelName(); SlackAPI.sharedInstance.channels_join(channel_name: channel_name) { (channelID: String) -> Void in UserDefaults.standard.setValue(channelID, forKey: "ChannelID"); self.channelID = channelID; self.inviteBotToChannel(); } } } func inviteBotToChannel() { if(self.channelID == nil || self.botID == nil) { return } SlackAPI.sharedInstance.channels_invite(channelID: channelID ?? "", userID: self.botID ?? "", completion: nil); } func getRandomChannelName() -> String { let prefix = self.randomString(length: 4) let username = Slack.misc.usernames[Int(arc4random()) % Int(Slack.misc.usernames.count)]; return "\(prefix)-\(username)"; } func randomString(length: Int) -> String { let letters : NSString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789" let len = UInt32(letters.length) var randomString = "" for _ in 0 ..< length { let rand = arc4random_uniform(len) var nextChar = letters.character(at: Int(rand)) randomString += NSString(characters: &nextChar, length: 1) as String } return randomString } func convertToDictionary(text: String) -> [String: Any]? { if let data = text.data(using: .utf8) { do { return try JSONSerialization.jsonObject(with: data, options: []) as? [String: Any] } catch { print(error.localizedDescription) } } return nil } }
To send the message, we’ve to implement socket sendMessage function and to receive messages implement socket delegate that we declared in viewDidLoad(). Finally, copy the last code snippet and you are done.
extension ViewController: UITextFieldDelegate { func textFieldShouldReturn(_ textField: UITextField) -> Bool { textField.resignFirstResponder() SocketAPI.shared.sendMessage(id: 2323, type: "message", channelID: channelID ?? "", text: textField.text ?? "") return true } } //MARK:- Socket Delegate extension ViewController: SocketDelegate { func message(_ messageDict: String) { let json = JSON.init(parseJSON: messageDict) print(json) responseView.text = String(describing: json) print(messageDict) if let dict = convertToDictionary(text: messageDict) { if (dict["type"] ?? "") as? String == "message" { print(dict["text"] ?? "") } } } }
Run the app and try to send messages. You’ll definitely receive messages on Slack workplace in newly created channel.
I leave JSON to you as I know you can do that easily.
Also, if you want to send message to specific channel then instead of creating a new channel, try hitting the channel list API in ViewController class and you’ll get all channel names and IDs in JSON and pass any channel ID that you want to use.
SlackAPI.sharedInstance.getChannelList()
Catch Github repo of this project https://github.com/SandeepSpider811/SlackBotIOSIntegration
Do ? ? ? if you ? it.