Write Slack slash commands with Clime (TypeScript + Node.js)
Slack slash commands is one of the major interfaces interacting with a Slack integration. It is quite easy to integrate different functionalities with Slack slash commands. And this post will introduce a practice using TypeScript command-line interface framework Clime.
Please note that the following content is not by any mean a step-by-step tutorial. If you don't get something (e.g. Nginx configuration), try to Google it yourself.
Setting up server and application
Slack triggers commands by sending requests to given URLs, so we need a public server that can be accessed from internet.
During the development, we may setup port forwarding using SSH to forward a port on the public server to local computer for smoother debugging. Take Nginx an example, assuming we are going to listen port 10047
for this application, we could have a configuration file like this:
server {
server_name [domain_name];
listen 80;
location / {
proxy_pass http://127.0.0.1:10047;
}
}
Execute an SSH command on your local computer and forward remote port 10047
to local:
ssh -R 10047:127.0.0.1:10047 [user]@[server]
Now let's write a simple Express application and listen on port 10047
:
src/main.ts
import * as express from 'express';
let app = express();
app.post('/api/slack/command', async (req, res) => {
res.send('hello, world!');
});
app.listen(10047);
Compile and start the app, and make sure we can access the API from the internet.
Once that is done, we are already able to add a valid Slack slash command. How simple is that!
Open Building Slack apps page, create an app, and add a slash command /demo
with proper request URL. Make sure to have the "Escape channels, users, and links sent to your app" option checked:
Save the command and switch to Slack client, execute command /demo
. And if things went well, you should see Slack responds with the "hello, world!" message:
Now we'll add body-parser
package for parsing command arguments:
src/main.ts
import * as BodyParser from 'body-parser';
import * as express from 'express';
let app = express();
app.use(
BodyParser.urlencoded({
extended: false,
}),
);
app.post('/api/slack/command', async (req, res) => {
console.log(req.body);
res.send('hello, world!');
});
app.listen(10047);
Update the app and execute /demo
command again, and the app console should print infomation like this:
{ token: '[token]',
team_id: '[team_id]',
team_domain: '[team_domain]',
channel_id: '[channel_id]',
channel_name: 'makeflow-dev',
user_id: '[user_id]',
user_name: 'vilic',
command: '/demo',
text: '',
response_url: '[response_url]',
trigger_id: '[trigger_id]' }
You may write down the token here for later use (of course you can always find it on your Slack app page as well).
Writing commands with Clime
You may probably have heard of libraries like commander.js
and yargs
, but not Clime yesterday. However if you want to develop some sort of CLI tools in TypeScript, Clime could be a nice choice.
Clime is an "object-oriented" command-line interface framework, and its core is not coupled with concrete user interfaces. To build Slack slash commands with Clime, the only thing we need is a shim against Slack. And here we are going to add two npm packages clime
and clime-slack
, and update the app code as below:
src/main.ts
import * as Path from 'path';
import * as BodyParser from 'body-parser';
import * as express from 'express';
import {CLI} from 'clime';
import {SlackShim} from 'clime-slack';
let cli = new CLI('/', Path.join(__dirname, 'commands'));
let shim = new SlackShim(cli /*, [token]*/);
let app = express();
app.use(
BodyParser.urlencoded({
extended: false,
}),
);
app.post('/api/slack/command', async (req, res) => {
let result = await shim.execute(req.body);
res.json(result);
});
app.listen(10047);
Now let's create the command file for /demo
:
src/commands/demo.ts
import {Command, command, param} from 'clime';
import {SlackUser} from 'clime-slack';
@command({
description: 'This is a command for printing a greeting message',
})
export default class extends Command {
execute(
@param({
description: 'A slack user',
required: true,
})
user: SlackUser,
) {
return `Hello, ${user}, your ID is ${user.id}!`;
}
}
Clime requires the compilation target to be es6
or higher, and due to the fact it's using decorators and docorator metadata, we'll need to update tsconfig.json
accordingly:
{
"compilerOptions": {
"target": "esnext",
"module": "commonjs",
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"rootDir": "src",
"outDir": "bld",
...
}
}
Compile and restart the app, and execute command /demo
, we'll get the following result:
And of course you can execute /demo --help
to gently retrieve the help content.
The error is caused by parameter user
being required in this case. If we try something like /demo @vilic
(please use an existing username in your organization), we'll have the desired result:
Here we are using
SlackUser
provided byclime-slack
that implementsStringCastable
interface of Clime. The argument<@user_id|user_name>
sent by Slack is automatically casted into aSlackUser
instance.Package
clime-slack
also provides another similarSlackChannel
class, as well as other utilties likeSlackCommandContext
.Please check out the Clime project for more common castable stuffs and usages.
It is rather easy to add a new command now. Just create file src/commands/[command-name].ts
with similiar content in demo.ts
file for the new command, and add the correspondant command in your Slack app.
Getting online
By now we are still running the app locally, but I believe it won't be a problem for you to get it online. To ensure recovery on crashes and system reboots, we may use tools like pm2
to manage our app:
mkdir /var/www/slack-integration
cd /var/www/slack-integration
git clone [...] .
yarn install
yarn build
pm2 start bld/main.js
pm2 startup
pm2 save
And now you may just enjoy the new Slack slash commands integration!