slackapi / bolt-js

A framework to build Slack apps using JavaScript
https://tools.slack.dev/bolt-js/
MIT License
2.75k stars 393 forks source link

Unhandled request to /slack/install #2096

Closed zhifengkoh closed 5 months ago

zhifengkoh commented 6 months ago

Apologies in advance if this isn't the right place to ask, but I didn't know where else to turn to after a lot of Googling and ChatGPT/Perplexity.

I am trying to setup OAuth for my BoltJS app so that I can prepare my app for distribution on multiple workspaces. I'm not very familiar building with OAuth so the inner workings are not familiar to me. So far, I'm following along the BoltJS documentation, which has been quite easy to follow for the most part. But where the BoltJS documentation says once OAuth is enabled, I should see an automatically rendered App Install page located at the URL path /slack/install, my server is instead giving me a 404 error and that this path is unhandled.

I don't understand what the issue could be because so far my app is very simple: it has only implemented

Thank you in advance for your help! My app.js is appended below.

Reproducible in:

The Slack SDK version

"slack/bolt": "^3.17.1"

Node.js runtime version

v20.11.1

OS info

ProductName:        macOS
ProductVersion:     14.1.2
BuildVersion:       23B92
Darwin Kernel Version 23.1.0: Mon Oct  9 21:28:45 PDT 2023; root:xnu-10002.41.9~6/RELEASE_ARM64_T6020

Steps to reproduce:

