Aws lambda handler not working with oauth express handler
Closed this issue · 6 comments
(Describe your issue and goal here)
Reproducible in:
The Slack SDK version
3.17.1
Node.js runtime version
nodejs 18
OS info
ProductName: macOS
ProductVersion: 13.2
BuildVersion: 22D49
Steps to reproduce:
`const { parse } = require('csv-parse');
const { App, ExpressReceiver, AwsLambdaReceiver,LogLevel } = require('@slack/bolt')
const { MongoClient } = require("mongodb")
const fs = require('fs');
require('dotenv').config()
var curr;
var channelt;
var textchat
var name;
var realU;
var anon;
var channelId = "xxx"
const uri =
"mongodb+srv://shanbom:xxxxx@slackappmongo.vatmjnk.mongodb.net/";
// The MongoClient is the object that references the connection to our
// datastore (Atlas, for example)
const client = new MongoClient(uri);
// is required.
(async () => {
// // Start your app
// await app.start(process.env.PORT || 3000)
await client.connect();
console.log('MONGO BABY')
})()
// will create them automatically when you first write data.
const dbName = "StoredShoutouts";
const collectionName = "People";
var botT;
const database = client.db(dbName);
const collection = database.collection(collectionName);
const storedU = database.collection("users")
//Initializes your app with your bot token and signing secret
// const awsLambdaReceiver = new AwsLambdaReceiver({
// signingSecret: process.env.SLACK_SIGNING_SECRET,
// //signingSecret: process.env.SLACK_SIGNING_SECRET,
// clientId: process.env.SLACK_CLIENT_ID,
// clientSecret: process.env.SLACK_CLIENT_SECRET,
// stateSecret: 'michigania',
// scopes: ['chat:write','commands'],
// });
const myInstallationStore = {
storeInstallation: async (installation) => {
// change the line below so it saves to your database
if (installation.isEnterpriseInstall && installation.enterprise !== undefined) {
// support for org wide app installation
botT = installation.bot.token;
return await storedU.insertOne(userStore(installation.enterprise.id,installation)[0])
}
if (installation.team !== undefined) {
// single team app installation
console.log(installation)
botT = installation.bot.token;
return await storedU.insertOne(userStore(installation.team.id,installation)[0])
}
throw new Error('Failed saving installation data to installationStore');
},
fetchInstallation: async (installQuery) => {
// change the line below so it fetches from your database
if (installQuery.isEnterpriseInstall && installQuery.enterpriseId !== undefined) {
// org wide app installation lookup
const findOneQuery = { key: installQuery.enterpriseId};
try {
const findOneResult = await storedU.findOne(findOneQuery);
} catch (err) {
console.error(`Something went wrong trying to find one document: ${err}\n`);
}
console.log(findOneResult)
botT = findOneResult.bot.token
return await findOneResult;
}
if (installQuery.teamId !== undefined) {
// single team app installation lookup
const findOneQuery = { key: installQuery.teamId};
try {
const findOneResult = await storedU.findOne(findOneQuery);
} catch (err) {
console.error(`Something went wrong trying to find one document: ${err}\n`);
}
console.log(findOneResult)
botT = findOneResult.bot.token
return await findOneResult;
}
throw new Error('Failed fetching installation');
},
deleteInstallation: async (installQuery) => {
// change the line below so it deletes from your database
if (installQuery.isEnterpriseInstall && installQuery.enterpriseId !== undefined) {
// org wide app installation deletion
const findOneQuery = { key: installQuery.enterpriseId};
try {
const findOneResult = await storedU.deleteOne(findOneQuery);
} catch (err) {
console.error(`Something went wrong trying to find one document: ${err}\n`);
}
return await findOneResult;
// return await database.delete(installQuery.enterpriseId);
}
if (installQuery.teamId !== undefined) {
// single team app installation deletion
const findOneQuery = { key: installQuery.teamId};
try {
const findOneResult = await storedU.deleteOne(findOneQuery);
} catch (err) {
console.error(`Something went wrong trying to find one document: ${err}\n`);
}
return await findOneResult;
}
throw new Error('Failed to delete installation');
},
};
// OAuth Flow
const expressReceiver = new ExpressReceiver({
clientSecret: process.env.SLACK_CLIENT_SECRET,
processBeforeResponse: true,
clientId: process.env.SLACK_CLIENT_ID,
scopes: ['commands','chat:write','users:write'],
stateSecret: 'michigania',
installationStore: myInstallationStore
});
const awsServerlessExpress = require('aws-serverless-express');
const server = awsServerlessExpress.createServer(expressReceiver.app);
module.exports.oauthHandler = (event, context) => {
awsServerlessExpress.proxy(server, event, context);
}
// Slack Event Handler
const eventReceiver = new AwsLambdaReceiver({
signingSecret: process.env.SLACK_SIGNING_SECRET,
});
const app = new App({
receiver: eventReceiver,
logLevel: LogLevel.DEBUG,
authorize: async (source) => { myInstallationStore }
});
app.command("/hello-bolt-js", async ({ ack }) => {
await ack("I'm working!");
});
// async (event, context,callback) => {
// const handler = await this.event.start();
// context.callbackWaitsForEmptyEventLoop = false;
// return handler(event, context, callback);
// }
function userLine(id, name){
const user = [{
username: id,
number : 1,
namer : name,
}
]
return user;
}
function userStore(id, install){
const user = [{
key: id,
info: install
}]
return user;
}
// (async () => {
// // Start your app
// await app.start(process.env.PORT || 3000)
// console.log('⚡️Hello World.. Bolt app is running!')
// })()
const res = [];
const talk = [];
const shouts = [];
// fs.createReadStream('test.txt')
// .pipe(parse({ record_delimiter: ' '}))
// .on('data', (data) => shouts.push(data)) // push data to the result array
// .on('end', () => {
// //console.log(res[0])
// // var price = 0; // create a variable for the price
// for(var s=0; s<shouts.length; s++) // iterate over all records
// console.log(shouts[s][0])
// // console.log(price); // print the price
// })
// .on('error', (err) => {
// console.log("error: " + err);
// });
app.event('app_home_opened', async ({ event, say, client, view }) => {
//console.log("NOW")
await ack();
console.log('⚡️Hello! Someone just opened the app to DM so we will send them a message!')
//say(`Hello world and <@${event.user}>! `)
const textMe = ""
const query = ""
const leaders = []
lead = "Leaderboard\n"
try{
const cursor = await collection.find().sort({number: -1}).toArray()
console.log("Search success")
console.log(cursor[0])
for(blue in cursor){
leaders[blue] = cursor[blue]
console.log(leaders[blue])
}
for(blue in leaders){
lead = lead + leaders[blue].namer + " " + leaders[blue].number + "\n";
}
//textMe =cursor[0].namer + " " + cursor[0].number;
//console.log(cursor)
}catch{
console.log("failure on search")
}
const resultt = await client.users.info({
user: event.user
});
adminStatus = resultt.user.is_admin;
console.log(resultt)
if(!adminStatus){
try {
/* view.publish is the method that your app uses to push a view to the Home tab */
await client.views.publish({
/* the user that opened your app's app home */
user_id: event.user,
view: {
"type": "home",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "Welcome to the App Home",
"emoji": true
}
},
{
"type": "context",
"elements": [
{
"type": "plain_text",
"text": "Author: Matthew Shanbom \n This app is currently in development",
"emoji": true
}
]
}
]
}
});
}
catch (error) {
console.error(error);
}
}else{
try {
/* view.publish is the method that your app uses to push a view to the Home tab */
await client.views.publish({
/* the user that opened your app's app home */
user_id: event.user,
view: {
"type": "home",
"blocks": [
{
"type": "header",
"text": {
"type": "plain_text",
"text": "Welcome to the Leaderboard",
"emoji": true
}
},
{
"type": "context",
"elements": [
{
"type": "plain_text",
"text": "Author: Matthew Shanbom",
"emoji": true
}
]
},
{
"type": "divider"
},
{
"type": "section",
"fields": [
{
"type": "plain_text",
//only usable on deployed workspace
"text": lead,
"emoji": true
}
]
}
]
}
});
}
catch (error) {
console.error(error);
}
}
})
app.command('/thanks', async ({ ack, body, client, logger,say }) => {
// Acknowledge the command request
await ack();
console.log(body)
say(`From <@${body.user_id}>! `)
say(body.text)
});
app.action('plain', async ({ body,payload, ack,context }) => {
await ack();
console.log("sup")
});
app.action('hidden', async ({ body,payload, ack }) => {
await ack();
console.log("here \n")
console.log(payload.value)
});
app.action('users', async ({ body,payload, ack }) => {
// Get information specific to a team or channel
await ack();
curr = payload.selected_user
realU = payload.selected_user.real_name
console.log(payload.selected_user)
});
app.options('users', async ({ body,payload, ack }) => {
// Get information specific to a team or channel
await ack();
//curr =
});
app.view('view_1', async ({ ack, say, body, view, client, logger ,payload}) => {
// Acknowledge the view_submission request
await ack();
if(body.user.id == curr){
client.chat.postEphemeral({
channel: channelt,
user: curr,
text: "You are the best you. You don't need to shout yourself out!"
})
return;
console.log("testing")
}
if(curr == undefined){
client.chat.postEphemeral({
channel: channelt,
user: body.user.id,
text: "Make sure to select a user to shout out"
})
return;
}
if(channelt != channelId){
client.chat.postEphemeral({
channel: channelt,
user: body.user.id,
text: "This shoutout has been posted in the shout out channel"
})
}
//console.log(payload.state.values.EwH7m.plain.value)
seen = false
num = 1
// console.log(body)
console.log("made here")
//console.log(payload)
try {
// Call the users.info method using the WebClient
const result = await client.users.info({
user: curr
});
realU = result.user.real_name
console.log(result);
}
catch (error) {
console.error(error);
}
const findOneQuery = { username: curr};
try {
const findOneResult = await collection.findOne(findOneQuery);
if (findOneResult === null) {
collection.insertOne((userLine(curr,realU))[0])
console.log("not found")
num = 1;
} else {
console.log("found")
console.log(findOneResult.number);
console.log(findOneResult.username)
num = findOneResult.number + 1;
}
} catch (err) {
console.error(`Something went wrong trying to find one document: ${err}\n`);
}
const updateDoc = { $set: { number: num } };
// The following updateOptions document specifies that we want the *updated*
// document to be returned. By default, we get the document as it was *before*
// the update.
const updateOptions = { returnOriginal: false };
try {
const updateResult = await collection.findOneAndUpdate(
findOneQuery,
updateDoc,
updateOptions,
);
console.log(`Here is the updated document:\n` + updateResult.number);
} catch (err) {
console.error(`Something went wrong trying to update one document: ${err}\n`);
}
console.log(payload.state.values.check.hidden.selected_option.value)
if(payload.state.values.check.hidden.selected_option.value == 'anon_yes'){
anon = true;
console.log("MADE 1\n")
}if(payload.state.values.check.hidden.selected_option.value == 'anon_no'){
anon = false;
console.log("MADE 2\n")
}
console.log(anon)
if(anon){
textchat = `Shoutout to to <@${curr}>! Thanks for ${payload.state.values.inp.plain.value}.`
}else{
textchat = `Shoutout to to <@${curr}>! Thanks for ${payload.state.values.inp.plain.value}. From <@${body.user.id}>.`
}
curr = undefined
await client.chat.postMessage(
{
channel: channelId,
text: textchat,
username: "Shoutouts",
icon_url: "https://avatars.slack-edge.com/2024-02-15/6671055905424_6e134146fcb112523948_132.png"
}
);
});
app.command('/shoutout', async ({ ack, body, client, logger }) => {
// Acknowledge the command request
await ack();
const resultt = await client.users.info({
user: body.user_id
});
adminStatus = resultt.user.is_admin;
if(!adminStatus){
console.log("Not admin")
client.chat.postEphemeral({
channel: body.channel_id,
user: body.user_id,
text: "This app is currently in development. Please check back later."
})
return;
}
channelt = body.channel_id
try {
// Call views.open with the built-in client
const result = await client.views.open({
// Pass a valid trigger_id within 3 seconds of receiving it
trigger_id: body.trigger_id,
// View payload
view: {
type: 'modal',
// View identifier
callback_id: 'view_1',
title: {
type: 'plain_text',
text: 'Give Gratitude'
},
blocks: [
{
"type": "input",
"block_id": "inp",
"element": {
"type": "plain_text_input",
"action_id": "plain"
},
"label": {
"type": "plain_text",
"text": "Thanks for ____",
"emoji": true
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Who do you want to shout out?"
},
"accessory": {
"type": "users_select",
"placeholder": {
"type": "plain_text",
"text": "Select a user",
"emoji": true
},
"action_id": "users"
}},
{
"type": "input",
"block_id": "check",
"element": {
"type": "static_select",
"placeholder": {
"type": "plain_text",
"text": "Select an item",
"emoji": true
},
"options": [
{
"text": {
"type": "plain_text",
"text": "Yes",
"emoji": true
},
"value": "anon_yes"
},
{
"text": {
"type": "plain_text",
"text": "No",
"emoji": true
},
"value": "anon_no"
}
],
"action_id": "hidden"
},
"label": {
"type": "plain_text",
"text": "Would you like to be anonymous?",
"emoji": true
}
}
],
submit: {
type: 'plain_text',
text: 'Submit'
}
}
});
//logger.info(result);
}
catch (error) {
logger.error(error);
}
});
// module.exports.eventHandler = async (event, context, callback) => {
// const eventHandler = await eventReceiver.start();
// context.callbackWaitsForEmptyEventLoop = false;
// console.log(event)
// return eventHandler(event, context, callback);
// }
// const awsServerlessExpress = require('aws-serverless-express');
// const server = awsServerlessExpress.createServer(expressReceiver.app);
// module.exports.oauthHandler = async (event, context) => {
// awsServerlessExpress.proxy(server, event, context);
// }
// module.exports.handler = async (event, context, callback) => {
// if (event.source === 'serverless-plugin-warmup') {
// return callback(null, 'Lambda is warm!')
// }
// const handler = await awsLambdaReceiver.start()
// return handler(event, context, callback)
// }
app.error((error) => {
// Check the details of the error to handle cases where you should retry sending a message or stop the app
console.error(error);
});
module.exports.eventHandler = eventReceiver.toHandler();
serverless.yml
org: shanbom
app: bot
service: slackapp
frameworkVersion: '3'
provider:
name: aws
runtime: nodejs18.x
timeout: 15
memorySize: 2048
environment:
SLACK_SIGNING_SECRET: xxx
SLACK_BOT_TOKEN: xxx
SLACK_CLIENT_ID: xxxx
SLACK_CLIENT_SECRET: xxxx
functions:
oauth_redirect:
handler: index.oauthHandler
events:
- http:
path: slack/oauth_redirect
method: get
install:
handler: index.oauthHandler
events:
- http:
path: slack/install
method: get
slack:
handler: index.eventHandler
events:
- http:
path: slack/events
method: post
plugins:
- serverless-offline
Expected result:
I expect to be able to handle events
Actual result:
(Tell what actually happened with logs, screenshots)
Requirements
For general questions/issues about Slack API platform or its server-side, could you submit questions at https://my.slack.com/help/requests/new instead. 🙇
Please read the Contributing guidelines and Code of Conduct before creating this issue or pull request. By submitting, you are agreeing to those rules.
Can you set your logging to debug and/or attach your own custom error handler to log out more details? It is difficult for us to help you because to reproduce this issue we'd have to setup your very specific deployment requirements (mongodb, aws).
Both of these are setup and I still receive the same error message
My code previously worked prior to adding an express receiver for oauth with this handler
This leads me to believe something goes wrong in the new AWS handler
// Slack Event Handler
const eventReceiver = new AwsLambdaReceiver({
signingSecret: process.env.SLACK_SIGNING_SECRET,
});
const app = new App({
receiver: eventReceiver,
logLevel: LogLevel.DEBUG,
authorize: async (source) => { myInstallationStore }
});
module.exports.eventHandler = eventReceiver.toHandler();
Note, I changed the name of the handler in serverless to eventhandler as shown above
I took much of the top part of the code, with modifications, from #784
Sorry, but I'm not sure how to assist you. I would need to set up AWS and mongodb accounts, use the serverless framework to deploy the app, and so on.
It seems like only sometimes the "MONDO BABY" log that bootstrapping your mongodb client executes as part of the initialization shows up in the logs. That seems suspect. The way you are structuring your code, you are booting the DB client initialization off to essentially a background, non-blocking "thread" when wrapping it in an async function that is not awaited, which tells me maybe your DB connection may not be established by the time Bolt tries to process the event? But, frankly, the code snippet you posted is messy and hard to follow.
As for the 'unhandled error' log you see, that comes from here: https://github.com/slackapi/bolt-js/blob/main/src/receivers/AwsLambdaReceiver.ts#L188-L205
I would suggest tracing bolt code in your particular setup, modifying bolt code and possibly adding additional logging, to get awareness about what is happening. I am unable to help you without more information, especially because there is no available reproduction case and it is very dependent on your particular infrastructure setup and specific vendor choices.
Thanks for trying to help. Just for additional reference, I have a screenshot of the installation object in mongodb
(async () => {
// // Start your app
// await app.start(process.env.PORT || 3000)
await client.connect();
console.log('MONGO BABY')
})()
Do the awaits here not start the function?
This method worked before adding oauth.
Do you have any more tips to implement oauth with aws lambda as talked about in #784
Consider this code:
async function sleep (time) {
return new Promise((res) => {
setTimeout(res, time);
});
}
console.log('hi');
(async () => {
console.log('start of async wrapper, waiting one second...');
await sleep(1000);
console.log('.. it has been one second');
})();
console.log('goodbye');
If I run this, here is the output:
➜ node asynctest.js
hi
start of async wrapper, waiting one second...
goodbye
.. it has been one second
If you put your ENTIRE application code within the async wrapper function, then the order of events would happen in an expected and deterministic way. As soon as you spread code out between one or more async wrapper function, and outside of them as well, it becomes less consistent and deterministic.
I redid the procedure and switched to an authorize function instead of the fully built in oauth and it worked