Skip to content

Slack Handlers Documentation

This document provides technical documentation for all Slack event handlers in the Yappa Knowledge Hub bot.

Architecture Overview

The Slack bot uses the Bolt framework for Node.js with Socket Mode. Handlers are organized by responsibility:

  • events.ts - Message and file events
  • commands.ts - Slash commands
  • actions.ts - Button clicks and select menus
  • submissions.ts - Modal submissions and closures
  • shortcuts.ts - Global and message shortcuts
  • home.ts - App Home tab
  • dashboard.ts - Dashboard modal logic
  • items.ts - Knowledge item operations
  • lists.ts - Category/list management
  • saveModals.ts - Save modal builders
  • helpers.ts - Validation and parsing utilities

Handler Registration

Handlers are registered in src/app.ts:

typescript
// Events
app.message(handleMessageEvent);
app.event('file_shared', handleFileShared);

// Commands
app.command('/knowledge', handleKnowledgeCommand);

// Actions
app.action(/.+/, handleActions);

// Shortcuts
app.shortcut('add_to_hub', handleAddToHubShortcut);

// Submissions
app.view('add_knowledge', handleViewSubmission);
app.view({ callback_id: /.+/, type: 'view_closed' }, handleViewClosed);

// App Home
app.event('app_home_opened', handleAppHomeOpened);

events.ts - Message and File Events

Purpose

Detects URLs and files shared in channels, offering to save them to the knowledge hub.

Key Functions

handleMessageEvent()

Processes all message events in channels where the bot is present.

typescript
async function handleMessageEvent({
  message,
  client
}: SlackEventMiddlewareArgs<'message'> & AllMiddlewareArgs): Promise<void>

Flow:

  1. Skip bot messages and messages with subtypes (edits, deletes)
  2. Extract URLs using regex: /(https?:\/\/[^\s<>]+)/g
  3. Call handleUrlDetection() if URLs found

Event Filtering:

  • Ignores messages with subtype (bot messages, edits, etc.)
  • Requires text property
  • Only processes first URL in message

handleUrlDetection()

Scrapes URL metadata and posts interactive message.

typescript
async function handleUrlDetection({
  message,
  urls,
  client
}: {
  message: any;
  urls: string[];
  client: WebClient;
}): Promise<void>

Flow:

  1. Clean URL (remove trailing punctuation)
  2. Validate URL format
  3. Scrape metadata using scrapeUrlMetadata()
  4. Build Block Kit message with:
    • Title and description
    • Preview image (if available)
    • Site name, type, author (context)
    • "Opslaan in Hub" button
  5. Post ephemeral message to user

Button Value:

json
{
  "url": "https://example.com",
  "messageText": "Original message text...",
  "messageTs": "1234567890.123456",
  "channelId": "C01234567",
  "metadata": {
    "title": "Page Title",
    "description": "Page description",
    "image": "https://example.com/image.jpg"
  }
}

handleFileShared()

Detects file shares and offers to save them.

typescript
async function handleFileShared({
  event,
  client
}: SlackEventMiddlewareArgs<'file_shared'> & AllMiddlewareArgs): Promise<void>

Flow:

  1. Fetch file info using files.info API
  2. Extract file details (name, type, URL)
  3. Post ephemeral message with "Opslaan in Hub" button
  4. Button opens save file modal

Supported File Types:

  • PDFs
  • Images (jpg, png, gif)
  • Documents (docx, xlsx, pptx)
  • Text files

Error Handling

  • Catches scraping errors and logs them
  • Falls back to URL-only if metadata unavailable
  • Validates URLs before processing

Dependencies

  • urlScraper.ts - Metadata scraping
  • saveModals.ts - Modal builders

commands.ts - Slash Commands

Purpose

Handles the /knowledge slash command to open the dashboard.

Key Functions

handleKnowledgeCommand()

Opens the knowledge dashboard modal.

typescript
async function handleKnowledgeCommand({
  command,
  ack,
  client
}: SlackCommandMiddlewareArgs & AllMiddlewareArgs): Promise<void>

Flow:

  1. Acknowledge command immediately
  2. Call openDashboard() with trigger_id
  3. Handle errors with ephemeral message

openDashboard()

Opens dashboard modal with loading state, then updates with content.

typescript
async function openDashboard(
  client: WebClient,
  triggerId: string,
  userId: string,
  page: number = 0,
  selectedCategories: string[] = []
): Promise<void>

