Coder Social home page Coder Social logo

seratch / slack-edge Goto Github PK

View Code? Open in Web Editor NEW
60.0 4.0 4.0 357 KB

Slack app development framework for edge functions with streamlined TypeScript support

Home Page: https://github.com/seratch/slack-edge-app-template

License: MIT License

Shell 0.13% TypeScript 99.60% Ruby 0.21% JavaScript 0.06%
cloudflare-workers fetch-api slack slack-api slack-bot typescript typescript-library vercel-edge-functions

slack-edge's Introduction

Slack Edge

npm version deno module

The slack-edge library is a Slack app development framework designed specifically for the following runtimes:

Not only does it work with the above, but it also functions with the latest versions of Deno, Bun, and Node.js.

This framework draws significant inspiration from Slack's Bolt framework, but its design does not strictly follow the bolt-js blueprint. Key differences include:

  • Edge function ready: Out-of-the-box edge function (e.g., Cloudflare Workers) support
  • TypeScript focused: Enhances type safety and clarifies typings for developers
  • Lazy listener enabled: bolt-python's lazy listener feature is provided out of the box
  • Zero additional dependencies: No other dependencies required beyond TypeScript types and slack-web-api-client (our fetch-function-based Slack API client)

Getting Started

Currently, you can use this library for three different platforms.

Cloudflare Workers

Cloudflare Workers is an edge function platform for quickly deploying an app.

The only additional thing you need to do is to add the slack-cloudflare-workers package to your Cloudflare Workers app project. The slack-cloudflare-workers package enhances slack-edge with Cloudflare Workers-specific features, such as KV-based installation stores.

npm install -g wrangler@latest
npx wrangler generate my-slack-app
cd my-slack-app
npm i slack-cloudflare-workers@latest

A simple app that handles a slash command can be structured like this:

import { SlackApp, SlackEdgeAppEnv } from "slack-cloudflare-workers";

export default {
  async fetch(
    request: Request,
    env: SlackEdgeAppEnv,
    ctx: ExecutionContext
  ): Promise<Response> {
    const app = new SlackApp({ env })
      .command("/hello-cf-workers",
        async (req) => {
          // sync handler, which is resposible to ack the request
          return ":wave: This app runs on Cloudflare Workers!";
          // If you don't have anything to do here, the function doesn't need to return anything
          // This means in that case, simply having `async () => {}` works for you
        },
        async ({ context: { respond } }) => {
          // Lazy listener, which can be executed asynchronously
          // You can do whatever may take longer than 3 seconds here
          await respond({ text: "This is an async reply. How are you doing?" });
        }
      );
    return await app.run(request, ctx);
  },
};

Also, before running this app, don't forget to create a .dev.vars file with your env variables for local development:

SLACK_SIGNING_SECRET=....
SLACK_BOT_TOKEN=xoxb-...
SLACK_LOGGING_LEVEL=DEBUG

When you run wrangler dev --port 3000, your app process spins up, and it starts handling API requests at http://localhost:3000/slack/events. You may want to run Cloudflare Tunnel or something equivalent to forward public endpoint requests to this local app process.

Refer to https://github.com/seratch/slack-cloudflare-workers/blob/main/docs/index.md for further guidance.

Vercel Edge Functions

Vercel Edge Functions is an edge function platform, which is part of Vercel's Frontend Cloud.

Create a new Next.js project w/ slack-edge library by following these steps:

npx create-next-app@latest --typescript
cd your-app-name
npm i slack-edge

Create a new source file pages/api/slack.ts that contains the following code:

import type { NextFetchEvent, NextRequest } from "next/server";
import { SlackApp } from "slack-edge";

export const config = {
  runtime: "edge",
};

const app = new SlackApp({
  env: {
    SLACK_SIGNING_SECRET: process.env.SLACK_SIGNING_SECRET!,
    SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN!,
    SLACK_LOGGING_LEVEL: "DEBUG",
  },
});

app.command("/hello-edge",
  async (req) => {
    // sync handler, which is resposible to ack the request
    return ":wave: This app runs on Vercel Edge Function platform!";
    // If you don't have anything to do here, the function doesn't need to return anything
    // This means in that case, simply having `async () => {}` works for you
  },
  async ({ context: { respond } }) => {
    // Lazy listener, which can be executed asynchronously
    // You can do whatever may take longer than 3 seconds here
    await respond({ text: "This is an async reply. How are you doing?" });
  }
);

