This article is part of #25DaysOfServerless. New challenges will be published every day from Microsoft Cloud Advocates throughout the month of December. Find out more about how Microsoft Azure enables your Serverless functions.
Have an idea or a solution? Share your thoughts on Twitter!
Slack as a comms tool is also an excellent tool for automation and productivity. Slack achieves this using an arsenal of developer tools. For today’s challenge, we are going to be using two of those tools to clone the in-built /remind
command in slack.
This command is what Slack refers to as slash commands.
Our responsibility is to send a command using the slash command and get a response after our command is processed.
Here is what the steps look like
-
Send a slash command using slack
-
Receive a message from the bot that your message has been scheduled
-
At the due time (5 seconds in our case), remind the user to do what they need to do.
How are we going to build this monster?
The Big Picture
In as much as this might seem like a tricky assignment, it’s rather straightforward to build. First, you need to understand the requirements and secondly, draw a flow diagram to show how this would work.
Requirements (in user story)
As a user
- I want to be able to send a
/schedule
command to schedule a reminder on slack - I want to be notified that my message has been scheduled
- I want to be notified in due time regarding what I scheduled
- I want to be able to schedule in natural language (e.g.
/schedule eat in 5 seconds
) - I want my message to be sent timely, considering my timezone
Now, what would the flow diagram for this story look like?
Take a look at a typical flow:
This kind of flow is the first thing that comes to mind. The assumptions are:
- When you hit enter after the slash command, slack would send a message to your serverless function
- The function will send back a response for slack to send the user a message
Unfortunately, this simple scenario will not work. It will not work because we want to delay the function somehow until the user’s scheduled time is due or runs out — depends on how you chose to see it.
We will come back to continue our big picture drawing. But for now, we need to take a detour and figure out how to schedule timers on a serverless function dynamically.
Scheduling Timers on a Serverless Function
Scheduling static timers on serverless functions are not complicated. As the developer, you can use cron jobs or triggers to tell the function when to run.
What is not easy though, is when you user wants to tell the function when to run. How can your users schedule timers dynamically?
Durable functions are my favorite kind of serverless function. They are stateful functions that remember their state between multiple runs. This means you can do all kinds of magic and orchestration with them. A durable feature I enjoy a lot and love talking about is timers.
Since durable functions remember their state, it becomes effortless to set up timers with them.
It seems like we have a solution for scheduling dynamic timers — let’s head back to the drawing board and try again.
The Bigger Picture
Here's is where we left off…
But our current flow does not cut it because we want to set up timers and dynamic ones for that matter.
Our simple drawing board is about to get busier. Let’s expand the function part to handle timing:
Woooo! Our function just got a do-over. Don’t panic; we switched it from a plain Function to Durable Functions, and here’s what is going on:
- Slack is sending the slash command to the durable functions which have an HTTP Trigger function that receives the request for processing
- The Trigger sends a response to slack telling the slack user that the request is being processed, in this case, scheduled. It then starts another special function called the Orchestrator. The Orchestrator is the part of a durable function that has state. It uses this powerful ability to start a timer
-
When the timer runs out, the Orchestrator will execute the third function, Activity. An Activity is what completes what the user actually wants a durable function to process. In the diagram, I left a big question mark to allow you to guess what the activity will do.
If you guessed that it sends a message to the trigger, you are so wrong!
If you guessed that it sends a slack notification to the user regarding their reminder, you guessed right!
The next question though is, how will this message be sent to slack. We already lost our opportunity to say something to slack after we sent a response through arrow 2 from the HTTP Trigger to slack. Slack is not expecting a message from us, so it’s not listening for one.
Well, this is why slack made webhooks. To wake slack and send it a message. Here is what the biggest picture of our flow will look like:
Activity sends a request to the Slack webhook API. The request asks the webhook to send a message to a Slack user
The webhook sends the message to the specific Slack that owns the webook and then to the user.
10 Steps to Solution
Now that you have seen the big picture and you know how to pitch this to your boss in a meeting let’s see how to implement and code the solution.
Step 1: Create a Slack App
Head to slack API page and create a new App:
Step 2: Create a Slash Command
When you have your app set and open, click Slash Commands
on the sidebar and create a new command:
You can add any wrong URL to the URL field that the slash post request will be sent to. We will update it later when we have a ready URL.
Step 3: Create a Webhook
Click on Incoming Webhook on the sidebar as well and enable incoming webhook. Create one if none is there and copy the URL somewhere safe:
Step 4: Get your OAuth Token
You need a token to get more information about the user. For example, we need to get users timezones when they send a message. To get your app’s token, click OAuth & Permissions on the sidebar and copy the URL.
Step 5: Create a Durable Function
You are going to need a free Azure account to create a Durable function.
Once you have that, follow the steps here to create a Durable Function without leaving VS Code.
The steps will take you 10 mins to complete. Come back here when you are done so we can continue — I will miss you while you are away.
Step 6. Add Slack Credentials to your Environmental Variables.
Open local.settings.json
and update with your slack credentials:
{
"IsEncrypted": false,
"Values": {
"AzureWebJobsStorage": "🙈",
"FUNCTIONS_WORKER_RUNTIME": "node",
"SLACK_SIGNING_SECRET": "🙈",
"SLACK_WEBHOOK_URL": "🙈",
"SLACK_ACCESS_TOKEN": "🙈"
}
}
Step 7: Update the HTTP Trigger to Handle Slack Slash Command
Copy the code here to your HTTP trigger function. The most important bits in the code are:
- Get the input from slack slash
req.body;
- Activating the Orchestrator
const instanceId = await client.startNew(
req.params.functionName,
undefined,
Object.assign(req.body, { timeZone: userTimeZone })
);
The startNew
function takes the orchestrator name from the query parameter and kicks of the orchestrator. The third argument is the payload you want to send to the orchestrator. It has information like the message that will be sent later, the timezone of the user, the user, etc.
- Create a Status Checker
const timerStatus = client.createCheckStatusResponse(
context.bindingData.req,
instanceId
);
With the instance ID received from starting an orchestrator, we can call createCheckStatusResponse
which returns links you can use to check the status of the orchestrator. This way, you can click these links in the future to see if the orchestrator is still running or completed. Running in our example means that the timer has not yet completed.
- Response
return {
headers: {
'Content-Type': 'application/json'
},
status: 200,
body: {
response_type: 'in_channel',
text: `*${req.body.text}* has been scheduled`
}
};
This is how we tell slack something about the request it made to this function.
Step 8: Update the Orchestrator to Kick Off the Timer
Copy the code here to your Orchestrator function. The most important bits in the code are:
- Get the input sent from the HTTP Trigger:
const input = context.df.getInput();
- Convert natural language to JavaScript date:
const parsedDate = chrono.parseDate(
naturalLanguage,
context.df.currentUtcDateTime
);
Orchestrator functions must be deterministic. It’s kinda like saying they have to be pure. Orchestrator functions run more than ones by itself like a loop until it’s completed at intervals.
What being deterministic means is that for every time an orchestrator function runs, the same value it started on the first run with should be the same till the last run. Non-Deterministic orchestrators are the most common source of errors to Durable Functions. Why am I telling you all these though?
Well, chrono.parseDate
which is the method that converts natural languages to JS Date, takes the natural language to be converted and a reference date. The reference date tells chrono more about which Friday you are talking about. This Friday? Next week’s Friday? Which one?
new Date().now
as the reference value is fine in our case since we want it to know that whatever I am saying now, I am scheduling with reference to the time I scheduled — what a mouthful.
So why are we using context.df.currentUtcDateTime
instead of new Date().now
? This is because new Date() will always give the function a the current date for every time it runs (loops). What we want to give it is the time of the first run.
- Get the Right Timezone
const remindAt = moment(parsedDate)
.tz(timeZone)
.format();
You need to make sure you are sending the message at the timezone of the user and not the server’s timezone.
- Start the Timer
yield context.df.createTimer(new Date(remindAt));
The createTimer
method kicks off the timer with the parsed date.
Step 9: Update the Activity to Send Message to Slack with the Webhook
Copy the code here to your Activity function. This one is pretty straightforward; it uses axios to send a post request to the webhook. It attaches the message as text to the request body.
Step 10: Test and Deploy
- Deploy the function like you learned in step 5 (link to section in article) and copy the URL of the HTTP trigger
- Paste the URL in slack slash command page where we left it pending in step 3
- Try
/schedule lunch in 10 seconds
in your slack
Want to submit your solution to this challenge? Build a solution locally and then submit an issue. If your solution doesn't involve code you can record a short video and submit it as a link in the issue description. Make sure to tell us which challenge the solution is for. We're excited to see what you build! Do you have comments or questions? Add them to the comments area below.
Watch for surprises all during December as we celebrate 25 Days of Serverless. Stay tuned here on dev.to as we feature challenges and solutions! Sign up for a free account on Azure to get ready for the challenges!