Flow:

  1. Open modal immediately with loading state (prevents trigger_id expiration)
  2. Fetch categories from cache (fast)
  3. Fetch items only if categories selected
  4. Paginate items (10 per page)
  5. Update modal with full content

Loading State:

typescript
{
  type: 'modal',
  callback_id: 'knowledge_dashboard',
  title: { type: 'plain_text', text: 'Knowledge Hub' },
  blocks: [
    {
      type: 'section',
      text: {
        type: 'mrkdwn',
        text: ':hourglass_flowing_sand: *Laden...*\n_Dashboard wordt geladen_'
      }
    }
  ]
}

Performance Optimization:

  • Opens modal immediately to avoid trigger_id expiration (3 seconds)
  • Uses category cache (30-second TTL)
  • Only loads items when categories selected
  • Pagination reduces payload size

Trigger ID Expiration

Trigger IDs expire after 3 seconds. The loading state pattern ensures the modal opens before expiration, then updates with content.

actions.ts - Button Clicks and Select Menus

Purpose

Routes all action events to appropriate handlers based on action_id.

Key Functions

handleActions()

Main router for all action events.

typescript
async function handleActions({
  ack,
  body,
  action,
  client
}: SlackActionMiddlewareArgs & AllMiddlewareArgs): Promise<void>

Action Routing:

Action IDHandlerDescription
open_add_modalopenAddModalFromAction()Open add modal from dashboard
home_add_knowledgeopenAddModal()Open add modal from App Home
list_select_with_updatehandleCategorySelectionChange()Update target groups on category change
refresh_dashboardrefreshDashboard()Refresh dashboard content
open_searchopenSearchModal()Open search modal
open_digestopenDigestModalFromDashboard()Open digest modal
manage_lists_dashboardhandleManageLists()Open manage lists modal
dashboard_next_pageupdateDashboardPage()Next page
dashboard_prev_pageupdateDashboardPage()Previous page
apply_category_filterapplyDashboardCategoryFilter()Apply category filter
clear_category_filterclearDashboardCategoryFilter()Clear category filter
manage_item_*Item handlersEdit, delete, share operations
browse_list_*handleBrowseList()Browse category items
save_url_to_knowledgeopenSaveUrlModal()Save URL from detection
save_file_to_knowledgeopenSaveFileModal()Save file from detection
save_reaction_to_knowledgeopenSaveReactionModal()Save from reaction

Flow:

  1. Acknowledge action immediately
  2. Extract action_id from action object
  3. Route to appropriate handler
  4. Handle errors with user-facing messages

Item Management Actions

Overflow menu actions follow pattern: manage_item_{itemId}

Value Format: {operation}_{itemId}

Operations:

  • delete_{itemId} - Delete item
  • edit_{itemId} - Edit item
  • share_{itemId} - Share item to channel

Parsing:

typescript
const value = action.selected_option.value;
const underscoreIndex = value.indexOf('_');
const operation = value.substring(0, underscoreIndex);
const itemId = value.substring(underscoreIndex + 1);

Category Selection Change

The list_select_with_update action enables cascading selection:

  1. User selects category
  2. Action handler fetches category's default target groups
  3. Modal is updated with pre-selected target groups

Note: This uses views.update API, not dispatch_action (which doesn't exist in Slack API).

submissions.ts - Modal Submissions

Purpose

Handles modal submissions and view_closed events.

Key Functions

handleViewSubmission()

Processes modal submissions based on callback_id.

typescript
async function handleViewSubmission({
  ack,
  body,
  view,
  client
}: SlackViewMiddlewareArgs<SlackViewAction> & AllMiddlewareArgs): Promise<void>

Callback IDs:

Callback IDHandlerDescription
add_knowledgehandleAddKnowledge()Add new knowledge item
edit_knowledgehandleEditKnowledge()Edit existing item
save_url_modalhandleSaveUrl()Save URL from detection
save_file_modalhandleSaveFile()Save file from detection
add_list_modalhandleAddList()Add new category
edit_list_modalhandleEditList()Edit category
search_modalhandleSearch()Search knowledge

Flow:

  1. Extract callback_id from view
  2. Parse private_metadata (JSON)
  3. Extract form values from view.state.values
  4. Validate input
  5. Call backend API
  6. Acknowledge with success/error
  7. Refresh relevant views

handleAddKnowledge()

Adds new knowledge item to backend and Notion.

typescript
async function handleAddKnowledge(
  ack: any,
  view: any,
  client: WebClient,
  userId: string
): Promise<void>

Form Values:

typescript
const values = view.state.values;
const title = values["title_block"]?.["title_input"]?.value || '';
const url = values["url_block"]?.["url_input"]?.value || '';
const summary = values["summary_block"]?.["summary_input"]?.value || '';
const selectedCategories = values["list_block"]?.["list_select_with_update"]?.selected_options || [];
const selectedTargetGroups = values["target_groups_block"]?.["target_groups_select"]?.selected_options || [];
const tags = values["tags_block"]?.["tags_input"]?.value || '';

Validation:

  • Title required (max 200 chars)
  • URL optional but must be valid format
  • At least one category required
  • At least one target group required

Backend API Call:

typescript
const response = await fetch(`${BACKEND_URL}/api/knowledge`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    title,
    url,
    summary,
    categories: categoryIds,
    targetGroups: targetGroupNames,
    tags: parsedTags,
    source: 'slack',
    addedBy: userId
  })
});

