Slack Channel integration with iOS App using chat Bot

Date :
November 8, 2017
Listed by :
Amit Rana
Sidebar
×

Let’s Build Your Dream App!

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.

medium_slack_1

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.

medium_slack_2

Let’s Integrate Chat Bot With iOS App

medium_slack_3

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.

medium_slack_4

  • Here, we will create a UITextField first to send messages to Slack channel which are received by chat bot that we create on our Slack account.
  • Add UILabel for some understanding purpose but you can get away with it if you are lazier than me.
  • Add, UITextView in which we are going to show success response of message and messages received from slack channel.
  • Do not forget to change return type of UITextField that we created to “Send” that we are going to use for sending a message and also remember to disable the editing of UITextView because we are going to use it only for reading purpose.

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
    }
}

Generating Slack Authentication Tokens

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.

medium_slack_5

A new window will open where you can search for bots and then click on Bots.

medium_slack_6

On the next screen click on Add Configuration.

medium_slack_7

Write your chat bot name and click on Add bot integration.

medium_slack_8

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.

Screen Shot 2017-10-30 at 9.40.08 AM

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 IDClient Secret and Verification Token.

Screen Shot 2017-10-30 at 9.47.15 AM

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.

Screen Shot 2017-10-30 at 9.58.00 AM

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

Screen Shot 2017-10-30 at 9.58.37 AM

Now you have to add some permissions to this app before installation that decide the purpose behind making this app.

Screen Shot 2017-10-30 at 10.13.37 AM

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.

Screen Shot 2017-10-30 at 10.19.05 AM

Now finally you can install your app to Slack account.

Screen Shot 2017-10-30 at 10.26.12 AM

After that, click on Authorize on next screen.

Screen Shot 2017-10-30 at 10.28.04 AM

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:

  • setupChannel(): With this we will hit the API for joining a new channel with random new name and after success of channel join API we are going to invite bot to this channel by inviteBotToChannel() function.
  • inviteBotToChannel(): Using this function, we will hit channel invite API by sending parameter botID and channelID.
  • getRandomChannelName(), randomString(length: Int): It will be used to generate a random channel name to setupChannel first time.
  • convertToDictionary(text: String): This function will be used to convert JSON data into Dictionary.

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.

ezgif-4-02be0250df

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.



Get In Touch
partnership
Join, Sell & Earn

Explore Our Partnership Program to Sell
Our Fully Customized Tech Solution To Your Clients.

Partner With Us!

Wait! Looking for Right Technology Partner For Your Business Growth?

It's Time To Convert Your Business Idea Into Success!

Get Free Consultation From Top Industry Experts:
gif
I would like to keep it to myself