Search code examples
node.jshttpsaws-lambdaslackslack-api

HTTPS GET Request from within a Lambda function for slack bot?


I am new to Node, AWS Lambdas, and Slackbots, so what better way to learn them all than by trying to figure them out at the same time? Just kidding.

I have created a slackbot slash command app. That is, when a user on Slack types '/anondm @username msg' Slack will send this command to my Lambda function, which then returns an answer directly to the sender.

That goal is, have the Lambda take the message, remove the username, and forward the message onto the recipient anonymously.

Now, first off, this exists. Or at least, I can find code repos on GitHub. But I wanted a branded version and I wanted to add a couple little tricks.

What works: — Creating the bot and slash command was not a problem. — Getting the Lambda to receive the message and respond to the sender was also not a problem.

If you're interested, you just need an API Gateway and a Lambda function, and this tutorial will suffice: https://api.slack.com/tutorials/aws-lambda

What is not working: — I need to make an HTTPS GET request to the Slack API so it will execute the command of sending the message to the user.

I spent about a day trying to figure this out and went back and forth between many solutions, but then this morning after some coffee landed on something I REALLY like.

I've seen people suggesting using an SNS Topic. So one Lambda receives web request, then publishes to an SNS Topic which triggers another Lambda to send. Well, that sounds nice for something high volume where I would have use a job queue, but... i mean it's a web handler and a GET request... this should be a couple lines of code.

So the question: What is the easiest way to execute a GET request during a Lambda that has received a web request?

Here is what didn't work (in simplest form) but was expected to be the solution:

'use strict';

var qs = require('querystring');
var https = require('https');

var target = "_snip_";

exports.handler = function(event, context) {
    console.log("This will for sure execute.");

    https.get(target, function(res) {
        console.log("Got response: " + res.statusCode);
    }).on('error', function(e) {
        console.log("Got error: " + e.message);
    });

    console.log("This will for sure NOT execute.");

    return "Your quest is complete.";
};

And here is what would work:

'use strict';

var qs = require('querystring');
var https = require('https');

var target = "_snip_";

exports.handler = function(event, context) {
    console.log("This will for sure execute.");

    https.get(target, function(res) {
        console.log("Got response: " + res.statusCode);

        context.succeed("Your quest is complete");
    }).on('error', function(e) {
        console.log("Got error: " + e.message);
    });
};

Solution

  • The reason I was failing was originally: My lambda was async and returning while i was trying to execute another async or sync function. The Lambda was simply returning too quickly, or getting stuck inside another function waiting to return.

    This is definitely a lack of knowledge problem, but I noticed sooooooooooooooooooooooooooooo many people online struggling with same issue.

    The trick was to use:

    context.succeed("Your message: '"+msg+"' to: '<"+channel+">' has been sent.");
    

    inside the https.get function. This effectively tells the Lambda to return. I should have known this, but I kept expecting the code to get to the bottom and last statement where I returned.

    So no need for axios or requests or any package includes. Simply an API Gateway and a Lambda.

    'use strict';
    
    var qs = require('querystring');
    var https = require('https');
    
    var target = "_snip_";
    
    exports.handler = function(event, context) {
        if( event && event.body && event.body.length > 0 ) {
            var body = event.body.toString();
            var buf = Buffer.from(body, 'base64').toString('ascii');
            var params = qs.parse(buf);
    
            var text = "";
            var channel = "";
            var username = "";
            var msg = "";
    
            if( params && params.text && params.text.length > 0 ) {
                text = params.text;
    
                username = text.split(" ").shift().trim();
    
                username = username.slice(1,-1).split("|");
                channel = username[0];
                username = username[1];
    
                msg = text.split(" ").splice(1).join(" ").trim();
    
                console.log( "channel id: __"+channel+"__" );
                console.log( "username: __"+username+"__" );
                console.log( "msg: __"+msg+"__" );
    
                var msg_slash_resp = "You've received an anonymous message: " + msg;
    
                target = target + "&channel=" + channel;
                target = target + "&text=" + msg_slash_resp;
    
                https.get(target, function(res) {
                    console.log("Got response: " + res.statusCode);
    
                    // return from lambda function
                    context.succeed("Your message: '"+msg+"' to: '<"+channel+">' has been sent.");
                }).on('error', function(e) {
                    console.log("Got error: " + e.message);
                    //context.done(null, 'FAILURE');
                });
            } else {
                context.succeed("Format: /anondm @username your message goes here.");
            }
        } else {
            context.succeed("Format: /anondm @username your message goes here.");
        }
    };
    

    Currently, this is working well in practice and I'm happy with the solution. Please hit me up if you think I could add any more detail to help others.

    Thanks, Chris