(Share the commands to run, source code, and project settings)

  1. Start my app using node app.js
  2. Hit the URL path of the Slack Install page that is supposed to be rendered automatically by Bolt (http://my-domain.ngrok-free.app/slack/install)

Expected result:

I was expecting to see the default slack install webpage rendered by Bolt, as mentioned here: https://slack.dev/bolt-js/concepts#authenticating-oauth.

My app is working fine when events and user interactions (with modals, multi-select menus built in Block Kit, etc.) are sending requests to http://my-domain.ngrok-free.app/slack/events. So it's not an issue with my ngrok tunnel or the app itself.

Actual result:

After entering http://my-domain.ngrok-free.app/slack/install in my browser:

  1. I see in my terminal log the following error message:
    [INFO]   Unhandled HTTP request (GET) made to /slack/install
  2. And in the ngrok command line (the 404 Not Found for /slack/install):
    
    HTTP Requests                                                                                                                                                                                                                                             
    -------------                                                                                                                                                                                                                                             

POST /slack/events 200 OK
POST /slack/events 200 OK
GET /slack/install 404 Not Found
GET /slack/install 404 Not Found
POST /slack/events 200 OK
POST /slack/events 200 OK


# My app.js

const { App } = require('@slack/bolt'); const { FileInstallationStore} = require ('@slack/oauth')

const app = new App({ token: process.env.SLACK_BOT_TOKEN, signingSecret: process.env.SLACK_SIGNING_SECRET, clientId: process.env.SLACK_CLIENT_ID, clientSecret: process.env.SLACK_CLIENT_SECRET, stateSecret: 'my-secret', //Note: This should be randomly generated and stored/rotated as an environment variable in production scopes: ['chat:write', 'commands'], installationStore: new FileInstallationStore() });

//Retrieve the names of conversations or users given their Slack conversation IDs async function getNames(client, ids) { const names = [];

for (const id of ids) { try { if (id.startsWith('U')) { // Handle user IDs const result = await client.users.info({ user: id }); if (result.ok) { names.push(result.user.real_name); // or result.user.name for the username } else { names.push(Failed to fetch details for user ID: ${id}); } } else { // Handle conversation IDs const result = await client.conversations.info({ channel: id }); if (result.ok) { names.push(result.channel.name); } else { names.push(Failed to fetch details for conversation ID: ${id}); } } } catch (error) { console.error(Error fetching details for ID: ${id}, error); names.push(Error for ID: ${id}); } }

return names; }

//Open a Modal window for the command /alertbot-create app.command('/alertbot-create', async ({ ack, body, client }) => { // Acknowledge the command request await ack(); console.log("command invoked: alertbot-create");

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 the command trigger_id: body.trigger_id, // View payload view: { "type": "modal", "callback_id": "alertbot_create_view", "title": { "type": "plain_text", "text": "Create a new Alert" }, "submit": { "type": "plain_text", "text": "Submit" }, "blocks": [ { "type": "input", "block_id": "search_phrase_block", "element": { "type": "plain_text_input", "action_id": "search_phrase_action", "multiline": false, "placeholder": { "type": "plain_text", "text": "The exact word or phrase that you want to listen out for" } }, "label": { "type": "plain_text", "text": "Search Phrase" } }, { "type": "input", "block_id": "alert_message_contents_block", "element": { "type": "plain_text_input", "multiline": true, "action_id": "alert_message_contents_action", "placeholder": { "type": "plain_text", "text": "The contents of your alert message, excluding users or groups" } }, "label": { "type": "plain_text", "text": "Alert Message Contents", "emoji": true } }, { "type": "input", "block_id": "recipients_block", "element": { "type": "multi_conversations_select", "placeholder": { "type": "plain_text", "text": "Select conversations" }, "filter": { "include": [ "public", "private", "im" ], "exclude_bot_users": true }, "action_id": "recipients_action", "default_to_current_conversation": true }, "label": { "type": "plain_text", "text": "Select users to @mention and/or #channels to post to" } }, { "type": "input", "block_id": "alert_configuration_block", "element": { "type": "checkboxes", "options": [ { "text": { "type": "plain_text", "text": "Send alert as DM only" }, "description": { "type": "mrkdwn", "text": "Alerts will be sent from AlertBot directly to users mentioned. Channels will be ignored." }, "value": "is_dm_only" }, { "text": { "type": "plain_text", "text": "Disable threaded alerts" }, "description": { "type": "mrkdwn", "text": "For alerts posted as a response to the trigger message in its original channel, do not reply in a thread." }, "value": "disable_threading" }, { "text": { "type": "plain_text", "text": "Disable link to trigger message" }, "description": { "type": "mrkdwn", "text": "For alerts posted elsewhere besides the original channel, do not include a link to the triggering message." }, "value": "disable_link_to_trigger" } ], "action_id": "alert_configuration_action" }, "label": { "type": "plain_text", "text": "Alert configuration", "emoji": true } } ] } }); } catch (error) { console.error(error); } });

//Listen to user selection of @users or #channels in the multi conversations select menu of the alertbot-create modal app.action('multi_conversations_select-action', async({ack, body, client}) => { await ack(); console.log("action captured: multi_conversations_select-action");

});

//Listen to the Modal window submission for /alertbot-create app.view('alertbot_create_view', async ({ ack, body, view, client}) => { console.log("view submitted: alertbot_create_view");

// Check if at least one conversation is selected in the recipients_action multi conversations select menu const selectedConversations = view.state.values.recipients_block.recipients_action.selected_conversations; if (!selectedConversations || selectedConversations.length === 0) { // Acknowledge the view submission with an error if no conversations are selected return await ack({ response_action: 'errors', errors: { recipients_action: 'Please select at least one conversation.' } }); }

// Acknowledge the view submission event await ack();

const user_id = body.user.id; const alert_search_phrase = view.state.values.search_phrase_block.search_phrase_action.value; const alert_message_contents = view.state.values.alert_message_contents_block.alert_message_contents_action.value; const selected_config_options = view.state.values.alert_configuration_block.alert_configuration_action.selected_options;

try { //Retrieve conversation names from conversation IDs const names = await getNames(client, selectedConversations); console.log("Conversation names:", names);

//Extract alert configuration options
const configOptions = selected_config_options.map(option => option.value);
console.log(configOptions);

await client.chat.postMessage({
  channel: user_id,
  text: "You created a new AlertBot alert",
  blocks: [
    {
      "type": "section",
      "text": {
        "type": "mrkdwn",
        "text": "⚠️ *You created a new alert*"
      }
    },
    {
      "type": "section",
      "fields": [
        {
          "type": "mrkdwn",
          "text": "*Search Phrase*"
        },
        {
          "type": "plain_text",
          "text": alert_search_phrase
        },
        {
          "type": "mrkdwn",
          "text": "*Alert Message Contents*"
        },
        {
          "type": "plain_text",
          "text": alert_message_contents
        },
        {
          "type": "mrkdwn",
          "text": "*Recipients*"
        },
        {
          "type": "plain_text",
          "text": names.join(", ")
        },
        {
          "type": "mrkdwn",
          "text": "*Configuration Options*"
        },
        {
          "type": "plain_text",
          "text": configOptions.join(", ")
        }
      ]
    }
  ]
});

} catch (error) { console.error(error); }

});

// Listen for messages containing "ping" and respond app.message('ping', async ({ message, say }) => { console.log("messge event: specific string"); try { await say({ text: Hello <@${message.user}>!, blocks: [ { type: "section", text: { type: "mrkdwn", text: Hey <@${message.user}>, check this out! }, accessory: { type: "button", text: { type: "plain_text", text: "Click me!" }, url: "https://example.com" } } ] }); } catch (error) { console.error(error); } });

(async () => { let port = process.env.PORT; if (port == null || port == "") { port = 8000; } // app.listen(port); await app.start(process.env.PORT || 3000); console.log('Slack app is running on port ' + (process.env.PORT || 3000));

})();

seratch commented 6 months ago

Hi @zhifengkoh, thanks for asking the question. I just quickly checked if your example app works for me, and I didn't see any issues with it (token: process.env.SLACK_BOT_TOKEN, is unnecessary though). http://localhost:300/slack/install services the page as I expect. I guess your ngrok might be forwarding your browser request to a different process or your changes to enable the OAuth flow might not be reflected to the running process. I hope you will figure the cause out soon.

zhifengkoh commented 6 months ago

Thanks @seratch for helping me to verify that! I will try to figure it out.

github-actions[bot] commented 5 months ago

👋 It looks like this issue has been open for 30 days with no activity. We'll mark this as stale for now, and wait 10 days for an update or for further comment before closing this issue out. If you think this issue needs to be prioritized, please comment to get the thread going again! Maintainers also review issues marked as stale on a regular basis and comment or adjust status if the issue needs to be reprioritized.

github-actions[bot] commented 5 months ago

As this issue has been inactive for more than one month, we will be closing it. Thank you to all the participants! If you would like to raise a related issue, please create a new issue which includes your specific details and references this issue number.

latifs commented 4 months ago

Hey @zhifengkoh, @seratch, I'm having the exact same issue, do you mind sharing how you fixed it?

shahkeyur commented 4 months ago

@latifs I had the same issue, I realized I didn't enter clientId and clientSecret in my .env. It worked when I added those.

PhilDakin commented 2 months ago

Encountered similar issue - missing stateSecret.