slackapi/bolt-js

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:

Screenshot 2024-04-10 at 11 54 22 AM

(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
Screenshot 2024-04-10 at 12 16 04 PM

My code previously worked prior to adding an express receiver for oauth with this handler
Screenshot 2024-04-10 at 12 17 29 PM

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
Screenshot 2024-04-10 at 12 59 08 PM

(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