export default async function handler(
  req: NextRequest,
  context: NextFetchEvent
) {
  return await app.run(req, context);
}

With this code, your Request URL for handling requests from Slack API server will be https://{your public domain}/api/slack instead of /slack/events.

Lastly, you can run the app by following these steps:

export SLACK_SIGNING_SECRET="..."
export SLACK_BOT_TOKEN="xoxb-..."
npm run dev

If you use Cloudflare Tunnel or something equivalent, you may want to run cloudflared tunnel --url http://localhost:3000 in a different terminal window.

We are currently working on the slack-vercel-edge-functions package, which offers Vercel-specific features in addition to the core ones provided by slack-edge. Until its release, please use slack-edge directly and implement the missing features on your own as necessary.

Supabase Edge Functions

Supabase is a BaaS that provides a full PostgreSQL database, along with other products such as Supabase Edge Functions.

Create a new Supabase project locally by following these steps:

mkdir myapp
cd myapp

brew install supabase/tap/supabase
supabase init
supabase functions new hello-world

Open the source file supabase/functions/hello-world/index.ts and replace it with the following:

import {
  SlackApp,
  SlackEdgeAppEnv,
} from "https://deno.land/x/[email protected]/mod.ts";

const app = new SlackApp<SlackEdgeAppEnv>({
  env: {
    SLACK_SIGNING_SECRET: Deno.env.get("SLACK_SIGNING_SECRET")!,
    SLACK_BOT_TOKEN: Deno.env.get("SLACK_BOT_TOKEN"),
    SLACK_LOGGING_LEVEL: "DEBUG",
  },
});

app.command("/hello-edge", async (req) => {
  // sync handler, which is resposible to ack the request
  return ":wave: This app runs on Vercel Edge Function platform!";
  // If you don't have anything to do here, the function doesn't need to return anything
  // This means in that case, simply having `async () => {}` works for you
}, async ({ context: { respond } }) => {
  // Lazy listener, which can be executed asynchronously
  // You can do whatever may take longer than 3 seconds here
  await respond({ text: "This is an async reply. How are you doing?" });
});

Deno.serve(async (req) => {
  return await app.run(req);
});

Run locally

Create a supabase/functions/.env file with the following:

export SLACK_SIGNING_SECRET="..."
export SLACK_BOT_TOKEN="xoxb-..."

Then run:

# Terminal A
supabase start
supabase functions serve --env-file supabase/functions/.env

# Terminal B
brew install cloudflare/cloudflare/cloudflared
cloudflared tunnel --url http://127.0.0.1:54321

Run in the cloud

In a browser, login to your Supabase account and create a new project.

Then open a terminal in your myapp's project directory and run:

supabase login

Link the Supabase project you just made:

supabase link

Deploy your hello-world edge function.

supabase functions deploy hello-world --no-verify-jwt

Finally, add your environment variables:

  • Go to your Supabase project
  • Go to the Edge Functions tab, and click on your edge functions
  • Tap on Manage Secrets and then add your SLACK_BOT_TOKEN and SLACK_SIGNING_SECRET

Run Your App with Deno / Bun / Node.js

This library is available not only for edge function use cases but also for any JavaScript runtime use cases. Specifically, you can run an app with Deno, Bun, and Node.js. To learn more about this, please check the example files under the ./test directory.

Run with Bun

import { SlackApp } from "slack-edge";

const app = new SlackApp({
  env: {
    SLACK_SIGNING_SECRET: process.env.SLACK_SIGNING_SECRET!,
    SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN,
    SLACK_LOGGING_LEVEL: "DEBUG",
  },
});

// Add listeners here

export default {
  port: 3000,
  async fetch(request) {
    return await app.run(request);
  },
};

You can run the app by:

# Terminal A
bun run --watch my-app.ts
# Terminal B
brew install cloudflare/cloudflare/cloudflared
cloudflared tunnel --url http://localhost:3000

Run with Deno

Please refer to README for the Deno module.

Run with Node.js (Socket Mode: Production-ready)

If you need a stable Socket Mode integration, we recommend using @slack/socket-mode along with this package. With Node 20+, the following example works for you:

// npm i slack-edge @slack/socket-mode
import {
  SlackApp,
  fromSocketModeToRequest,
  fromResponseToSocketModePayload,
} from "slack-edge";
import { SocketModeClient } from "@slack/socket-mode";
import { LogLevel } from "@slack/logger";