Success Response:

typescript
await ack({
  response_action: 'update',
  view: {
    type: 'modal',
    title: { type: 'plain_text', text: 'Succes!' },
    blocks: [
      {
        type: 'section',
        text: {
          type: 'mrkdwn',
          text: ':white_check_mark: *Item toegevoegd!*\n\nHet item is opgeslagen in de Knowledge Hub.'
        }
      }
    ]
  }
});

Error Response:

typescript
await ack({
  response_action: 'errors',
  errors: {
    title_input: 'Titel is verplicht',
    list_select_with_update: 'Selecteer minimaal n categorie'
  }
});

handleViewClosed()

Cleans up when modals are closed without submission.

typescript
async function handleViewClosed({
  body,
  view
}: SlackViewMiddlewareArgs<SlackViewClosedAction> & AllMiddlewareArgs): Promise<void>

Use Cases:

  • User clicks "Cancel" or "X"
  • Modal times out
  • User presses Escape

Cleanup:

  • Log closure for analytics
  • Clear temporary data
  • No API calls needed

Private Metadata

Modals use private_metadata to pass data between views:

typescript
const metadata = {
  itemId: '123',
  returnView: 'dashboard',
  page: 0,
  filters: ['cat1', 'cat2']
};

view.private_metadata = JSON.stringify(metadata);

Size Limit: 3000 characters

Best Practice: Only store IDs and minimal state, not full objects.

shortcuts.ts - Global and Message Shortcuts

Purpose

Handles the add_to_hub global shortcut.

Key Functions

handleAddToHubShortcut()

Opens add knowledge modal from anywhere in Slack.

typescript
async function handleAddToHubShortcut({
  shortcut,
  ack,
  client
}: SlackShortcutMiddlewareArgs<GlobalShortcut> & AllMiddlewareArgs): Promise<void>

Flow:

  1. Acknowledge shortcut immediately
  2. Fetch categories from cache
  3. Open add modal with pre-populated categories
  4. Handle trigger_id expiration with loading state

Performance Issue:

  • Originally took 3+ seconds due to fetching 26 categories from Notion
  • Fixed with 30-second category cache
  • Pre-cache categories on app startup

Shortcut Configuration:

  • Type: Global shortcut
  • Name: "Toevoegen aan Hub"
  • Callback ID: add_to_hub
  • Description: "Voeg een item toe aan de Knowledge Hub"

helpers.ts - Validation and Parsing

Purpose

Shared utilities for validation and data parsing.

Key Functions

parseTags()

Parses comma-separated tag string into array.

typescript
function parseTags(tagsString: string): string[]

Input: "technology, innovation, AI"Output: ["technology", "innovation", "AI"]

Processing:

  • Split on commas
  • Trim whitespace
  • Remove empty strings
  • Deduplicate

parseMultiSelect()

Extracts values from Slack multi-select options.

typescript
function parseMultiSelect(options: any[]): string[]

Input:

typescript
[
  { value: '1', text: { type: 'plain_text', text: 'Option 1' } },
  { value: '2', text: { type: 'plain_text', text: 'Option 2' } }
]

Output: ['1', '2']

validateAddKnowledge()

Validates add knowledge form data.

typescript
function validateAddKnowledge(data: {
  title: string;
  url?: string;
  categories: string[];
  targetGroups: string[];
}): { valid: boolean; errors: Record<string, string> }

Validation Rules:

  • Title: Required, 1-200 characters
  • URL: Optional, must be valid URL format
  • Categories: At least one required
  • Target Groups: At least one required

