Introduction
This article shows how any application can be connected to any slack workspace where application users can authenticate from slack and perform activities from slack which communicates to the application. Also, application can communicate back to the slack workspace.
Background
Throughout my life, I came across various ways to communicate with applications. Take an example, say drop box app, you can see that there are lot of ways to communicate with it, e.g., web app, various types of mobile apps, voice apps such as alex app for dropbox and what not! Now, how would you react if your most favourite tool (i.e., Slack) lets you connect to dropbox, it is pretty much time saving, isn't it! You don't have to deviate from your work, just integrate dropbox and issue some command to get your output.
Now, apply the above thought process about your consumers of your application. Think of this article as a helper guide to enable one more medium/interface for your pro-users/pro-consumer of your application. This will enhance their way of talking to your application and get more productive with your application, i.e., one more selling point for your product/application.
In case you are wondering about Slack, here is a little more background about it. It is a team collaboration tool, which lets you create a workspace which belongs to your organization. Inside your organization, you can build closed group or open groups which are named as channels. Then, every person can communicate with other people in your organization.
Scope
If you read the background, there is huge scope of enhancing your application by integrating it. However, in this article, the scope is limited. From now on, instead of speaking application, I am going to take a real life example. Here, I have taken "Buzz Forum" as application. This application lets you sign up for it. Admins can create groups consisting of some members. Members can ask a question and other members can post answers, that's all about the app. This application uses "oauth
" for authentication. It provides APIs to fetch groups, supports webhook for event, etc. This application is hosted in public website. For demo purposes, I am not giving any further details about the "Buzz forum"'s application URL, rather I will use it as "http://app.example.com". Since this app is mainly for "coder
", I am going to use "coder
" app from now on.
What is included in the demo:
- Build Slack app with required settings
- Install the Slack app to specific channel
- Sign in to coder app from Slack
- Fetch all groups form coder app from Slack
- Subscribe to any group from Slack
- If any new question is posted in coder, post update to Slack channel
There are some limitations for the demo:
- To make Slack app distributable, we have to use HTTPS (in all required URLs) for Slack hosted app. In this article, I am using HTTP, which makes it non distributable and I will refer to the old demo when it was allowed to install HTTP enabled app via URL.
- This integration is applicable to oauth enabled application, preferably web application. However, in future, if there is a request for other type of auth, I can take a look.
- I am using node.js for this demo. All required credential / config is saved in file system instead of DB which is more recommended.
- I will concentrate more on Slack-coder app integration code instead of coder app's functionality. All URLs are purposefully hidden and instead used domain name as "example.com".
- The integration mainly concentrates on showing capability rather than focusing on coding standard obviously which can be improved.
Prerequisites
- I have coder application's oauth credential, i.e.,
client_id
, client_secret
, scope
, redirect_url
. It is hosted - "https://app.example.com". - I have created a Slack account using this - https://slack.com/get-started. Then I created Slack channel named "
code-project-v1
". - I have a valid credential for coder application.
- Slack-coder app is hosted here - http://slack.example.com. It has to be publicly hosted/accessible.
- Node and npm need to be installed. Node version higher than 10 and npm version higher than 5.
Using the Code
I have used node.js, here are the dependencies needed.
"dependencies": {
"body-parser": "^1.18.3",
"express": "^4.16.4",
"querystring": "^0.2.0",
"request": "^2.88.0"
}
The project mainly contains 2 files, one is for settings (settings.js), one is for handling the slack callbacks (server.js).
Here is the source code which I will be using.
Create Slack App
Navigate to https://api.slack.com/apps?new_app=1 and create a new app. In the app name, I used "CoderApp
" and in workspace, I chose my existing workspace. Here is the dialog shown:
The next step is to configure some required settings.
OAuth & Permission
Usage: This is used to specify the callbacks from slack when "CoderApp
" (i.e., the Slack app) is installed in some other workspace. Since we will post our application information to a channel, we will need permission to post it.
How to set: Follow the screenshot (on the Left side, click on "OAuth & Permission"). In the redirect URL, I used http://slack.example.com/slack. Permission I used "Post to specific channels in Slack".
Slash Command
Usage: This enables to add custom Stack command. You might know the existing slash
command, e.g., "/remind
" , "/archive
" etc.. Here, we will introduce "/coder
" as slash
command. Using that command, you can sign in and list groups.
How to set: Follow the screenshot (on the Left side, click on "Slash command"). Here, in the request URL, I used http://slack.example.com/slack_cmd.
Interactive Command
Usage: This enables slack user to execute some user input which in turn contacts to our integration app. Here, we utilize that to subscribe to a group.
How to setup: Follow the screenshot (On the left side, click on "Interactive command"). Here, the request URL is set as "http://slack.example.com/slack_interactive".
Here, we are done with setting of Slack app. Now, go to basic information on the left side, note down the Slack app id, client id, client secret, etc. We will use that in the next step. Here is the basic information shown for me.
Making Our Application Be Installable in Other Workspace
In OAuth, redirect URL is set as "http://slack.example.com/slack". Here is how it is handled in code:
app.get('/slack', function (req, res) {
settings.slack_callback_data.formData["code"] = req.query.code;
slack_response_on_install = res;
console.log("code = " + req.query.code);
console.log( settings.slack_callback_data);
request(settings.slack_callback_data, function (error, response, body) {
if (error) throw new Error(error);
console.log(body);
json_body = JSON.parse(body);
file_name = "data/" + json_body.team_name.replace(/\s+/g, '').toLowerCase() + ".json";
fs.writeFile(file_name, body, function (err) {});
sendSlackMsg(json_body.incoming_webhook.url);
slack_response_on_install.send("Welcome : " + json_body.team_name +
" to coder slack connect app.");
slack_response_on_install = null;
});
})
In the settings, I have maintained slack_callback_data
which contains all Slack credentials, refer to the next code snippet. When someone installs our Slack app, Slack invokes our "/slack
" endpoint - this is as per redirect URL set, it also sends a code in querystring
. We read that code and again post to https://slack.com/api/oauth.access to get access token of Slack workspace (of that Slack user who installs our app). After we get that, we will save that to file system (data/{slack_workspace_name}.json). Then, we will send notification saying "Welcome : {slack_workspace_name}
" to coder Slack connect app.
Here is the snippet for slack_callback_data
.
slack_callback_data: {
method: 'POST',
url: 'https://slack.com/api/oauth.access',
headers: {
'cache-control': 'no-cache',
'content-type': 'multipart/form-data;'
},
formData: {
client_id: "XXX.XXX",
client_secret: "XXXXXXX"
}
}
After this, we will get Slack credential for that client out of which incoming_webhook
is important to us and we will use that to send message to Slack channel.
Authenticate to Coder App From Slack
This is one of the trickiest parts of this article. Here, we will map Slack user to coder app user. You might wonder why we would need this. Let me tell you, from Slack, we enabled to interact with our app, but we have to tell our integration app that any request coming from Slack is authenticated to interact with code app. Here, Slack user has to first authenticate with coder app to get updates about group which they select. They have to go the specific channel where app is installed, there, they have to type "/coder signin
" which will present them the sign in button. On clicking that, it will present them coder app login screen followed by oauth consent screen, then if all goes well, it will say, "Thanks for connecting the coder App" .
How it will work: In the slash
command, we added support from /coder
signin and url to act on is "http://slack.example.com/slack_cmd". Here is the server side code:
app.post('/slack_cmd', function (req, res) {
console.log("----- slack command -----");
console.log(req.body);
command_reply_url = req.body.response_url;
signin_token_file = "data/" + req.body.team_domain + "-app-access.json";
var commands = req.body.text.split(" ");
if (commands[0] == "signin") {
res.send(settings.signin_option);
} else if (commands[0] == "groups") {
no_of_comm = commands[1];;
loadGroups("data/" + req.body.team_domain + "-app-access.json", no_of_comm);
res.send("You will receive groups shortly");
} else {
res.send(req.body.text + " is not supported, only `[signin, groups]` supported");
command_reply_url = null;
signin_token_file = null;
}
})
When signin
command is send, then it will send settings.signin_option
to respond. Here is the snippet for signin_option
.
signin_option: {
"text": "Authenticate with coder portal",
"attachments": [{
"text": "Click signin",
"fallback": "You are unable to authenticate",
"callback_id": "signin_callback",
"color": "#3AA3E3",
"attachment_type": "default",
"actions": [{
"name": "signin",
"text": "Signin",
"type": "button",
"url": "http://slack.example.com/coder_signin",
"value": "signin"
}]
}]
}
Here is how it will look:
Here, the action url is http://slack.example.com/coder_signin, i.e., on server side, we have handler for coder_signin
, here is that.
app.get('/coder_signin', function (req, res) {
console.log("----- coder signin start -----");
app_response = res;
if (req.query.code) {
settings.app_auth.form['code'] = req.query.code;
request(settings.app_auth, function (error, response, body) {
if (error) throw new Error(error);
fs.writeFile(signin_token_file, body, function (err) {});
onAppSuccessSendToSlack(settings.app_connect_msg);
signin_token_file = null;
});
} else {
res.writeHead(302, {
Location: settings.app_init_auth
});
res.end();
}
})
Here, it will do oauth
for coder app, i.e., first, it will redirect to settings.app_init_auth
(in "else
" part), i.e., "http://app.example.com/oauth/authorize?response_type=code&client_id={coder_client_id}&redirect_uri=http://slack.example.com/coder_signin".
Then, coder app will send the code in redirect url, again same handler is called where it received the code in "if
" block. Again, it will post to "http://app.example.com/oauth/token" with code, client secret to get the access_token
, refresh token, etc. Here is the snippet for settings.app_auth
:
app_auth: {
method: 'POST',
url: 'http://app.example.com/oauth/token',
headers: {
'cache-control': 'no-cache',
'Content-Type': 'application/x-www-form-urlencoded'
},
form: {
grant_type: 'authorization_code',
client_id: "coder_client_id",
client_secret: "coder_client_secret"
}
},
After all this is done, we save all token to "data/teamname-app-access.json" and send notification to Slack channel, saying Thanks for connecting the coder App.
Here is the snippet for onAppSuccessSendToSlack
.
function onAppSuccessSendToSlack(message) {
sendSlackMsg(command_reply_url, message, function(){
if (app_response) {
app_response.send("You may close this tab and open slack");
}
app_response = null;
command_reply_url = null;
})
}
function sendSlackMsg(url, message, callback) {
settings.post_message["url"] = url;
if (message) {
settings.post_message.json.text = message;
}
request(settings.post_message, function (error, response, body) {
if (error) throw new Error(error);
console.log(body);
if (callback) callback();
});
}
Here is how it will look when signin is clicked. Currently, I signed in to coder app, so no login or consent is shown.
Listen to Coder Groups Command
Refer to "/slack_cmd" handler, there we defined that if we get groups, then call loadGroups
method. User will send "/coder groups {count_of_groups}
" e.g. "/coder groups 2
". This will fetch 2 latest groups from coder app and present it in Slack showing option to subscribe to one of the groups.
Here is the snippet for loadGroups
:
function loadGroups(team_name, no_of_grp) {
refreshTheToken(team_name, function(access_token){
settings.app_groups_api.headers["Authorization"] = 'Bearer ' + access_token;
settings.app_groups_api.qs.limit = no_of_grp;
request(settings.app_groups_api, function (error, response, body) {
if (error) throw new Error(error);
console.log("Found groups");
console.log(body);
body = JSON.parse(body);
content = {};
content["text"] = "Total groups: " + body.total_count;
content["attachments"] = [];
for (i = 0; i < body.content.length; i++) {
content["attachments"].push(buildGroupContent(body.content[i]));
}
sendSlackMsgWithObject(command_reply_url,content, function () {
console.log("Group fetch msg sent")
});
});
})
}
Here, I am calling refreshTheToken
which gets fresh coder app token so as to avoid expired token, then it invokes coder app's group_api
URL with required access token (of coder app) which gets us list of groups, then we build nicely formatted Slack compatible JSON and send it to Slack via the command_reply_url
(which is the req.body.response_url
of /slack_cmd). Please refer to the actual codebase for function definitions of buildGroupContent
and sendSlackMsgWithObject
.
Screenshot for group command:
Listen to Subscribe Button Click in Slack
We have defined Slack interactive command which comes to picture now, there the request URL is "http://slack.example.com/slack_interactive". Here is the snippet for that:
app.post('/slack_interactive', function (req, res) {
console.log("----- slack interactive -----");
console.log(req.body);
if(req.body.payload){
payload = JSON.parse(req.body.payload);
if(payload.actions && payload.actions[0].value == "subscribe"){
comm_id = payload.callback_id.split("_")[1];
subscribeToGroup(comm_id, payload.team.domain);
res.send({
text: "Thanks " + payload.team.domain + " for subscribing to groups!"
});
} else {
res.send({
text: "listening"
});
}
}
})
Here, we get the payload from req.body.payload
and it checks if command is subscribe
, then calls subscribeToGroup
which saves the group-id
in file system location is "data/{slack_workspace}-subscribe.json", it also tell coderapp
which Slack channel subscribed to what group. This will help us in sending notification to the concerned Slack channel whenever any group activity happens. Then, we respond to Slack command by sending text "Thanks {channelname} for subscribe to groups!
" to response object.
Send Notification to Slack from Code Application
This is the last article where we will send notification to Slack if any new question is posted to coder app. To support this, I have added one endpoint, here is the snippet:
app.get('/message', function (req, res) {
client = req.query.client;
var object = JSON.parse(fs.readFileSync('data/' + client + ".json", 'utf8'));
sendSlackMsg(object.incoming_webhook.url, req.query.msg);
res.send("msg will be delivered");
})
Whenever there is any event in coder application, we will just post a message via "http://slack.example.com/message?client={slack_workspace}&msg={actual_msg}". Here is the screenshot of how it looks:
Points of Interest
This article helped me integrate coder app to Slack for mainly information posting, there are future plans to support adding question or answer via Slack. If you have done any such integration, I would love to know about this, please post about it or give any suggestions here.
History
This is the current version of my article, however if I come across any suggestions or improvement, I will post them here.