const app = new SlackApp({
  socketMode: true,
  env: {
    SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN!,
    SLACK_APP_TOKEN: process.env.SLACK_APP_TOKEN!,
    SLACK_LOGGING_LEVEL: "DEBUG",
  },
});

// Add listeners here
app.command("/hello", async ({}) => {
  return "Hi!";
});

// Start a Socket Mode client
const socketModeClient = new SocketModeClient({
  appToken: process.env.SLACK_APP_TOKEN!,
  logLevel: LogLevel.DEBUG,
});
socketModeClient.on("slack_event", async ({ body, ack, retry_num: retryNum, retry_reason: retryReason }) => {
  const request = fromSocketModeToRequest({ body, retryNum, retryReason });
  if (!request) { return; }
  const response = await app.run(request);
  await ack(await fromResponseToSocketModePayload({ response }));
},);
(async () => {
  await socketModeClient.start();
})();

If you want to build a Slack app this way, this project template should be pretty useful for you!

Run with Remix + Node.js

If you're looking for a way to serve Slack app using Remix, slack-edge is the best way to go! Here is a simple example to run your Slack app as part of a Remix app:

import { LoaderFunctionArgs } from "@remix-run/node";
import { SlackApp } from "slack-edge";

export async function action({ request }: LoaderFunctionArgs) {
  const app = new SlackApp({
    env: {
      SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN,
      SLACK_SIGNING_SECRET: process.env.SLACK_SIGNING_SECRET!,
    }
  });
  // Add listeners here

  return await app.run(request);
}

You can run the app by following these steps:

  • npx create-remix@latest
  • cd my-remix-app
  • npm i @remix-run/serve @remix-run/node slack-edge
  • Add app/routes/api.slack.tsx (if you go with POST /api/slack) with the above code
  • export SLACK_SIGNING_SECRET=...
  • export SLACK_BOT_TOKEN=xoxb-...
  • npm run dev

Reference

Middleware

This framework offers ways to globally customize your app's behavior, like you do when developing web apps. A common example is to attach extra data to the context object for following listeners.

Pattern Description
app.beforeAuthorize The passed function does something before calling authorize() function. If this method returns SlackResponse, the following middleware and listeners won't be executed.
app.afterAuthorize / app.use / app.middleware The passed function does something right after calling authorize() function. If this method returns SlackResponse, the following middleware and listeners won't be executed.

ack / lazy Functions

You may be unfamiliar with the "lazy listener" concept in this framework. To learn more about it, please read bolt-python's documentation: https://slack.dev/bolt-python/concepts#lazy-listeners

The ack function must complete within 3 seconds, while the lazy function can perform time-consuming tasks. It's important to note that not all request handlers support the ack or lazy functions. For more information, please refer to the following table, which covers all the patterns in detail.

Starting from v0.9, if desired, you can execute lazy listeners after the completion of their ack function. To customize the behavior in this manner, you can pass startLazyListenerAfterAck: true as one of the arguments in the App constructor.

Pattern Description ack function lazy function
app.command The passed function handles a slash command request pattern. If the function returns a message, the message will be posted in the channel where the end-user invoked the command.
app.event The passed function asynchronously does something when an Events API request that matches the constraints comes in. Please note that manually acknowledge a request is unsupported. You can pass only one function, which can be executed as a lazy listener. x
app.function [experimental] The passed function asynchronously does something when a "function_executed" event request that matches the constraints comes in. Please note that manually acknowledge a request is unsupported. You can pass only one function, which can be executed as a lazy listener. Also, this feature is still in beta so that the details could be changed on the Slack plaform side until it's GAed. x
app.message / app.anyMessage The passed function asynchronously does something when an a message event comes in. Please note that manually acknowledge a request is unsupported. You can pass only one function, which can be executed as a lazy listener. If the message pattern argument can be any of string, regexp, and undefined. When you pass undefined, the listener matches all messages. x
app.shortcut / app.globalShortcut / app.messageShortcut The passed function handles a global/message shortcut request pattern. Please note that returning a message text in the ack function does not work for shortcuts. Instead, you can use context.respond for it.
app.action The passed function handles a user interaction on a Block Kit component such as button clicks, item selection in a select menu, and so on.
app.options The passed function handles an external data source reqeust for Block Kit select menus. You cannnot respond to this request pattern asynchronously, so slack-edge enables developers to pass only ack function, which must complete within 3 seconds, here. x
app.view / app.viewSubmission / app.viewClosed The passed function handles either a modal data submission or the "Close" button click event. ack function can return various response_actions (errors, update, push, clear) and their associated data. If you want to simply close the modal, you don't need to return anything.