Error Format:

typescript
{
  valid: false,
  errors: {
    title_input: 'Titel is verplicht',
    list_select_with_update: 'Selecteer minimaal n categorie'
  }
}

buildAddKnowledgeBlocks()

Builds Block Kit blocks for add knowledge modal.

typescript
function buildAddKnowledgeBlocks(options: {
  categories: Category[];
  targetGroups: TargetGroup[];
  initialValues?: {
    title?: string;
    url?: string;
    summary?: string;
    selectedCategories?: string[];
    selectedTargetGroups?: string[];
    tags?: string;
  };
}): any[]

Block Structure:

  1. Title input (plain_text_input)
  2. URL input (url_text_input)
  3. Summary input (plain_text_input, multiline)
  4. Category select (static_select with list_select_with_update)
  5. Target groups multi-select (multi_static_select)
  6. Tags input (plain_text_input)

Category Select:

typescript
{
  type: 'input',
  block_id: 'list_block',
  label: { type: 'plain_text', text: 'Thematische Lijst' },
  element: {
    type: 'static_select',
    action_id: 'list_select_with_update',
    placeholder: { type: 'plain_text', text: 'Selecteer een lijst' },
    options: categoryOptions
  }
}

Error Handling Patterns

Try-Catch Blocks

All handlers use try-catch for error handling:

typescript
try {
  // Handler logic
} catch (error) {
  console.error('[Handler] Error:', error);
  // User-facing error message
}

User-Facing Errors

Errors are shown to users via:

  1. Ephemeral messages (for actions):
typescript
await client.chat.postEphemeral({
  channel: channelId,
  user: userId,
  text: ':x: Er is een fout opgetreden. Probeer het opnieuw.'
});
  1. Modal errors (for submissions):
typescript
await ack({
  response_action: 'errors',
  errors: {
    field_id: 'Error message'
  }
});
  1. Update modal (for severe errors):
typescript
await ack({
  response_action: 'update',
  view: {
    type: 'modal',
    title: { type: 'plain_text', text: 'Fout' },
    blocks: [/* error blocks */]
  }
});

Logging

All handlers log key events:

typescript
console.log('[Handler] Action:', actionId);
console.log('[Handler] User:', userId);
console.log('[Handler] Error:', error);

Log Format: [HandlerName] Event: details

Performance Considerations

Trigger ID Expiration

Trigger IDs expire after 3 seconds. Strategies:

  1. Loading State Pattern:

    • Open modal immediately with loading message
    • Fetch data asynchronously
    • Update modal with content
  2. Caching:

    • Cache frequently accessed data (categories)
    • Pre-load on app startup
    • Use short TTL (30 seconds)
  3. Minimize API Calls:

    • Batch requests when possible
    • Only fetch required data
    • Use pagination
  • Blocks: Max 100 blocks per view
  • Private Metadata: Max 3000 characters
  • Text: Max 3000 characters per text block

Rate Limiting

Slack API rate limits:

  • Tier 1: 1+ requests per minute
  • Tier 2: 20+ requests per minute
  • Tier 3: 50+ requests per minute
  • Tier 4: 100+ requests per minute

Best Practices:

  • Acknowledge immediately (within 3 seconds)
  • Use views.update instead of views.open when possible
  • Batch operations
  • Implement exponential backoff

Testing

Manual Testing

  1. Message Events:

    • Share URL in channel
    • Verify detection message appears
    • Click "Opslaan in Hub" button
  2. Commands:

    • Run /knowledge command
    • Verify dashboard opens
    • Test pagination and filtering
  3. Shortcuts:

    • Open shortcuts menu (Cmd+Shift+A)
    • Select "Toevoegen aan Hub"
    • Verify modal opens quickly
  4. Modals:

    • Fill out form
    • Submit
    • Verify success message
    • Check backend for saved data

Automated Testing

typescript
import { describe, it, expect, vi } from 'vitest';

describe('handleMessageEvent', () => {
  it('detects URLs in messages', async () => {
    const mockClient = {
      chat: {
        postEphemeral: vi.fn()
      }
    };

    const message = {
      text: 'Check out https://example.com',
      user: 'U123',
      channel: 'C123',
      ts: '1234567890.123456'
    };

    await handleMessageEvent({
      message,
      client: mockClient as any
    });

    expect(mockClient.chat.postEphemeral).toHaveBeenCalled();
  });
});

Next Steps