Slack slash command delayed response on AWS Lambda - javascript

I'm trying to make an integration for Slack that queries a server and gets back some results to the user. The search sometimes takes longer than the window Slack allows for responses, so I need to immediately return status 200.
How can I do this with a lambda function? I tried using the callback function and then sending the data to another lambda service, but the original function waits for it to return, meaning I'm still getting held up by the server I'm querying.
Here's what I'm working with
var rp = require('request-promise');
exports.handler = (event, context, callback) =>{
//I wanted this to send back my STATUS 200 so the command wouldn't time out
callback(null, "Working...");
//I want this post to happen asynchronously so that slack gets the callback response while the search is happening
//but this still waits until the post comes back before resolving the callback
var options = {
method: 'POST',
uri: "https://url-to-other.service",
body:{
"team": event.team,
"label": event.label,
"url": event.aresponse_url
},
json:true
};
rp(options);
};
When I run this, the callback text shows up after the result from the other function, meaning this service is waiting for the other to stop running before returning the status.

That won't work in a single lambda function because as soon as you invoke callback() the lambda container dies. What you could do is have this lambda function invoke another lambda function before calling callback() which in turn will POST to your slack channel url.
Here's an example of how that would work (it's not a 100% but should give you a good idea of how it'll work.)
Function 1: (receive slack event, invoke second function, immediatly return 200
let AWS = require('aws-sdk')
exports.handler = (event, context, callback) => {
let lambda = new AWS.Lambda()
let params = {
FunctionName: 'YOUR_SECOND_FUNCTION_NAME',
InvocationType: 'Event', // Ensures asynchronous execution
Payload: JSON.stringify({
team: event.team,
label: event.label,
url: event.aresponse_url
})
}
return lambda.invoke(params).promise() // Returns 200 immediately after invoking the second lambda, not waiting for the result
.then(() => callback(null, 'Working...'))
}
Function 2: (receive first lambda event, wait for search to complete, POST to slack channel)
let rp = require('request-promise')
exports.handler = (event, context, callback) => {
let searchOptions = {
method: 'POST',
uri: 'https://url-to-other.service',
body: {
'team': event.team,
'label': event.label,
'url': event.aresponse_url
},
json:true
}
return rp(searchOptions)
.then(result => {
let slackOptions = {
method: 'POST',
uri: 'YOUR_SLACK_CHANNEL_URL',
body: {
text: JSON.stringify(result) // I stringified the search result for you for ease's sake, not sure what you need here
},
json: true
}
return rp(slackOptions)
})
.then(() => { callback(null, 'good measure') })
}

Related

How to use a Google Cloud Task Queue and get a json response?

I would like to use Google Cloud Task Queue in my firebase cloud function to call another function and return the work. Is this something we can do with Google Cloud Task?
I am trying to do something similar such as Redis queue where once the work is done I can get the results and continue with my app.
Here I am trying to show what I want to do with a simple addition function. In reality, I would like to call another Http endpoint and use my Queue to rate limit request. For now, this example gets the point across however I cant seem to send the data back.
exports.addNumbersFunc = functions.https.onRequest(async (data, res) => {
const payload = data.body;
console.log("Received: ", payload);
// Work done
const answer = payload["numbers"]["a"] + payload["numbers"]["b"];
// Can I send back the answer here?
res.json({ "answer": answer });
}
);
exports.exampleFunction1 = functions.https.onCall(async (data) => {
// create json object to send to server with data
const jsonPayload = JSON.stringify({
"numbers": { "a": 1, "b": 2 }
});
const url =
`https://${location}-${project}.cloudfunctions.net/addNumbersFunc`;
const task = {
httpRequest: {
httpMethod: 'POST',
url: url,
headers: {
'Content-Type': 'application/json'
},
body: Buffer.from(jsonPayload).toString('base64')
}, oidcToken: {
serviceAccountEmail
}
};
const request = {
parent: parent,
task: task,
};
// Send create task request.
console.log('Sending task:');
const [response] = await client.createTask(request);
console.log(`Response: ${response}`);
console.log(`Created task ${response.name}`);
// Would the response be here??
console.log(`Response httpReques: ${response.httpRequest.body}`);
return response;
});
Cloud Task is async. You cann't get the HTTP response of the task; –
guillaume blaquiere

Twilio: Delay SMS message while reacting to call

I'm building a twilio function that reacts to incoming phone calls. After a call is received the function should send a SMS to the caller. However, I would like the SMS to be delayed by a couple minutes. Currently, my function looks like this:
exports.handler = function (context, event, callback) {
// Create a new voice response object
const client = context.getTwilioClient();
const client_number = event.From;
const twilio_number = +xxxx;
client.messages.create({ // Send SMS
to: client_number,
from: twilio_number,
body: "Hello on SMS"
}).then(() => { // When request to send SMS is complete, deal with the caller
let twiml = new Twilio.twiml.VoiceResponse();
twiml.say(""); // respond to voice caller
callback(null, twiml);
})
};
Right now the SMS is sent and then the response to the call is sent. I'm not sure how to delay the message.create. I've been reading this but it doesn't work because I can't deal with the call first and then send the message.
https://www.twilio.com/docs/runtime/quickstart/add-delay
Twilio developer evangelist here.
This is not something you can do with just a Twilio Function. While aLittleSalty's answer will work in a situation where your application and event loop continues to run, like in an Express app that you run, but in a Twilio Function once you call the callback method the event loop is terminated so the delay will be cancelled.
Similarly, Twilio Function execution time is limited to 10 seconds, so you can't delay a message by a couple of minutes even if the above would work.
One idea could be that you send the message once the call has completed. Your example code only includes a <Say> so I will work with that, but you could do this in other ways if the call continues differently.
First, update your initial Function to return just the TwiML, but add a <Redirect> after the <Say> so that once the message completes Twilio sends a new webhook.
exports.handler = function (context, event, callback) {
let twiml = new Twilio.twiml.VoiceResponse();
twiml.say(""); // respond to voice caller
twiml.redirect("/after_say");
callback(null, twiml);
};
Then create another Function at the /after_say path that sends the message and hangs up the call.
exports.handler = function (context, event, callback) {
// Create a new voice response object
const client = context.getTwilioClient();
const client_number = event.From;
const twilio_number = +xxxx;
let twiml = new Twilio.twiml.VoiceResponse();
twiml.hangup();
client.messages.create({ // Send SMS
to: client_number,
from: twilio_number,
body: "Hello on SMS"
}).then(() => {
callback(null, twiml);
}).catch(error => {
console.error(error);
callback(null, twiml);
});
}
That's still not going to give you a couple of minutes between the call coming in and the message being sent, but it will hopefully send once the call is complete and be a better experience.
This doesn't work with Twilio Functions check #philnash answer
You have to return the callback first to answer the call, and then in a delayed promise send the SMS
exports.handler = function (context, event, callback) {
const delay = (delayMS) => new Promise((res) => {
setTimeout(res, delayMS)
})
// Create a new voice response object
const client = context.getTwilioClient();
const client_number = event.From;
const twilio_number = +xxxx;
let twiml = new Twilio.twiml.VoiceResponse();
twiml.say(""); // respond to voice caller
callback(null, twiml);
return (
delay(10000)
.then(() =>
client.messages.create({ // Send SMS
to: client_number,
from: twilio_number,
body: "Hello on SMS"
})
)
.then(() => console.log("SMS sent"))
)
}
Hopefully this helps.
Note: delay is an utility function where you pass the amounts of milliseconds you want to wait.

AWS Lambda to be invoked directly from a step function (without invoking the API) and also within an API context

I am struggling to understand a basic aspect of Lambda implementation.
Problem: how to use a lambda both inside and outside of an API context?
I have a lambda (nodejs) with an API gateway in front of it:
MyFunction:
Type: AWS::Serverless::Function
Properties:
CodeUri: functions/myfunction/
Handler: app.lambdaHandler
Runtime: nodejs14.x
Timeout: 4
Policies:
- DynamoDBCrudPolicy:
TableName: MyTable
Events:
ApiEvent:
Type: Api
Properties:
Path: /myfunc
Method: any
The handler is used to read (GET) or write (POST) into a a DynamoDB table and returns accordingly. If no method is passed it assumes GET.
exports.lambdaHandler = async (event) => {
const method = event.httpMethod ? event.httpMethod.toUpperCase() : "GET";
try {
switch (method) {
case "GET":
// assuming an API context event.queryStringParameters might have request params. If not the parameters will be on the event
const params = event.queryStringParameters ? event.queryStringParameters : event;
// read from dynamo db table then return an API response. what if outside an API context?
return {
statusCode: 200,
body: JSON.stringify({ message: "..." })
};
case "POST":
// similarly the body will be defined in an API context
const body = typeof event.body === "string" ? JSON.parse(event.body) : event.body;
// write to dynamo db table
return {
statusCode: 200,
body: JSON.stringify({ message: "..." })
};
default:
return {
statusCode: 400,
body: JSON.stringify({ error: "method not supported" })
};
}
} catch (error) {
// this should throw an Error outside an API context
return {
statusCode: 400,
body: JSON.stringify({ error: `${typeof error === "string" ? error : JSON.stringify(error)}` })
};
}
}
Is there an easy way to refactor this code to support both scenarios? For example a step function could call this lambda as well. I know I can have the step function invoking an API but I think this is overkill as step functions support invoking lambdas directly.
I see 2 ways I can go about this:
The lambda has to be aware of whether it is being invoked within an API context or not. It needs to check if there's a http method, queryStringParameters and build its input from these. Then it needs to return a response accordingly as well. A stringified JSON with statusCode or something else, including throwing an Error if outside an API call.
The lambda assumes it is being called from an API. Then the step function needs to format the input to simulate the API call. The problem is that the response will be a string which makes it difficult to process inside a step function. For example assigning it to a ResultPath or trying to decide if there was an error or not inside a choice.
Additionally I could have the step function calling an API directly or the last resort would be to have 2 separate lambdas where the API lambda calls the other one but this will incur additional costs.
Thoughts? Thanks.
This is where middlewares like MIDDY come into picture.
All the logic to determine event type, event source and parsing will be abstracted out and actual business logic always coded to use standard input.
we can add as many layers as we need and send standard schema as lambda input.
Typically, events may come from Api Gateway, Step function, Kinesis, SQS, etc and same lambda works for any event source.
export const handler = middy((event, context, callback) => {
const mainProcess = async () => {
const response = {}
// Busines Logic using event
return response;
};
mainProcess()
.then((result) => {
callback(null, result);
})
.catch((error) => {
callback(error);
});
})
.use({
before: (hndlr, next) => {
const parsedEvent = parseApiGatewayRequest(hndlr.event);
if (parsedEvent) {
hndlr.event = parsedEvent;
}
next();
},
})
.use({
before: (hndlr, next) => {
const parsedEvent = parseStepFuncRequest(hndlr.event);
if (parsedEvent) {
hndlr.event = parsedEvent;
}
next();
},
});

Facebook Graph API in aws lambda failing

I'm having trouble getting this to run. It runs just fine if I make the same FB.api call directly from the front end or through "serverless invoke local" and it console.logs my response. But when I deploy this function to a lambda and try to call it, I hit the "console.log("TRY"), get a 502 error, and then nothing after that. No response from the FB.api call, no errors, no info at all. I've tried upping the timeout as well and have brought it up to as much as 15 seconds and still getting no response. Anyone else run into this? Thanks!
export async function main(event, context, callback){
var FB = require('fb');
const data = JSON.parse(event.body)
console.log("DATA: ", data)
const requestString = data.Id + '/accounts'
console.log(requestString)
console.log("ACCESS TOKEN: ", data.accessToken)
const pages = []
try{
console.log("TRY")
await FB.api(requestString, 'get', { access_token: data.accessToken }, function(response){
console.log("RESPONSE: ", response)
callback(null, success(response));
})
}
catch (e){
console.log("CATCH")
console.log(e)
callback(null, failure({ status: false }));
}
}

AWS Lambda get https response after request

I implemented this AWS Lambda that receives events from slack and response back to slack a sentence and I want to monitor their answer back to the lambda to verify that the message arrived and posted.
// Lambda handler
exports.handler = (data, context, callback) => {
switch (data.type) {
case "url_verification": verify(data, callback); break;
case "event_callback": process(data.event, callback); break;
default: callback(null);
}
};
// Post message to Slack - https://api.slack.com/methods/chat.postMessage
function process(event, callback) {
// test the message for a match and not a bot
if (!event.bot_id && /(aws|lambda)/ig.test(event.text)) {
var text = `<#${event.user}> isn't AWS Lambda awesome?`;
var message = {
token: ACCESS_TOKEN,
channel: event.channel,
text: text
};
var query = qs.stringify(message); // prepare the querystring
https.get(`https://slack.com/api/chat.postMessage?${query}`);
}
callback(null);
}
I want to know how can I get the response of my HTTPS request (that send to me by slack) back to my lambda?
If I understood correctly you want to wait for the result of the your get query.
In your code callback is called immediately and lambda finishes its execution.
To be able to wait for the response you need to remove callback from its current position in the code and call it after request was performed.
// Post message to Slack - https://api.slack.com/methods/chat.postMessage
function process(event, callback) {
// test the message for a match and not a bot
if (!event.bot_id && /(aws|lambda)/ig.test(event.text)) {
var text = `<#${event.user}> isn't AWS Lambda awesome?`;
var message = {
token: ACCESS_TOKEN,
channel: event.channel,
text: text
};
var query = qs.stringify(message); // prepare the querystring
https.get(`https://slack.com/api/chat.postMessage?${query}`, (res, err) => {
if (err) return callback(err);
callback(null);
})
}
// callback was here
}
If you can, use request/request-promise to save some lines of code.
To get the http response in your Lambda Function you just need to wait for the response before calling the Lambda Callback.
Eg.:
var request = require('request-promise');
exports.handler = (event, context, callback) => {
request('https://somedomain.com').then((body) => {
//got the response body
callback(null, body);
});
}
It's the same idea if you're using the https module.

Categories

Resources