slack-edge's People

Contributors

abc123931 avatar gadfly361 avatar seratch avatar stephentangcook avatar zhawtof avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar

slack-edge's Issues

Unexpected Behavior with SlackOAuthApp's stateStore, onStateValidationError, and callback Functions

I have been using the stateStore setting in SlackOAuthApp and noticed some surprising behavior.

When the consume function returns false, I would expect the onStateValidationError function in the oauth options to get executed, which does occur as expected.

However, the process seems to proceed further than anticipated, and the callback function in the oauth options gets executed as well. Is this the intended behavior or could this possibly be a bug?

Customization of the OAuth flow page rendering

Kazuhiro, interesting project and keep it up.

are you open for Pull Request?

Any flexibility on render page?

slack-cloudflare-workers/node_modules/slack-edge/dist/oauth/oauth-page-renderer.js

It's hard coded. I am finding the other way to modifying it.

Custom Page Redirection After OAuth in SlackOAuthApp

I am trying to modify the behavior of the SlackOAuthApp.
Specifically, after redirecting from '/slack/install' to '/slack/oauth_redirect', I want to display an original page or redirect to another page instead of showing the default renderCompletionPage.
Is there any way to accomplish this?

Interruption of Long Responses in Slack App Using Vercel Edge Functions with OpenAI's Chat API

Hello,

I'm currently developing a Slack application that mentions and asks questions, receiving responses from ChatGPT. This app is built using Vercel Edge Functions and integrates with OpenAI's Chat API in stream mode. The process involves editing the messages using chat.update to create a streaming effect.

However, I'm encountering an issue where the processing of long responses gets interrupted after approximately 30 seconds. This seems to align with the behavior mentioned in an article regarding interruptions in processes handled by waitUntil.

I'm seeking advice or potential solutions to prevent this interruption and ensure longer responses are fully processed. Any suggestions or insights into this matter would be greatly appreciated.

Thank you for your assistance.

[Deno] Potential type issue with SlackApp's SlackEdgeAppEnv

First, thank you so much for this library! I am trying to test this out using Deno and got an unexpected linting error from a README example.

The snippet below is from the README for Deno.

import { SlackApp } from "https://deno.land/x/[email protected]/mod.ts";

const app = new SlackApp({
  env: {
    SLACK_SIGNING_SECRET: Deno.env.get("SLACK_SIGNING_SECRET"),
    SLACK_BOT_TOKEN: Deno.env.get("SLACK_BOT_TOKEN"),
    SLACK_LOGGING_LEVEL: "DEBUG",
  },
});

I am seeing a linter issue with a squiggly line under env that seems to be complaining about types. The message is as follows:

Type '{ SLACK_SIGNING_SECRET: string | undefined; SLACK_BOT_TOKEN: string | undefined; SLACK_LOGGING_LEVEL: "DEBUG"; }' is not assignable to type 'SlackEdgeAppEnv | SlackSocketModeAppEnv'.
  Type '{ SLACK_SIGNING_SECRET: string | undefined; SLACK_BOT_TOKEN: string | undefined; SLACK_LOGGING_LEVEL: "DEBUG"; }' is not assignable to type 'SlackSocketModeAppEnv'.
    Property 'SLACK_APP_TOKEN' is missing in type '{ SLACK_SIGNING_SECRET: string | undefined; SLACK_BOT_TOKEN: string | undefined; SLACK_LOGGING_LEVEL: "DEBUG"; }' but required in type '{ SLACK_SIGNING_SECRET?: string | undefined; SLACK_BOT_TOKEN?: string | undefined; SLACK_APP_TOKEN: string; }'.

The env appears to take in SlackAppOptions and it can take in SlackEdgeAppEnv or SlackSocketModeAppEnv.

export interface SlackAppOptions<
  E extends SlackEdgeAppEnv | SlackSocketModeAppEnv,
