buster/packages/slack/README.md

319 lines
7.9 KiB
Markdown

# @buster/slack
Standalone Slack integration package with OAuth 2.0, messaging, and channel management capabilities.
## Installation
```bash
pnpm add @buster/slack
```
## Features
- **OAuth 2.0 Authentication** - Complete OAuth flow with CSRF protection
- **Channel Management** - List, validate, join, and leave channels
- **Messaging** - Send messages with blocks, attachments, and threading support
- **Type Safety** - Full TypeScript support with no `any` types
- **Zod Validation** - Runtime validation for all inputs
- **Dependency Injection** - Easy testing with injectable WebClient
- **Framework Agnostic** - Works with any Node.js framework
- **Storage Interfaces** - Bring your own storage implementation
- **Retry Logic** - Automatic retry with exponential backoff
- **Error Handling** - Typed errors with user-friendly messages
## Usage
### OAuth Authentication
```typescript
import { SlackAuthService, ISlackTokenStorage, ISlackOAuthStateStorage } from '@buster/slack';
// Implement storage interfaces
const tokenStorage: ISlackTokenStorage = { /* your implementation */ };
const stateStorage: ISlackOAuthStateStorage = { /* your implementation */ };
const authService = new SlackAuthService(
{
clientId: 'your-client-id',
clientSecret: 'your-client-secret',
redirectUri: 'https://your-app.com/slack/callback',
scopes: ['channels:read', 'chat:write'],
},
tokenStorage,
stateStorage
);
// Generate OAuth URL
const { authUrl, state } = await authService.generateAuthUrl({ userId: 'user-123' });
// Handle OAuth callback
const result = await authService.handleCallback(code, state, 'user-123');
```
### Channel Management
```typescript
import { SlackChannelService } from '@buster/slack';
const channelService = new SlackChannelService();
// Get available channels
const channels = await channelService.getAvailableChannels(accessToken);
// Validate channel access
const channel = await channelService.validateChannelAccess(accessToken, channelId);
// Join a channel
const { success } = await channelService.joinChannel(accessToken, channelId);
// Leave a channel
const leaveResult = await channelService.leaveChannel(accessToken, channelId);
```
### Sending Messages
```typescript
import { SlackMessagingService } from '@buster/slack';
const messagingService = new SlackMessagingService();
// Send a simple message
const result = await messagingService.sendMessage(
accessToken,
channelId,
{ text: 'Hello, Slack!' }
);
// Send a message with blocks
const richMessage = await messagingService.sendMessage(
accessToken,
channelId,
{
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: '*Important Update*\nThis is a formatted message.',
},
},
],
}
);
// Send with automatic retry
const retryResult = await messagingService.sendMessageWithRetry(
accessToken,
channelId,
{ text: 'Important notification' },
3 // max retries
);
```
### Threading and Replies
```typescript
// Send initial message
const { messageTs } = await messagingService.sendMessage(
accessToken,
channelId,
{ text: 'Deployment started...' }
);
// Reply to the message
await messagingService.replyToMessage(
accessToken,
channelId,
messageTs,
{ text: 'Deployment completed successfully!' }
);
```
### Message Tracking
Implement the `ISlackMessageTracking` interface to enable message tracking for threading:
```typescript
import { ISlackMessageTracking, MessageTrackingData } from '@buster/slack';
class MyMessageTracking implements ISlackMessageTracking {
async storeMessageTracking(trackingData: MessageTrackingData): Promise<void> {
// Store message mapping in your database
}
async getMessageTracking(internalMessageId: string): Promise<MessageTrackingData | null> {
// Retrieve message mapping
}
// ... other methods
}
```
### Message Formatting Utilities
```typescript
import {
formatSimpleMessage,
formatBlockMessage,
createSectionBlock,
createActionsBlock,
createContextBlock,
createDividerBlock,
MessageTemplates
} from '@buster/slack';
// Simple message
const message = formatSimpleMessage('Hello, Slack!');
// Message with blocks
const blockMessage = formatBlockMessage([
createSectionBlock('*Important Announcement*', { type: 'mrkdwn' }),
createDividerBlock(),
createContextBlock(['Posted by bot at ' + new Date().toISOString()])
]);
// Deployment notification template
const deploymentMessage = MessageTemplates.deployment({
project: 'my-app',
environment: 'production',
version: '1.2.3',
status: 'success',
duration: '2m 30s',
url: 'https://example.com/deployments/123'
});
// Alert message template
const alertMessage = MessageTemplates.alert({
title: 'High CPU Usage',
message: 'CPU usage exceeded 90% threshold',
severity: 'warning',
source: 'monitoring-system',
actions: [
{ text: 'View Dashboard', url: 'https://example.com/dashboard' }
]
});
// Review flagging template
const reviewMessage = MessageTemplates.reviewFlag({
reviewerName: 'Jane Doe',
profileUrl: 'https://example.com/profile/jane',
issueTitle: 'Data Quality Issue',
description: 'The query returned unexpected results that may indicate a data integrity problem.'
});
// Update and delete messages
await messagingService.updateMessage(accessToken, channelId, messageTs, {
text: 'Updated message content'
});
await messagingService.deleteMessage(accessToken, channelId, messageTs);
```
## Testing
The package includes comprehensive test coverage and supports dependency injection for easy testing:
```typescript
import { SlackMessagingService } from '@buster/slack';
import { WebClient } from '@slack/web-api';
// Create a mock WebClient for testing
const mockClient = {
chat: {
postMessage: jest.fn().mockResolvedValue({ ok: true, ts: '123' })
}
} as unknown as WebClient;
// Inject the mock client
const messagingService = new SlackMessagingService(mockClient);
// Test your integration
const result = await messagingService.sendMessage(
'test-token',
'C123',
{ text: 'Test message' }
);
```
## Error Handling
All services provide typed errors for different scenarios:
```typescript
import { SlackIntegrationError } from '@buster/slack';
try {
await messagingService.sendMessage(token, channelId, message);
} catch (error) {
if (error instanceof SlackIntegrationError) {
switch (error.code) {
case 'INVALID_TOKEN':
// Handle invalid token
break;
case 'CHANNEL_NOT_FOUND':
// Handle channel not found
break;
case 'RATE_LIMITED':
// Handle rate limiting
break;
}
}
}
```
## Development
```bash
# Install dependencies
pnpm install
# Type check
pnpm run typecheck
# Build
pnpm run build
# Test
pnpm run test
# Lint and format
pnpm run check:fix
```
## Integration Testing
Want to test against your real Slack workspace? It's super easy:
1. **Create a Slack app** at https://api.slack.com/apps
2. **Copy `.env.example` to `.env`**
3. **Add just 2 values:**
- `SLACK_BOT_TOKEN` - Your bot token from OAuth & Permissions page
- `SLACK_CHANNEL_ID` - Any channel ID (right-click channel → View details)
4. **Run the tests:**
```bash
pnpm run test:integration
```
That's it! The integration tests will:
- ✅ Validate your bot token
- ✅ Check channel access
- ✅ Send test messages
- ✅ Test message updates
- ✅ Test threading
- ✅ List available channels
- ✅ Test OAuth flows
- ✅ Test error handling
All tests run in seconds and show clear pass/fail results. Integration tests are in `.int.test.ts` files alongside the service files.
## Architecture
This package follows a clean architecture with:
- **Services** - Core business logic (auth, channels, messaging)
- **Interfaces** - Storage contracts for tokens and state
- **Types** - TypeScript types and Zod schemas
- **Utils** - Helper functions and formatters
- **Mocks** - Testing utilities
All code is strictly typed with no `any` or `unknown` types, ensuring type safety throughout your application.