> {

I think the README example is trying to make use of SlackEdgeAppEnv, so looking at that I see:

export type SlackAppEnv = SlackLoggingLevel & {
  SLACK_SIGNING_SECRET?: string;
  SLACK_BOT_TOKEN?: string;
  SLACK_APP_TOKEN?: string;
};

export type SlackEdgeAppEnv = SlackAppEnv & {
  SLACK_SIGNING_SECRET: string;
  SLACK_BOT_TOKEN?: string;
};

Should SlackAppEnv (that SlackEdgeAppEnv depends on) include the SLACK_APP_TOKEN? If so, does the README example need to be updated?


image

Completing the Ack handler of a slash command prematurely terminates the lazy handler

Slack documentation says that you need to acknowledge a slash command to prevent it showing a timeout error to the user after 3 seconds, but slack-edge terminates the SlashCommandLazyHandler as soon as the SlashCommandAckHandler completes.

Below is a contrived example. I discovered this when trying to call an API function that took several seconds to complete.

function wait(ms) {
  return new Promise(resolve => {setTimeout(resolve, ms));
}

app.command( '/your_command',
  async ({}) => {
    return 'done'
  },
  async ({ context }) => {
    await wait(2000)
    context.respond({ text: 'You will never see this'})
  }
)

[Discussion] Lazy vs Ack Race Condition

Hey @seratch, we've recently run into a section of your code that we believe might have a better solution.

slack-edge/src/app.ts

Lines 667 to 668 in 21f9178

ctx.waitUntil(handler.lazy(slackRequest));
const slackResponse = await handler.ack(slackRequest);

In the above code, the lazy function is triggered before the ack function. While this might be intended, we believe that the lazy function would be substantially more useful if it was triggered after the ack function.

Our thoughts in no particular order:

  1. If lazy and ack both access and update the same information, this will cause a race condition.
  2. lazy by its name implies that it would follow Lazy evaluation and only be triggered when resources are available. By triggering before ack, it still hogs resources that should be used for ack even if the processing is parallel.
  3. It's far less useful than a lazy function after the ack. A lazy function at the end of a time-sensitive ack could perform operations that are unnecessary for client resolution.

Real example

At the very onset of our product onboarding, we create an object that represents our platform. After our platform is created, we wanted to perform follow-on tasks that would continue preparing the rest of the platform behind the scenes. However, because lazy evaluates before ack evaluates, we cannot run any follow-on tasks since there is a race condition that the platform has yet to be created.

Our current solution

Our solution (which I will add is perfectly reasonable) is to pass down the Cloudflare ExecutionContext and run waitUntil within the ack directly. We mention the suggestions because we believe it would improve the slack-edge platform by abstracting Cloudflare capabilities.

Our suggestion

1) Swap the order of ack and lazy

const slackResponse = await handler.ack(slackRequest); 
ctx.waitUntil(handler.lazy(slackRequest)); 

2) Introduce 3 commands. preAck, ack, postAck.

ctx.waitUntil(handler.preAck(slackRequest)); 
const slackResponse = await handler.ack(slackRequest); 
ctx.waitUntil(handler.postAck(slackRequest)); 

Would love to hear your thoughts! We love the platform and we're excited to scale with it!

SLACK_BOT_SCOPES and SLACK_USER_SCOPES should not be Env variables

Maybe I'm missing the intention of making scopes environment variables but I can see some clear disadvantages:

  1. Using Slack APIs sometimes requires an update to the scopes for an application. They should be written into the code as constants so that they can be tracked across version control. Rarely are scopes environment specific between development, QA, and prod.
  2. Requiring a global set of scopes prevents the developer from using a limited subset of scopes for some users and a more expansive set of scopes for others. Scopes as an environment variable prevent variations at runtime.

TypeError in request-verification.js after upgrading from Next.js v13 to v14 with Edge Functions

We recently upgraded our project from Next.js v13.3.1 to v14.2.1 and are using slack-edge v0.10.9.

After upgrading our project from Next.js v13 to v14 and utilizing Edge Functions, we started encountering a TypeError when attempting to verify a request using the SubtleCrypto.verify method in the request-verification.js module.

The error message indicates that the third argument passed to SubtleCrypto.verify is not an instance of ArrayBuffer, Buffer, TypedArray, or DataView. This issue only arose after the upgrade, suggesting a potential compatibility problem with the new version of Next.js or its implementation of Edge Functions. Here is the complete error message for reference:

⨯ Error [TypeError]: Failed to execute 'verify' on 'SubtleCrypto': 3rd argument is not instance of ArrayBuffer, Buffer, TypedArray, or DataView.
     at codedTypeError (node:internal/crypto/webidl:45:15)
     at makeException (node:internal/crypto/webidl:54:10)
     at converters.BufferSource (node:internal/crypto/webidl:218:11)
     at SubtleCrypto.verify (node:internal/crypto/webcrypto:870:33)
     at verifySlackRequest (webpack-internal:///(middleware)/../../node_modules/slack-edge/dist/request/request-verification.js:20:32)
     at process.processTicksAndRejections (node:internal/process/task_queues:95:5)

I would appreciate any guidance on how to address this issue or any updates if this is a known bug, especially in relation to changes between Next.js v13 and v14. Thank you for your assistance.

removing handlers

Thank you for creating this library! I have a suggestion to enhance its functionality by allowing handlers to be removed.

The reason for this proposal is to address issues encountered when using handlers in a persistent manner within durable objects. Currently, it seems there's no way to remove listeners, which necessitates reinstantiating the client. This can lead to memory leaks and corrupted handlers.

Here are a few potential solutions, each with its own advantages and disadvantages:

  1. Change handler arrays to protected:

    • Remove the private enforcement on the SlackApp handler arrays and change them to protected.
    • This would enable users to implement the removal of listeners at their discretion.
    • It wouldn’t affect the library's runtime and would maintain backward compatibility with existing users.
  2. Use WeakRefs in the handler arrays:

    • Implement WeakRefs inside the arrays and validate the references before calling them.
    • This approach allows the garbage collector to manage memory efficiently, preventing leaks.
    • Although this requires a significant amount of code changes, it maintains backward compatibility and keeps the private attributes intact.
  3. Add methods to facilitate handler removal:

    • Introduce new methods in the library specifically for removing handlers.
    • While this would require additional code changes and testing, it would also increase the library’s size.

By implementing one of these solutions, the library can better handle persistent use cases, preventing memory issues and enhancing overall stability.

Reacting app_mention and message events

[UPDATED]
This was my misunderstanding of how Slack sends events. I was confused if Slack catches a message then Slack sends one request that contains multiple events. In fact, Slack sends a request per event type, not per message 😥
https://api.slack.com/apis/connections/events-api#receiving-events


I wrote like this below. When a message has app_mention is received, not only A is reacted but also B.

app.event('app_mention', async ({ context, payload }) => {
  // Event A
});
app.event('message', async ({ context, payload }) => {
  // Event B
});

I can understand app_mention messages can be message but I'm feeling strange.
Is it common sense or do I miss something? How can I complete the process at only A if the app_mention messages are received?

Thank you for your help.

Issue running Vercel Edge Functions example from README. SlackApp is not returning challenge from API Verification + More

Hi there, I am running into a few issues using the SlackApp object in a NextJS project using App Router. I might be missing some configurations but as I follow the example in the README for the Vercel Edge Functions I run into some problems you may be able to shed some light on.

package.json:

{
  "name": "ask-ai-slackbot",
  "version": "0.1.0",
  "private": true,
  "scripts": {
    "dev": "next dev",
    "build": "next build",
    "start": "next start",
    "lint": "next lint"
  },
  "dependencies": {
    "@slack/web-api": "^7.0.4",
    "ai": "^3.0.24",
    "next": "14.2.2",
    "openai": "^4.38.1",
    "react": "^18",
    "react-dom": "^18",
    "slack-edge": "0.11.0"
  },
  "devDependencies": {
    "@types/node": "^20",
    "@types/react": "^18",
    "@types/react-dom": "^18",
    "eslint": "^8",
    "eslint-config-next": "14.2.2",
    "postcss": "^8",
    "tailwindcss": "^3.4.1",
    "typescript": "^5"
  }
}

Code Snippet 1:

// src/app/api/slack/route.ts
import type { NextFetchEvent, NextRequest } from 'next/server';
import { SlackApp } from 'slack-edge';

export const config = {
  runtime: 'edge',
};
export const dynamic  =  'force-dynamic';

const app = new SlackApp({
  env: {
    SLACK_SIGNING_SECRET: process.env.SLACK_SIGNING_SECRET!,
    SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN!,
    SLACK_LOGGING_LEVEL: 'DEBUG',
  },
});

app.event('app_mention', async ({ context, payload }) => {
  console.log(context, payload);
});

app.event('message', async ({ context, payload }) => {
  console.log(context, payload);
});

export  async  function  POST(req:  NextRequest, context: NextFetchEvent) {
  return  await  app.run(req, context);
}

When challenging this endpoint api/slack from Slack's Event Subscription there is no successful response to verify the endpoint. Is there any missing configuration to enable the SlackApp to return the challenge? I can see the app is returning a 401 response to the challenge request.

In an attempt to get pass the verification step I manually return the challenge in the following updated snippet. However, then I run into an error originating from the slack-edge library when hitting the endpoint from an app mention in Slack.

Code Snippet 2:

// src/app/api/slack/route.ts
import type { NextFetchEvent, NextRequest } from 'next/server';
import { SlackApp } from 'slack-edge';

export const config = {
  runtime: 'edge',
};
export const dynamic  =  'force-dynamic';

const app = new SlackApp({
  env: {
    SLACK_SIGNING_SECRET: process.env.SLACK_SIGNING_SECRET!,
    SLACK_BOT_TOKEN: process.env.SLACK_BOT_TOKEN!,
    SLACK_LOGGING_LEVEL: 'DEBUG',
  },
});

app.event('app_mention', async ({ context, payload }) => {
  console.log(context, payload);
});

app.event('message', async ({ context, payload }) => {
  console.log(context, payload);
});

export  async  function  POST(req:  NextRequest, context: NextFetchEvent) {
  // Manually return challenge start
  const blobRequestBody = await req.blob();
  const rawBody = await blobRequestBody.text();
  const body = JSON.parse(rawBody);
  const { type: requestType } = body;

  if (requestType === 'url_verification') {
    return new Response(body.challenge, { status: 200 });
  }
  // Manually return challenge end

  return  await  app.run(req, context);
}

Error from Code Snippet 2:

POST /api/slack 500 in 50ms
 ⨯ TypeError: Body is unusable
    at specConsumeBody (node:internal/deps/undici/undici:4712:15)
    at NextRequest.blob (node:internal/deps/undici/undici:4595:18)
    at SlackApp.handleEventRequest (webpack-internal:///(rsc)/./node_modules/slack-edge/dist/app.js:477:47)
    at SlackApp.run (webpack-internal:///(rsc)/./node_modules/slack-edge/dist/app.js:441:27)
    at POST (webpack-internal:///(rsc)/./src/app/api/slack/route.ts:37:22)

It appears SlackApp.run is having issues calling .blob on the NextRequest. This might be because I already called .blob on the request during my manual challenge verification step. But without that I can not reach the point in the route handler to run SlackApp.run.

Perhaps I am missing something here or configured something incorrectly. Thank you for any feedback!

The anyMessage() function is misleading

Context

According to the following code, AnyMessageEvent can be any of the following message events.

export type AnyMessageEvent =
| GenericMessageEvent
| BotMessageEvent
| ChannelArchiveMessageEvent
| ChannelJoinMessageEvent
| ChannelLeaveMessageEvent
| ChannelNameMessageEvent
| ChannelPostingPermissionsMessageEvent
| ChannelPurposeMessageEvent
| ChannelTopicMessageEvent
| ChannelUnarchiveMessageEvent
| EKMAccessDeniedMessageEvent
| FileShareMessageEvent
| MeMessageEvent
| MessageChangedEvent
| MessageDeletedEvent
| MessageRepliedEvent
| ThreadBroadcastMessageEvent;

As part of isPostedMessageEvent, the only messages that are allowed are the 4 types:

  • GenericMessageEvent
  • BotMessageEvent
  • FileShareMessageEvent
  • ThreadBroadcastMessageEvent
    export const isPostedMessageEvent = (event: {
    type: string;
    subtype?: string;
    }): event is
    | GenericMessageEvent
    | BotMessageEvent
    | FileShareMessageEvent
    | ThreadBroadcastMessageEvent => {
    return (
    event.subtype === undefined ||
    event.subtype === "bot_message" ||
    event.subtype === "file_share" ||
    event.subtype === "thread_broadcast"
    );
    };

The Problem

The issue is that isPostedMessageEvent is part of the critical path of .anyMessage() which now only executes the function for those 4 subtypes of messages.

slack-edge/src/app.ts

Lines 271 to 286 in 05eecb8

if (isPostedMessageEvent(body.event)) {
let matched = true;
if (pattern !== undefined) {
if (typeof pattern === "string") {
matched = body.event.text!.includes(pattern);
}
if (typeof pattern === "object") {
matched = body.event.text!.match(pattern) !== null;
}
}
if (matched) {
// deno-lint-ignore require-await
return { ack: async (_: EventRequest<E, "message">) => "", lazy };
}
}
return null;

This is misleading as one would assume the AnyMessageEvent and anyMessage() functions would support the same message types.

Recommendations

  1. Remove isPostedMessageEvent from the message() logic. Unless I'm missing something, this seems like the obvious move to conform with the current Slack Web API
  2. Provide an additional function to filter all message events by subtype. Something along the lines of:
    app.messageEvent('message_changed', lazyHandler())
    I would also recommend renaming app.message() and app.anyMessage() to app.postedMessage() and app.anyPostedMessage() respectively
  3. The minimum would be to update the documentation to mention other message types (e.g. message_changed, channel_topic) should use an EventLazyHandler<'message'> for handling.

Thanks

Huge fan of the framework Kaz. We've done a lot of work migrating over to Cloudflare, but this one caught us up during the transition.

Interactivity routing support

I'm trying it now I realized there is only supported events routing.

slack-edge/src/app.ts

Lines 529 to 534 in 1b7346f

if (this.routes.events) {
const { pathname } = new URL(request.url);
if (pathname !== this.routes.events) {
return new Response("Not found", { status: 404 });
}
}

However, the console has an input field for the interactivity path we have to.

image

But if we set it to the same event_subscriptions path, it seems to work well.

  1. What's the correct?
  2. Is there no problem with the wired setting?

Question: FileElement Cc vs EmailAddress?

There seem to be discrepancies between FileElement on slack-edge and FileElement on slack-web-api-client. This is not the only one but it is one of the problems that prevents them from being used together without type casting.

Here it is given a type EmailAddress

cc?: EmailAddress[];

Here it is given a type Cc
https://github.com/seratch/slack-web-api-client/blob/855308a88ab3f28e0da91684f9cdc53cb5456688/src/client/generated-response/ConversationsRepliesResponse.ts#L353

The problem is most evident in that EmailAddress has required properties and Cc has optional properties.

export interface EmailAddress {
address: string;
name: string;
original: string;
}

https://github.com/seratch/slack-web-api-client/blob/855308a88ab3f28e0da91684f9cdc53cb5456688/src/client/generated-response/ConversationsRepliesResponse.ts#L499-L503


I was hoping to make a PR to fix the overlap but I don't know which one was more correct given the direction you're building in. Thanks!

TypeScript usage

Hi,
I'm really confused about how to use types with this package.
For example if I look at the payload I receive when sending a message into a channel:

{
   "token":"0JW1qXXXXXX",
   "team_id":"T06MXXXXXX",
   "context_team_id":"T06MXXXXX",
   "context_enterprise_id":null,
   "api_app_id":"A06XXXXXX",
   "event":{
      "user":"U0XXXXXX",
      "type":"message",
      "ts":"17098XXXX.2XXXX",
      "client_msg_id":"43055b13-d578-XXXXX",
      "text":"test",
      "team":"T06MXXXXXX",
      "blocks":[
         {
            "type":"rich_text",
            "block_id":"gB9fq",
            "elements":[
               {
                  "type":"rich_text_section",
                  "elements":[
                     {
                        "type":"text",
                        "text":"test"
                     }
                  ]
               }
            ]
         }
      ],
      "channel":"C06MXXXXX",
      "event_ts":"1709806507.XXXXXX",
      "channel_type":"channel"
   },
   "type":"event_callback",
   "event_id":"Ev06NFBXXXXX",
   "event_time":1709806507,
   "authorizations":[
      {
         "enterprise_id":null,
         "team_id":"T06MXXXXX",
         "user_id":"U06MXXXXX",
         "is_bot":true,
         "is_enterprise_install":false
      }
   ],
   "is_ext_shared_channel":false,
   "event_context":"4-eyJldCI6Im1lc3NhZ2UiLCJ0aWQiOiJUMDZNMiQzA2TURIQTdaTFoifQ"
}

But I am not able to find a type which matches this payload with this package?

Thanks!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.