diff --git a/README.md b/README.md index ba98546..e2a02b5 100644 --- a/README.md +++ b/README.md @@ -1,927 +1,268 @@ -# ๐Ÿš€ Universal Bot Platform Wireframe +# Wireframe Notification System -

- English | ะ ัƒััะบะธะน -

+Universal notification system for the Wireframe platform with support for multiple messaging platforms. -

- TypeScript - Cloudflare Workers - Telegram - Type Safety: 100% - License: MIT -

+## Features -

- Production-ready wireframe for creating any bots (Telegram, Discord, Slack) on any cloud platform (Cloudflare Workers, AWS Lambda, Google Cloud) with TypeScript 100% strict mode -

+- **Platform-agnostic design** - Works with any messaging platform +- **Retry logic** - Automatic retry with exponential backoff +- **Batch processing** - Efficient bulk notifications +- **User preferences** - Respect user notification settings +- **Event-driven** - Full event tracking for monitoring +- **TypeScript 100% strict** - Type-safe implementation -

- Features โ€ข - Quick Start โ€ข - Architecture โ€ข - Deployment โ€ข - Documentation โ€ข - Roadmap -

+## Architecture ---- +The notification system consists of three main components: -## ๐Ÿ†• What's New in v1.3 +1. **NotificationService** - High-level business logic +2. **NotificationConnector** - Handles delivery, retries, and batching +3. **NotificationAdapter** - Platform-specific implementation -### ๐Ÿค– Automated Contribution System - -- **Interactive CLI tool** - `npm run contribute` for streamlined contributions -- **Auto-detection** - Identifies valuable patterns from your changes -- **Git worktree support** - Perfect for parallel development -- **Test generation** - Automatically creates appropriate tests - -### ๐ŸŒ Namespace-based i18n Architecture - -- **Organized translations** - Migrated from flat keys to namespaces -- **Platform formatters** - Telegram, Discord, Slack specific formatting -- **Multiple providers** - Static JSON and dynamic KV storage -- **Performance optimized** - Works within Cloudflare free tier limits - -### ๐ŸŽฏ Universal Platform Architecture - -- **Multi-cloud support** - Deploy on Cloudflare, AWS, GCP, or any cloud -- **Multi-messenger support** - Telegram, Discord, Slack, WhatsApp ready -- **ResourceConstraints** - Platform-agnostic resource management -- **Platform abstraction** - Zero code changes when switching providers -- **Event-driven architecture** with EventBus for decoupled communication -- **Service connectors** for AI, Session, and Payment services -- **Plugin system** for extensible functionality - -### Breaking Changes - -- No backward compatibility with v1.x -- TelegramAdapter replaced with TelegramConnector -- All services now communicate through EventBus -- Direct Cloudflare dependencies replaced with platform interfaces - -## โšก Quick Start with Claude Code - -

- Claude Code Ready - AI Friendly -

- -Start your bot with one command: - -```bash -Clone and setup github.com/talkstream/typescript-wireframe-platform -``` - -Claude Code will guide you through: - -- โœ… Installing dependencies -- โœ… Setting up MCP servers if needed -- โœ… Creating your Telegram bot -- โœ… Configuring Cloudflare resources -- โœ… Running tests and starting locally - -[Full AI Setup Instructions](./CLAUDE_SETUP.md) | [Manual Setup](#-quick-start-manual-setup) - ---- - -## ๐Ÿ’ซ Support the Project - -This wireframe is crafted with passion and care, drawing from decades of experience in IT communities and modern technical ecosystems. It's built by someone who believes that great tools should be both powerful and delightful to use. - -Every architectural decision here reflects a deep understanding of what developers need โ€” not just technically, but experientially. This is code that respects your time and intelligence. - -If this wireframe resonates with your vision of what development tools should be, consider supporting its continued evolution: - -**Cryptocurrency:** - -- **TON**: `UQCASJtr_1FfSjcLW_mnx8WuKxT18fXEv5zHrfHhkrwQj2lT` -- **USDT (BEP20)**: `0x16DD8C11BFF0D85D934789C25f77a1def24772F1` -- **USDT (TRC20)**: `TR333FszR3b7crQR4mNufw56vRWxbTTTxS` - -_Your support is invested thoughtfully into making this project even better. Thank you for being part of this journey._ - ---- - -## ๐ŸŒŸ Features - -### Core Technologies - -- **โ˜๏ธ Multi-Cloud** - Deploy on Cloudflare, AWS, GCP, Azure, or any cloud -- **๐Ÿ“˜ TypeScript** - Full type safety with strict mode -- **๐Ÿค– grammY** - Modern Telegram Bot framework (extensible to Discord, Slack, etc.) -- **๐Ÿ—„๏ธ SQL Database** - Platform-agnostic database interface (D1, RDS, Cloud SQL) -- **๐Ÿ’พ KV Storage** - Universal key-value storage abstraction -- **๐Ÿง  Multi-Provider AI** - Support for Google Gemini, OpenAI, xAI Grok, DeepSeek, Cloudflare AI -- **๐Ÿ” Sentry** - Error tracking and performance monitoring -- **๐Ÿ”Œ Plugin System** - Extend with custom functionality - -### Developer Experience - -- **๐Ÿ“ฆ Zero-config setup** - Start developing immediately -- **๐Ÿงช Testing suite** - Unit and integration tests with Vitest -- **๐Ÿ”ง Hot reload** - Instant feedback during development -- **๐Ÿ“ 100% Type safety** - No `any` types, full TypeScript strict mode -- **๐ŸŽฏ ESLint + Prettier** - Consistent code style with ESLint v9 -- **๐Ÿš€ CI/CD Pipeline** - GitHub Actions ready -- **โ˜๏ธ Istanbul Coverage** - Compatible with Cloudflare Workers runtime - -### Security & Performance - -- **๐Ÿ”’ Webhook validation** - Secure token-based authentication -- **โšก Rate limiting** - Distributed rate limiting with KV -- **๐Ÿ›ก๏ธ Security headers** - Best practices implemented -- **๐Ÿ“Š Health checks** - Monitor all dependencies -- **๐Ÿ”„ Session management** - Persistent user sessions -- **๐Ÿ’ฐ Telegram Stars** - Payment integration ready -- **๐ŸŽจ Provider Abstraction** - Switch AI providers without code changes -- **๐Ÿ’ธ Cost Tracking** - Monitor AI usage and costs across providers - -### Cloudflare Workers Tier Optimization - -- **๐Ÿ†“ Cloudflare Workers Free Plan** - Optimized for 10ms CPU limit -- **๐Ÿ’Ž Cloudflare Workers Paid Plan** - Full features with extended timeouts -- **๐Ÿš€ Auto-scaling** - Tier-aware resource management -- **โšก Request Batching** - Reduce API overhead -- **๐Ÿ”„ Smart Caching** - Multi-layer cache system -- **โฑ๏ธ Timeout Protection** - Configurable API timeouts - -## ๐ŸŽฏ Cloudflare Workers Performance Tiers - -> **๐Ÿ“Œ Important**: This wireframe is **100% free and open-source**. The tiers below refer to **Cloudflare Workers plans**, not our wireframe. You can use this wireframe for free forever, regardless of which Cloudflare plan you choose. - -### Cloudflare Workers Free Plan (10ms CPU limit) - -- **Lightweight mode** - Minimal features for fast responses -- **Aggressive caching** - Reduce KV operations (1K writes/day limit) -- **Request batching** - Optimize Telegram API calls -- **Limited AI features** - Disabled by default to save processing time -- **Sequential operations** - Avoid parallel processing overhead - -### Cloudflare Workers Paid Plan (30s CPU limit) - -- **Full feature set** - All capabilities enabled -- **AI integration** - Multiple LLM providers with smart retries -- **Parallel processing** - Concurrent health checks & operations -- **Advanced caching** - Edge cache + KV + memory layers -- **Extended timeouts** - Configurable per operation type - -### Tier Configuration - -```bash -# Set your Cloudflare Workers plan in .dev.vars or wrangler.toml -TIER="free" # for Cloudflare Workers Free Plan -TIER="paid" # for Cloudflare Workers Paid Plan -``` - -The wireframe automatically optimizes based on your Cloudflare Workers plan: - -- **Free Plan**: Fast responses, limited features (optimized for 10ms CPU limit) -- **Paid Plan**: Full functionality, better reliability (up to 30s CPU time) - -## ๐ŸŒฉ๏ธ Choose Your Cloud Platform - -Wireframe v1.2 supports multiple cloud platforms out of the box: - -```bash -# Set your preferred cloud platform -CLOUD_PLATFORM=cloudflare # Default: Cloudflare Workers -CLOUD_PLATFORM=aws # AWS Lambda + DynamoDB -CLOUD_PLATFORM=gcp # Google Cloud Functions -``` - -[Learn more about multi-cloud deployment โ†’](docs/CLOUD_PLATFORMS.md) - -## ๐Ÿš€ Quick Start (Manual Setup) - -> **๐Ÿ“– Need detailed setup instructions?** Check out our comprehensive [Setup Guide](SETUP.md) for step-by-step configuration with screenshots and troubleshooting. - -### One-Command Deploy - -```bash -# Clone and deploy a working Telegram bot in 5 minutes -git clone https://github.com/talkstream/typescript-wireframe-platform.git -cd typescript-wireframe-platform -npm install -npm run setup:bot # Interactive setup wizard -``` - -The setup wizard will: - -- โœ… Create your Telegram bot via @BotFather -- โœ… Configure all required secrets -- โœ… Create KV namespaces and D1 database -- โœ… Deploy to Cloudflare Workers -- โœ… Set up webhook automatically - -### Prerequisites - -- Node.js 20+ and npm 10+ -- [Cloudflare account](https://dash.cloudflare.com/sign-up) -- [Telegram Bot Token](https://t.me/botfather) -- AI Provider API Key (optional) - [Google Gemini](https://makersuite.google.com/app/apikey), [OpenAI](https://platform.openai.com/api-keys), [xAI](https://console.x.ai), [DeepSeek](https://platform.deepseek.com), or Cloudflare AI -- [Wrangler CLI](https://developers.cloudflare.com/workers/cli-wrangler/install-update) - -### 1. Clone and Install - -```bash -git clone https://github.com/talkstream/typescript-wireframe-platform.git -cd typescript-wireframe-platform -npm install - -# Verify setup -npm run setup:check -``` - -### 2. Configure Environment - -```bash -# Copy example configuration -cp .dev.vars.example .dev.vars -cp wrangler.toml.example wrangler.toml - -# Edit .dev.vars with your values -# TELEGRAM_BOT_TOKEN=your_bot_token_here -# TELEGRAM_WEBHOOK_SECRET=your_secret_here -# AI_PROVIDER=google-ai # Options: google-ai, openai, xai, deepseek, cloudflare-ai -# GEMINI_API_KEY=your_gemini_key_here # For Google Gemini -# See .dev.vars.example for more AI provider options ``` - -### 3. Create D1 Database - -```bash -# Create database -wrangler d1 create your-bot-db - -# Update wrangler.toml with the database ID -# Run migrations -npm run db:apply:local +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ NotificationService โ”‚ (Business Logic) +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚NotificationConnectorโ”‚ (Delivery & Retry) +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ผโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ NotificationAdapter โ”‚ (Platform-Specific) +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ ``` -### 4. Create KV Namespaces +## Installation ```bash -wrangler kv:namespace create CACHE -wrangler kv:namespace create RATE_LIMIT -wrangler kv:namespace create SESSIONS +npm install @wireframe/notification-system ``` -### 5. Start Development +## Quick Start -```bash -npm run dev -``` +### 1. Implement a Platform Adapter -Your bot is now running locally! Set the webhook URL to test: - -```bash -curl -X POST "https://api.telegram.org/bot/setWebhook" \ - -H "Content-Type: application/json" \ - -d '{"url": "https://localhost:8787/webhook/"}' -``` - -## ๐Ÿ—๏ธ Architecture - -### Connector-Based Architecture (v1.2) - -Wireframe v1.2 introduces a revolutionary connector-based architecture that decouples services from platforms: - -``` -src/ -โ”œโ”€โ”€ connectors/ # Platform & Service Connectors -โ”‚ โ”œโ”€โ”€ messaging/ # Messaging platform connectors -โ”‚ โ”‚ โ””โ”€โ”€ telegram/ # Telegram implementation -โ”‚ โ”œโ”€โ”€ ai/ # AI service connector -โ”‚ โ”œโ”€โ”€ session/ # Session management connector -โ”‚ โ””โ”€โ”€ payment/ # Payment service connector -โ”œโ”€โ”€ core/ # Core framework components -โ”‚ โ”œโ”€โ”€ events/ # Event bus for decoupled communication -โ”‚ โ”œโ”€โ”€ plugins/ # Plugin system -โ”‚ โ””โ”€โ”€ interfaces/ # Core interfaces -โ”œโ”€โ”€ services/ # Business logic services -โ”‚ โ”œโ”€โ”€ ai-service.ts # AI processing logic -โ”‚ โ”œโ”€โ”€ session-service.ts # Session management -โ”‚ โ””โ”€โ”€ payment-service.ts # Payment handling -โ”œโ”€โ”€ plugins/ # Extensible plugins -โ”‚ โ”œโ”€โ”€ start-plugin.ts # Start command handler -โ”‚ โ”œโ”€โ”€ ai-plugin.ts # AI commands plugin -โ”‚ โ””โ”€โ”€ settings-plugin.ts # User settings plugin -โ””โ”€โ”€ index.ts # Application entry point - -examples/ -โ”œโ”€โ”€ telegram-bot/ # Basic Telegram bot example -โ”‚ โ”œโ”€โ”€ bot.ts # Complete working bot -โ”‚ โ”œโ”€โ”€ wrangler.toml # Deployment configuration -โ”‚ โ””โ”€โ”€ README.md # Quick start guide -โ””โ”€โ”€ telegram-plugin/ # Plugin system example - โ”œโ”€โ”€ reminder-plugin.ts # Example reminder plugin - โ””โ”€โ”€ bot-with-plugins.ts # Bot with plugin integration -``` +```typescript +import { INotificationAdapter } from '@wireframe/notification-system'; -### Key Design Patterns +class MyPlatformAdapter implements INotificationAdapter { + async formatMessage(template, params, locale) { + // Format message for your platform + } -- **Connector Pattern** - Platform-agnostic service integration -- **Event-Driven Architecture** - All communication through EventBus -- **Plugin System** - Hot-swappable feature modules -- **Service Abstraction** - Business logic separated from connectors -- **Request/Response Events** - Async service communication -- **Batch Processing** - Optimized API calls -- **Repository Pattern** - Clean data access layer -- **TypeScript Strict Mode** - 100% type safety + async deliver(recipientId, message) { + // Send message via your platform + } -## ๐Ÿ“ฆ Examples + async checkReachability(recipientId) { + // Check if user is reachable + } -### Event-Driven Command + isRetryableError(error) { + // Determine if error should trigger retry + } -```typescript -// Using the new event-driven architecture -import { Plugin, PluginContext } from './core/plugins/plugin'; - -export class MyPlugin implements Plugin { - id = 'my-plugin'; - - async install(context: PluginContext) { - // Register command through plugin system - context.commands.set('hello', { - name: 'hello', - description: 'Greet the user', - handler: async (args, ctx) => { - await ctx.reply('๐Ÿ‘‹ Hello from Wireframe v1.2!'); - - // Emit custom event - context.eventBus.emit('greeting:sent', { - userId: ctx.sender.id, - timestamp: Date.now(), - }); - }, - }); + async getUserInfo(recipientId) { + // Get user locale/timezone/preferences } } ``` -### Service Integration Example +### 2. Create Notification Service ```typescript -// Integrate with AI service through events -context.eventBus.emit('ai:complete', { - prompt: 'What is the weather today?', - requestId: 'req_123', - options: { maxTokens: 500 }, +import { + NotificationService, + NotificationConnector, + EventBus, +} from '@wireframe/notification-system'; + +const eventBus = new EventBus(); +const adapter = new MyPlatformAdapter(); + +const connector = new NotificationConnector({ + adapter, + logger: console, + eventBus, + retryConfig: { + maxAttempts: 3, + initialDelay: 1000, + maxDelay: 60000, + backoffMultiplier: 2, + }, }); -// Listen for response -context.eventBus.once('ai:complete:success', (event) => { - console.log('AI Response:', event.payload.response.content); +const notificationService = new NotificationService({ + connector, + logger: console, + eventBus, }); ``` -### Connector Communication +### 3. Send Notifications ```typescript -// Services communicate through standardized events -// Payment example: -context.eventBus.emit('payment:create_invoice', { - playerId: 123, - invoiceType: 'premium_upgrade', - starsAmount: 100, -}); - -// Session management: -context.eventBus.emit('session:get', { - userId: 'user123', - requestId: 'req_456', -}); -``` - -## ๐Ÿš€ Deployment - -### Deploy to Production - -```bash -# Deploy to Cloudflare Workers -npm run deploy - -# Or deploy to staging first -npm run deploy:staging +// Single notification +await notificationService.send( + 'user123', + 'welcome', + { + type: 'user_onboarding', + data: { username: 'John' }, + }, + NotificationCategory.SYSTEM, +); + +// Batch notifications +await notificationService.sendBatch( + ['user1', 'user2', 'user3'], + 'announcement', + { + type: 'product_update', + data: { feature: 'Dark Mode' }, + }, + { batchSize: 50, delayBetweenBatches: 1000 }, +); ``` -### Set Production Webhook +## Platform Adapters -```bash -curl -X POST "https://api.telegram.org/bot/setWebhook" \ - -H "Content-Type: application/json" \ - -d '{ - "url": "https://your-bot.workers.dev/webhook/", - "secret_token": "" - }' -``` +### Telegram Adapter -### Environment Configuration +See [examples/telegram-notifications.ts](examples/telegram-notifications.ts) for a complete example. -Configure secrets for production: +```typescript +import { TelegramNotificationAdapter } from '@wireframe/notification-system/adapters/telegram'; -```bash -wrangler secret put TELEGRAM_BOT_TOKEN -wrangler secret put TELEGRAM_WEBHOOK_SECRET -wrangler secret put SENTRY_DSN - -# AI Provider Secrets (add only what you need) -wrangler secret put GEMINI_API_KEY # Google Gemini -wrangler secret put OPENAI_API_KEY # OpenAI -wrangler secret put XAI_API_KEY # xAI Grok -wrangler secret put DEEPSEEK_API_KEY # DeepSeek -wrangler secret put CLOUDFLARE_AI_ACCOUNT_ID -wrangler secret put CLOUDFLARE_AI_API_TOKEN - -# Owner configuration -wrangler secret put BOT_OWNER_IDS +const adapter = new TelegramNotificationAdapter({ + bot: grammyBot, + defaultLocale: 'en', +}); ``` -## ๐Ÿ“š Best Practices +## Events -### 1. **Content Management** +The notification system emits the following events: -All user-facing text should be managed through the content system: +- `notification:sent` - Successfully sent notification +- `notification:failed` - Failed to send notification +- `notification:blocked` - User blocked or unreachable +- `notification:batch:started` - Batch processing started +- `notification:batch:progress` - Batch processing progress +- `notification:batch:completed` - Batch processing completed ```typescript -const message = contentManager.format('welcome_message', { name: userName }); -``` - -### 2. **Error Handling** - -Comprehensive error handling with proper logging: - -```typescript -try { - await riskyOperation(); -} catch (error) { - logger.error('Operation failed', { error, context }); - await ctx.reply('An error occurred. Please try again.'); -} -``` - -### 3. **Rate Limiting** - -Apply appropriate rate limits to prevent abuse: +eventBus.on('notification:sent', (event) => { + console.log('Sent to:', event.recipientId); +}); -```typescript -app.post('/webhook/:token', rateLimiter({ maxRequests: 20, windowMs: 60000 }), handler); +eventBus.on('notification:failed', (event) => { + console.error('Failed:', event.error); +}); ``` -### 4. **Type Safety** +## User Preferences -Leverage TypeScript's strict mode for maximum safety: +Integrate with your user preference service: ```typescript -// Always define types for your data structures -interface UserData { - id: number; - telegramId: number; - username?: string; // Use optional properties appropriately +class MyUserPreferenceService implements IUserPreferenceService { + async getNotificationPreferences(userId) { + return { + enabled: true, + categories: { + [NotificationCategory.SYSTEM]: true, + [NotificationCategory.MARKETING]: false, + }, + quiet_hours: { + enabled: true, + start: '22:00', + end: '08:00', + timezone: 'UTC', + }, + }; + } } -``` -### 5. **Testing** - -Write tests for critical functionality: - -```typescript -describe('StartCommand', () => { - it('should create new user on first interaction', async () => { - // Test implementation - }); +const notificationService = new NotificationService({ + connector, + userPreferenceService: new MyUserPreferenceService(), + logger, + eventBus, }); ``` -**Important Note for Coverage**: This wireframe uses Istanbul coverage provider instead of V8 due to Cloudflare Workers compatibility. The V8 coverage provider relies on `node:inspector` which is not available in the Workers runtime. Istanbul works by instrumenting code at build time, making it compatible with Workers. - -## ๐Ÿ’ก Perfect Use Cases - -This wireframe is **ideal** for: - -### 1. **๐Ÿ›๏ธ E-commerce Bots** - -- Product catalogs with D1 database -- Payment processing with Telegram Stars -- Order tracking with KV sessions -- Global edge deployment for fast responses - -### 2. **๐ŸŽฎ Gaming & Entertainment Bots** - -- Real-time game state in KV storage -- Leaderboards with D1 queries -- In-game purchases via Telegram Stars -- Low-latency gameplay worldwide - -### 3. **๐Ÿ“Š Analytics & Monitoring Bots** - -- Data aggregation and reporting -- Scheduled tasks for regular updates -- Webhook integrations -- Rich interactive dashboards - -### 4. **๐Ÿค Customer Support Bots** - -- AI-powered responses with Gemini -- Ticket management system -- Multi-language support -- Integration with existing CRM systems - -### 5. **๐Ÿ“š Educational & Content Bots** - -- Course management with structured content -- Progress tracking in database -- Premium content via payments -- Interactive quizzes and assessments - -## โŒ When to Use Different Tools +## Templates -This wireframe might **not** be the best choice for: +Define notification templates with localization: -### 1. **๐Ÿ“น Heavy Media Processing** - -- **Why not**: Cloudflare Workers have CPU time limits (10ms free / 30s paid) -- **Alternative**: Use traditional servers with FFmpeg or cloud functions with longer timeouts - -### 2. **๐Ÿ”„ Long-Running Tasks** - -- **Why not**: Workers timeout after 30 seconds -- **Alternative**: Use AWS Lambda, Google Cloud Functions, or traditional servers - -### 3. **๐Ÿ“ฆ Large File Storage** - -- **Why not**: Workers have memory limits and no persistent file system -- **Alternative**: Combine with R2/S3 for file storage or use traditional hosting - -### 4. **๐Ÿ”Œ WebSocket Connections** - -- **Why not**: Workers don't support persistent WebSocket connections for bots -- **Alternative**: Use Node.js with libraries like node-telegram-bot-api - -### 5. **๐Ÿ›๏ธ Legacy System Integration** - -- **Why not**: May require specific libraries or protocols not available in Workers -- **Alternative**: Traditional servers with full OS access or containerized solutions - -## ๐Ÿ› ๏ธ Development - -### Available Scripts - -```bash -npm run dev # Start local development -npm run test # Run tests -npm run test:coverage # Run tests with coverage -npm run lint # Lint code -npm run typecheck # Type check -npm run format # Format code -npm run deploy # Deploy to production -npm run tail # View production logs +```typescript +const template: NotificationTemplate = { + id: 'welcome', + name: 'Welcome Message', + category: NotificationCategory.SYSTEM, + content: { + en: { + subject: 'Welcome!', + body: 'Welcome {{username}}! Thanks for joining.', + actions: [ + { + type: 'button', + label: 'Get Started', + url: '/tutorial', + }, + ], + }, + es: { + subject: 'ยกBienvenido!', + body: 'ยกBienvenido {{username}}! Gracias por unirte.', + actions: [ + { + type: 'button', + label: 'Comenzar', + url: '/tutorial', + }, + ], + }, + }, + variables: [ + { + name: 'username', + type: 'string', + required: true, + }, + ], +}; ``` -### CI/CD with GitHub Actions - -The repository includes GitHub Actions workflows: - -- **Test Workflow** - Automatically runs on every push and PR -- **Deploy Workflow** - Optional, requires Cloudflare setup (disabled by default) - -To enable automatic deployment: - -1. Set up GitHub secrets (see [Setup Guide](SETUP.md)) -2. Edit `.github/workflows/deploy.yml` to enable push trigger -3. Ensure all Cloudflare resources are created - -### Project Structure - -- **Commands** - Add new commands in `src/adapters/telegram/commands/` -- **Callbacks** - Handle button clicks in `src/adapters/telegram/callbacks/` -- **Services** - Business logic in `src/services/` -- **AI Providers** - LLM adapters in `src/lib/ai/adapters/` -- **Database** - Migrations in `migrations/` -- **Tests** - Test files in `src/__tests__/` - -### AI Provider System - -The wireframe includes a sophisticated multi-provider AI system: - -- **๐ŸŽจ Provider Abstraction** - Switch between AI providers without code changes -- **๐Ÿ’ฐ Cost Tracking** - Monitor usage and costs across all providers -- **๐Ÿ”„ Automatic Fallback** - Failover to backup providers on errors -- **๐Ÿ”” Smart Selection** - Automatically choose the best available provider -- **๐Ÿงช Mock Provider** - Test your bot without API costs - -Supported providers: - -- Google Gemini (default) -- OpenAI (GPT-4o, GPT-3.5) -- xAI Grok -- DeepSeek -- Cloudflare Workers AI - -### Access Control System - -The wireframe includes a comprehensive role-based access control system: - -- **๐Ÿ” Three-tier roles** - Owner, Admin, and User levels -- **๐Ÿ“ Access requests** - Users can request access through the bot -- **โœ… Request management** - Admins can approve/reject requests -- **๐Ÿ› Debug mode** - Owners can enable tiered debug visibility -- **๐ŸŒ Internationalization** - Full i18n support for all messages - -#### Role Hierarchy - -1. **Owner** (configured via `BOT_OWNER_IDS`) - - Full bot control and configuration - - Can manage administrators - - Access to technical information and debug mode - - Example commands: `/info`, `/admin`, `/debug` - -2. **Admin** (granted by owners) - - Can review and process access requests - - See debug messages when enabled (level 2+) - - Example command: `/requests` - -3. **User** (default role) - - Basic bot functionality - - Must request access if bot is restricted - - Example commands: `/start`, `/help`, `/ask` - -#### Configuration +## Development ```bash -# Required in .dev.vars or secrets -BOT_OWNER_IDS=123456789,987654321 # Comma-separated Telegram user IDs -``` - -#### Access Request Flow - -1. New user starts bot with `/start` -2. If access is restricted, user sees "Request Access" button -3. Admin receives notification and reviews with `/requests` -4. User gets notified when request is approved/rejected -5. Approved users gain full bot functionality +# Install dependencies +npm install -#### Debug Mode +# Run tests +npm test -Owners can control debug visibility: +# Type check +npm run typecheck -- **Level 1**: Only owners see debug messages -- **Level 2**: Owners and admins see debug messages -- **Level 3**: Everyone sees debug messages +# Lint +npm run lint -```bash -/debug on 2 # Enable debug for owners and admins -/debug off # Disable debug mode -/debug status # Check current debug status +# Build +npm run build ``` -## ๐Ÿ”’ Security - -### Security Best Practices - -This wireframe implements multiple layers of security: - -1. **Webhook Validation** - - URL token validation - - X-Telegram-Bot-Api-Secret-Token header validation (required) - - Request payload validation with Zod - -2. **Rate Limiting** - - Built-in rate limiting for all endpoints - - Distributed rate limiting using KV storage - -3. **Security Headers** - - X-Content-Type-Options: nosniff - - X-Frame-Options: DENY - - X-XSS-Protection: 1; mode=block - - Strict Referrer Policy - - Restrictive Permissions Policy - -4. **Data Validation** - - All input validated with Zod schemas - - SQL injection prevention with parameterized queries - - Type-safe data handling throughout - -5. **Logging Security** - - Sensitive headers automatically redacted - - No PII in logs by default - - Structured logging with request IDs - -### Responsible Disclosure - -Found a security vulnerability? Please report it responsibly: - -1. **DO NOT** create a public GitHub issue -2. Email security details to: `security@your-domain.com` -3. Include: description, steps to reproduce, potential impact -4. Allow reasonable time for a fix before public disclosure - -We appreciate your help in keeping this project secure! - -## ๐ŸŽฏ Framework Features - -### Plugin System - -Extend your bot with modular plugins: - -- **๐Ÿ”Œ Hot-swappable** - Install/uninstall plugins at runtime -- **๐Ÿ“ฆ Self-contained** - Each plugin manages its own state -- **๐Ÿ”” Event-driven** - Plugins communicate via event bus -- **๐Ÿ’พ Persistent storage** - KV-backed storage per plugin -- **โšก Lifecycle hooks** - Control plugin behavior - -### Event Bus - -Decoupled communication between components: - -- **๐Ÿ“ก Global events** - System-wide notifications -- **๐ŸŽฏ Scoped events** - Plugin-specific namespaces -- **โฑ๏ธ Event history** - Track and replay events -- **๐Ÿ” Filtering** - Subscribe to specific event types -- **โšก Async/sync** - Choose your handling strategy - -### Multi-Platform Support - -The framework is designed for multiple platforms: - -- **Telegram** - Full implementation included -- **Discord** - Connector interface ready -- **Slack** - Connector interface ready -- **WhatsApp** - Connector interface ready -- **Custom** - Easy to add new platforms - -## ๐Ÿค Contributing +## Contributing 1. Fork the repository -2. Create your feature branch (`git checkout -b feature/amazing-feature`) -3. Commit your changes (`git commit -m 'Add some amazing feature'`) -4. Push to the branch (`git push origin feature/amazing-feature`) -5. Open a Pull Request - -### Code Standards - -- **No `any` types** - Maintain 100% type safety -- **Test coverage** - Write tests for new features -- **Documentation** - Update docs for API changes -- **Security first** - Consider security implications - -## ๐Ÿ”ง Recommended MCP Servers - -### Accelerate Development with Model Context Protocol - -[MCP (Model Context Protocol)](https://modelcontextprotocol.io/) servers enable AI assistants like Claude to interact with your development tools. Here are the recommended MCP servers for this project: - -#### Essential MCP Servers - -1. **Cloudflare MCP Servers** - [Official Documentation](https://github.com/cloudflare/mcp-server-cloudflare) - - **Remote servers available at:** - - Observability: `https://observability.mcp.cloudflare.com/sse` - - Workers Bindings: `https://bindings.mcp.cloudflare.com/sse` - - Manage Workers, KV, D1, R2 resources - - Deploy and configure services - - Monitor logs and analytics - - Handle secrets and environment variables - -2. **Git MCP Server (GitMCP)** - [GitMCP.io](https://gitmcp.io) - - **Remote server for this project:** `https://gitmcp.io/talkstream/typescript-wireframe-platform` - - Access any GitHub repository content instantly - - No installation required - just use the URL format - - Read-only access to public repositories - - Perfect for exploring codebases and documentation - -3. **Sentry MCP Server** - [Official Repository](https://github.com/getsentry/sentry-mcp) - - **Remote server available at:** `https://mcp.sentry.dev` - - Official server maintained by Sentry - - Retrieve and analyze error reports - - Performance monitoring with 16 different tool calls - - OAuth support for secure authentication - - Built on Cloudflare's remote MCP infrastructure - -#### How These Servers Help This Project - -- **Cloudflare Server**: Essential for managing all Cloudflare resources (Workers, KV, D1) used by this bot -- **Git Server**: Access and explore repository content directly without leaving your development environment -- **Sentry Server**: Quickly diagnose production issues reported by your bot users with official Sentry integration - -These MCP servers significantly accelerate development by enabling natural language interactions with your tools, reducing context switching, and automating repetitive tasks. - -## โšก Performance & Cloudflare Plans - -### Understanding Cloudflare Workers Limits - -This wireframe works on both Free and Paid Cloudflare plans. Here's how different plans affect your bot's capabilities: - -#### Free Plan Limits - -- **CPU Time**: 10ms per request -- **Daily Requests**: 100,000 -- **KV Operations**: 100,000 reads, 1,000 writes per day -- **D1 Database**: 5M reads, 100k writes per day - -**Works well for:** - -- Simple command bots -- Quick responses without heavy processing -- Bots with up to ~3,000 active daily users -- Basic database operations - -#### Paid Plan ($5/month) Benefits - -- **CPU Time**: 30 seconds per request (3000x more!) -- **Daily Requests**: 10 million included -- **Queues**: Available for async processing -- **Workers Logs**: 10M events/month with filtering -- **Trace Events**: Advanced debugging capabilities - -**Enables advanced features:** - -- Complex AI text generation -- Image/file processing -- Bulk operations and broadcasts -- Heavy computational tasks -- Async job processing with Queues -- Advanced analytics and debugging - -### Choosing the Right Plan - -Most bots work perfectly on the **Free plan**. Consider the **Paid plan** when: - -- Your bot uses AI for lengthy text generation -- You need to process files or images -- You're broadcasting to thousands of users -- Your commands involve complex calculations -- You need detailed logs and debugging tools - -The wireframe automatically adapts to available resources and will work reliably on both plans. - -## ๐Ÿ“š Documentation - -Comprehensive documentation is available in the `docs/` directory: - -- **[Project Overview](docs/PROJECT_OVERVIEW.md)** - Architecture, technology stack, and quick start guide -- **[Development Guide](docs/DEVELOPMENT_GUIDE.md)** - Local setup, testing, and debugging -- **[Architecture Decisions](docs/ARCHITECTURE_DECISIONS.md)** - Key design choices and rationale -- **[API Reference](docs/API_REFERENCE.md)** - Telegram API types and webhook handling -- **[Deployment Guide](docs/DEPLOYMENT.md)** - Production deployment and configuration -- **[Troubleshooting](docs/TROUBLESHOOTING.md)** - Common issues and solutions - -### Code Patterns - -Reusable patterns for common tasks: - -- **[Webhook Validation](docs/patterns/webhook-validation.js)** - Secure webhook handling -- **[Error Handling](docs/patterns/error-handling.js)** - Robust error management -- **[Command Router](docs/patterns/command-router.js)** - Flexible command routing -- **[Access Control](docs/patterns/access-control.js)** - Role-based permissions - -## ๐Ÿš€ Roadmap - -### Phase 1: Core Enhancements (Days or Hours) - -- [ ] Plugin system for modular features -- [ ] Database migrations toolkit -- [ ] Advanced caching strategies -- [ ] WebSocket support for real-time features - -### Phase 2: Developer Tools (Days or Hours) - -- [ ] CLI tool for scaffolding commands -- [ ] Visual bot flow designer -- [ ] Automated performance profiler -- [ ] Integration test framework - -### Phase 3: Ecosystem (Days or Hours) - -- [ ] Plugin marketplace -- [ ] Starter templates gallery -- [ ] Community middleware -- [ ] Video tutorials series - -### Phase 4: Enterprise Features (Days or Hours) - -- [ ] Multi-tenant architecture -- [ ] Advanced analytics dashboard -- [ ] A/B testing framework -- [ ] Compliance tools (GDPR, etc.) - -## ๐Ÿ“„ License - -This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. - -## ๐Ÿ™ Acknowledgments - -- [Cloudflare Workers](https://workers.cloudflare.com/) for the amazing edge platform -- [grammY](https://grammy.dev/) for the excellent Telegram bot framework -- [Telegram Bot API](https://core.telegram.org/bots/api) for the powerful bot platform - ---- +2. Create your feature branch +3. Write tests for your changes +4. Ensure all tests pass and no TypeScript errors +5. Submit a pull request -

- Made with โค๏ธ for the Telegram bot developer community -

+## License -

- Contact Author โ€ข - Get Support -

+MIT diff --git a/docs/NOTIFICATION_SYSTEM.md b/docs/NOTIFICATION_SYSTEM.md new file mode 100644 index 0000000..35d76d1 --- /dev/null +++ b/docs/NOTIFICATION_SYSTEM.md @@ -0,0 +1,251 @@ +# Universal Notification System + +A robust, platform-agnostic notification system with retry logic, batch processing, and user preferences support. + +## Features + +- ๐Ÿ”„ **Automatic Retry Logic**: Exponential backoff with jitter for failed notifications +- ๐Ÿ“ฆ **Batch Processing**: Efficient handling of bulk notifications +- โš™๏ธ **User Preferences**: Granular control over notification categories +- ๐Ÿ›ก๏ธ **Error Handling**: Graceful handling of blocked users and network errors +- ๐Ÿ“Š **Event-driven**: Integration with EventBus for monitoring +- ๐ŸŒ **Platform Agnostic**: Easy to adapt for different messaging platforms + +## Architecture + +``` +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ NotificationService โ”‚โ”€โ”€โ”€โ”€โ–ถโ”‚ NotificationConnectorโ”‚โ”€โ”€โ”€โ”€โ–ถโ”‚ Platform Adapter โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ โ”‚ โ”‚ + โ”‚ โ–ผ โ–ผ + โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถโ”‚ EventBus โ”‚ โ”‚ Telegram/Discord โ”‚ + โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ +``` + +## Quick Start + +### 1. Install Dependencies + +```bash +npm install grammy # For Telegram adapter +``` + +### 2. Basic Setup + +```typescript +import { NotificationService } from '@/core/services/notification-service'; +import { NotificationConnector } from '@/connectors/notification-connector'; +import { TelegramNotificationAdapter } from '@/adapters/telegram/notification-adapter'; +import { Bot } from 'grammy'; + +// Create Telegram bot +const bot = new Bot(process.env.BOT_TOKEN); + +// Create adapter +const adapter = new TelegramNotificationAdapter({ bot }); + +// Create connector with retry logic +const connector = new NotificationConnector({ + adapter, + storage: env.KV, // Optional: for storing notification status + logger, + eventBus, + retryConfig: { + maxAttempts: 3, + initialDelay: 1000, + maxDelay: 60000, + backoffMultiplier: 2, + }, +}); + +// Create service +const notificationService = new NotificationService({ + connector, + userPreferenceService, // Optional: for user preferences + logger, + eventBus, +}); +``` + +### 3. Send Notifications + +```typescript +// Simple notification +await notificationService.send( + '123456789', // recipientId + 'welcome', // template + { + type: 'user_welcome', + data: { + username: 'John', + service: 'Premium', + }, + }, + 'system', // category +); + +// Batch notification +await notificationService.sendBatch( + ['123456789', '987654321'], // recipientIds + 'announcement', + { + type: 'system_announcement', + data: { + title: 'New Feature', + message: 'Check out our new feature!', + }, + }, + { + batchSize: 50, + delayBetweenBatches: 1000, + }, +); +``` + +## Components + +### NotificationService + +High-level service for sending notifications with business logic: +- User preference checking +- Template selection +- Event emission + +### NotificationConnector + +Low-level connector handling: +- Retry logic with exponential backoff +- Batch processing +- Status tracking +- Error handling + +### Platform Adapters + +Implement `INotificationAdapter` for your platform: + +```typescript +export interface INotificationAdapter { + deliver(recipientId: string, message: FormattedMessage): Promise; + checkReachability(recipientId: string): Promise; + getUserInfo(recipientId: string): Promise; + formatMessage( + template: NotificationTemplate, + params: Record, + locale: string, + ): Promise; + isRetryableError(error: unknown): boolean; +} +``` + +## User Preferences + +Implement `IUserPreferenceService` to support user preferences: + +```typescript +class UserPreferenceService implements IUserPreferenceService { + async getNotificationPreferences(userId: string): Promise { + // Fetch from database + return { + enabled: true, + categories: { + system: true, + transaction: true, + marketing: false, + // ... + }, + }; + } +} +``` + +## Events + +The system emits events for monitoring: + +```typescript +eventBus.on('notification:sent', (data) => { + console.log('Notification sent:', data); +}); + +eventBus.on('notification:failed', (data) => { + console.log('Notification failed:', data); +}); + +eventBus.on('notification:batch:completed', (data) => { + console.log('Batch completed:', data); +}); +``` + +## Templates + +Create notification templates: + +```typescript +const templates: Record = { + welcome: { + id: 'welcome', + name: 'User Welcome', + category: 'system', + content: { + en: { + body: 'Welcome {{username}}! Your {{service}} is now active.', + parseMode: 'HTML', + buttons: [[ + { text: 'Get Started', url: 'https://example.com/start' }, + ]], + }, + es: { + body: 'ยกBienvenido {{username}}! Tu {{service}} estรก activo.', + parseMode: 'HTML', + }, + }, + }, +}; +``` + +## Error Handling + +The system handles various error scenarios: + +1. **User Blocked**: Marked as `BLOCKED` status, no retry +2. **Network Errors**: Automatic retry with backoff +3. **Rate Limits**: Respects platform rate limits +4. **Invalid Recipients**: Logged and skipped + +## Testing + +```typescript +import { createMockAdapter } from '@/test-utils'; + +const mockAdapter = createMockAdapter({ + deliver: vi.fn().mockResolvedValue(undefined), + checkReachability: vi.fn().mockResolvedValue(true), +}); + +// Test retry logic +mockAdapter.deliver.mockRejectedValueOnce(new Error('Network error')); +mockAdapter.isRetryableError.mockReturnValue(true); + +// Should retry and succeed +await connector.send(message); +expect(mockAdapter.deliver).toHaveBeenCalledTimes(2); +``` + +## Production Considerations + +1. **Rate Limiting**: Implement rate limiting in adapters +2. **Monitoring**: Use EventBus events for metrics +3. **Storage**: Use KV/Database for notification history +4. **Scaling**: Batch processing for large recipient lists +5. **Localization**: Support multiple languages in templates + +## Contributing + +When adding new platform adapters: +1. Implement `INotificationAdapter` interface +2. Handle platform-specific errors +3. Support platform features (buttons, media, etc.) +4. Add comprehensive tests +5. Document platform-specific considerations \ No newline at end of file diff --git a/examples/notification-system/telegram-bot-integration.ts b/examples/notification-system/telegram-bot-integration.ts new file mode 100644 index 0000000..7471a64 --- /dev/null +++ b/examples/notification-system/telegram-bot-integration.ts @@ -0,0 +1,218 @@ +/** + * Example integration of the notification system + * Shows how to integrate with your Telegram bot + */ + +import { Bot } from 'grammy'; +import { NotificationService } from '../../src/core/services/notification-service'; +import { NotificationConnector } from '../../src/connectors/notification-connector'; +import { TelegramNotificationAdapter } from '../../src/adapters/telegram/notification-adapter'; + +import type { Env } from '../../src/core/interfaces/cloud'; +import type { ILogger } from '../../src/core/interfaces/logger'; +import type { IEventBus } from '../../src/core/interfaces/event-bus'; +import type { NotificationTemplate } from '../../src/core/interfaces/notification'; + +// Example templates +const notificationTemplates: Record = { + 'user.welcome': { + id: 'user.welcome', + name: 'User Welcome', + category: 'system', + content: { + en: { + body: '๐Ÿ‘‹ Welcome to {{botName}}!\n\nThank you for joining us. Here\'s what you can do:\n\n{{features}}\n\nNeed help? Use /help command.', + parseMode: 'HTML', + buttons: [[ + { text: '๐Ÿ“š View Commands', callbackData: 'show_commands' }, + { text: 'โš™๏ธ Settings', callbackData: 'show_settings' }, + ]], + }, + }, + }, + 'transaction.success': { + id: 'transaction.success', + name: 'Transaction Success', + category: 'transaction', + content: { + en: { + body: 'โœ… Transaction Successful\n\nAmount: {{amount}} {{currency}}\nBalance: {{balance}} {{currency}}\n\nTransaction ID: {{transactionId}}', + parseMode: 'HTML', + }, + }, + }, + 'service.expiring': { + id: 'service.expiring', + name: 'Service Expiring', + category: 'service', + content: { + en: { + body: 'โฐ Service Expiring Soon\n\nYour {{serviceName}} subscription expires in {{daysLeft}} days.\n\nRenew now to avoid service interruption.', + parseMode: 'HTML', + buttons: [[ + { text: '๐Ÿ”„ Renew Now', callbackData: 'renew_{{serviceId}}' }, + ]], + }, + }, + }, +}; + +/** + * Setup notification system + */ +export function setupNotificationSystem( + bot: Bot, + env: Env, + logger: ILogger, + eventBus: IEventBus, +): NotificationService { + // Create Telegram adapter + const adapter = new TelegramNotificationAdapter({ + bot, + defaultLocale: 'en', + }); + + // Create connector with retry logic + const connector = new NotificationConnector({ + adapter, + storage: env.KV, // Optional: for tracking notification status + logger, + eventBus, + retryConfig: { + maxAttempts: 3, + initialDelay: 1000, + maxDelay: 60000, + backoffMultiplier: 2, + }, + }); + + // Create notification service + const notificationService = new NotificationService({ + connector, + logger, + eventBus, + defaultLocale: 'en', + }); + + // Setup event listeners + setupEventListeners(eventBus, logger); + + return notificationService; +} + +/** + * Setup event listeners for monitoring + */ +function setupEventListeners(eventBus: IEventBus, logger: ILogger): void { + eventBus.on('notification:sent', (data) => { + logger.info('Notification sent', data); + }); + + eventBus.on('notification:failed', (data) => { + logger.error('Notification failed', data); + }); + + eventBus.on('notification:blocked', (data) => { + logger.warn('User blocked notifications', data); + }); + + eventBus.on('notification:batch:completed', (data) => { + logger.info('Batch notification completed', { + batchId: data.batchId, + sent: data.sent, + failed: data.failed, + duration: data.duration, + }); + }); +} + +/** + * Example usage in your bot commands + */ +export async function handleUserJoin( + userId: string, + notificationService: NotificationService, +): Promise { + // Send welcome notification + await notificationService.send( + userId, + 'user.welcome', + { + type: 'user_join', + data: { + botName: 'My Awesome Bot', + features: 'โ€ข Create tasks\nโ€ข Set reminders\nโ€ข Track progress', + }, + }, + 'system', + ); +} + +export async function handleTransaction( + userId: string, + amount: number, + balance: number, + transactionId: string, + notificationService: NotificationService, +): Promise { + // Send transaction notification + await notificationService.send( + userId, + 'transaction.success', + { + type: 'transaction', + data: { + amount, + balance, + currency: 'USD', + transactionId, + }, + }, + 'transaction', + ); +} + +export async function handleServiceExpiring( + userIds: string[], + serviceName: string, + serviceId: string, + daysLeft: number, + notificationService: NotificationService, +): Promise { + // Send batch notification to all affected users + await notificationService.sendBatch( + userIds, + 'service.expiring', + { + type: 'service_expiring', + data: { + serviceName, + serviceId, + daysLeft, + }, + }, + { + batchSize: 50, + delayBetweenBatches: 1000, + }, + ); +} + +/** + * Example: System-wide announcement + */ +export async function sendAnnouncement( + message: string, + notificationService: NotificationService, + userService: { getAllActiveUsers(): Promise }, +): Promise { + // Get all active users + const userIds = await userService.getAllActiveUsers(); + + // Send announcement to everyone + await notificationService.sendBulk( + userIds, + `๐Ÿ“ข Announcement\n\n${message}`, + 'system', + ); +} \ No newline at end of file diff --git a/examples/telegram-notifications.ts b/examples/telegram-notifications.ts new file mode 100644 index 0000000..ad1cfc0 --- /dev/null +++ b/examples/telegram-notifications.ts @@ -0,0 +1,171 @@ +/** + * Example: Telegram Notification System + * Shows how to integrate the notification system with Telegram + */ + +import { Bot } from 'grammy'; +import { NotificationService } from '../src/core/services/notification-service'; +import { NotificationConnector } from '../src/connectors/notification-connector'; +import { TelegramNotificationAdapter } from '../src/adapters/telegram/notification-adapter'; +import { EventBus } from '../src/core/events/event-bus'; +import { NotificationCategory } from '../src/core/interfaces/notification'; + +// Example logger implementation +class ConsoleLogger { + debug(message: string, meta?: unknown) { + console.debug(`[DEBUG] ${message}`, meta); + } + + info(message: string, meta?: unknown) { + console.info(`[INFO] ${message}`, meta); + } + + warn(message: string, meta?: unknown) { + console.warn(`[WARN] ${message}`, meta); + } + + error(message: string, meta?: unknown) { + console.error(`[ERROR] ${message}`, meta); + } +} + +// Example KV store implementation +class MemoryKVStore { + private store = new Map(); + + async get(key: string): Promise { + const value = this.store.get(key); + return value ? (JSON.parse(value) as T) : null; + } + + async put(key: string, value: string): Promise { + this.store.set(key, value); + } + + async delete(key: string): Promise { + this.store.delete(key); + } +} + +// Initialize services +const bot = new Bot(process.env.BOT_TOKEN || ''); +const logger = new ConsoleLogger(); +const eventBus = new EventBus(); +const kvStore = new MemoryKVStore(); + +// Create notification adapter +const adapter = new TelegramNotificationAdapter({ + bot, + defaultLocale: 'en', +}); + +// Create notification connector +const connector = new NotificationConnector({ + adapter, + storage: kvStore, + logger, + eventBus, + retryConfig: { + maxAttempts: 3, + initialDelay: 1000, + maxDelay: 60000, + backoffMultiplier: 2, + }, +}); + +// Create notification service +const notificationService = new NotificationService({ + connector, + logger, + eventBus, + defaultLocale: 'en', +}); + +// Example usage +async function sendWelcomeNotification(userId: string) { + await notificationService.send( + userId, + 'welcome', + { + type: 'user_onboarding', + data: { + username: 'John Doe', + registrationDate: new Date().toISOString(), + }, + }, + NotificationCategory.SYSTEM, + ); +} + +// Example batch notifications +async function sendMaintenanceNotifications(userIds: string[]) { + await notificationService.sendBatch( + userIds, + 'maintenance', + { + type: 'system_maintenance', + data: { + startTime: '2025-01-27 10:00 UTC', + duration: '2 hours', + affectedServices: ['API', 'Dashboard'], + }, + }, + { + batchSize: 50, + delayBetweenBatches: 1000, + }, + ); +} + +// Listen to notification events +eventBus.on('notification:sent', (event) => { + console.log('Notification sent:', event); +}); + +eventBus.on('notification:failed', (event) => { + console.error('Notification failed:', event); +}); + +eventBus.on('notification:batch:completed', (event) => { + console.log('Batch completed:', event); +}); + +// Example with custom templates +const templates = { + welcome: { + en: { + body: `Welcome {{username}}! + +Thank you for joining our platform. +Registration date: {{registrationDate}}`, + parseMode: 'HTML' as const, + buttons: [ + [ + { + text: 'Get Started', + callbackData: 'start_tutorial', + }, + { + text: 'View Help', + url: 'https://example.com/help', + }, + ], + ], + }, + }, + maintenance: { + en: { + body: `System Maintenance Notice + +We will be performing scheduled maintenance: +โ€ข Start: {{startTime}} +โ€ข Duration: {{duration}} +โ€ข Affected: {{affectedServices}} + +We apologize for any inconvenience.`, + parseMode: 'HTML' as const, + }, + }, +}; + +export { notificationService, sendWelcomeNotification, sendMaintenanceNotifications, templates }; diff --git a/migrations/0007_notification_preferences.sql b/migrations/0007_notification_preferences.sql new file mode 100644 index 0000000..af8c150 --- /dev/null +++ b/migrations/0007_notification_preferences.sql @@ -0,0 +1,47 @@ +-- Notification preferences table +-- Stores user notification preferences + +CREATE TABLE IF NOT EXISTS notification_preferences ( + user_id TEXT PRIMARY KEY, + enabled BOOLEAN DEFAULT TRUE, + + -- Category preferences (JSON object) + -- Example: {"system": true, "transaction": true, "marketing": false} + categories TEXT DEFAULT '{"system": true, "transaction": true, "balance": true, "service": true}', + + -- Quiet hours settings (JSON object) + -- Example: {"enabled": true, "start": "22:00", "end": "08:00", "timezone": "UTC"} + quiet_hours TEXT DEFAULT NULL, + + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Index for faster lookups +CREATE INDEX IF NOT EXISTS idx_notification_preferences_updated +ON notification_preferences(updated_at); + +-- Notification history table (optional) +-- Tracks sent notifications for audit/debugging +CREATE TABLE IF NOT EXISTS notification_history ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + message_id TEXT NOT NULL UNIQUE, + recipient_id TEXT NOT NULL, + template TEXT NOT NULL, + category TEXT NOT NULL, + status TEXT NOT NULL, -- 'sent', 'failed', 'blocked', 'retry' + error TEXT, + retry_count INTEGER DEFAULT 0, + sent_at TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP +); + +-- Indexes for notification history +CREATE INDEX IF NOT EXISTS idx_notification_history_recipient +ON notification_history(recipient_id, created_at DESC); + +CREATE INDEX IF NOT EXISTS idx_notification_history_status +ON notification_history(status, created_at DESC); + +CREATE INDEX IF NOT EXISTS idx_notification_history_message +ON notification_history(message_id); \ No newline at end of file diff --git a/src/adapters/telegram/notification-adapter.ts b/src/adapters/telegram/notification-adapter.ts new file mode 100644 index 0000000..7a4b34e --- /dev/null +++ b/src/adapters/telegram/notification-adapter.ts @@ -0,0 +1,232 @@ +/** + * Telegram notification adapter + * Implements notification delivery via Telegram Bot API + */ + +import { Bot } from 'grammy'; +import type { InlineKeyboardButton } from 'grammy/types'; + +import type { + INotificationAdapter, + NotificationTemplate, +} from '../../core/interfaces/notification'; + +// Telegram-specific types +interface TelegramError extends Error { + error_code?: number; + description?: string; +} + +interface TelegramButton { + text: string; + callbackData?: string; + url?: string; +} + +interface TelegramFormattedMessage { + text: string; + parseMode?: 'HTML' | 'Markdown' | 'MarkdownV2'; + inlineKeyboard?: TelegramButton[][]; +} + +interface TelegramTemplateContent { + body: string; + parseMode?: 'HTML' | 'Markdown' | 'MarkdownV2'; + buttons?: TelegramButton[][]; +} + +export interface TelegramNotificationAdapterDeps { + bot: Bot; + defaultLocale?: string; +} + +export class TelegramNotificationAdapter implements INotificationAdapter { + private bot: Bot; + private defaultLocale: string; + + constructor(deps: TelegramNotificationAdapterDeps) { + this.bot = deps.bot; + this.defaultLocale = deps.defaultLocale || 'en'; + } + + async deliver(recipientId: string, message: unknown): Promise { + const formattedMessage = message as TelegramFormattedMessage; + const telegramId = parseInt(recipientId); + if (isNaN(telegramId)) { + throw new Error(`Invalid Telegram ID: ${recipientId}`); + } + + try { + const options: Parameters[2] = { + parse_mode: formattedMessage.parseMode || 'HTML', + }; + + // Add inline keyboard if provided + if (formattedMessage.inlineKeyboard) { + options.reply_markup = { + inline_keyboard: this.convertToTelegramKeyboard(formattedMessage.inlineKeyboard), + }; + } + + await this.bot.api.sendMessage(telegramId, formattedMessage.text, options); + } catch (error) { + // Check for specific Telegram errors + const telegramError = error as TelegramError; + if (telegramError.error_code === 403) { + throw new Error('USER_BLOCKED'); + } + throw error; + } + } + + async checkReachability(recipientId: string): Promise { + const telegramId = parseInt(recipientId); + if (isNaN(telegramId)) { + return false; + } + + try { + // Try to get chat info + await this.bot.api.getChat(telegramId); + return true; + } catch (error) { + const telegramError = error as TelegramError; + // 400 Bad Request: chat not found + // 403 Forbidden: bot was blocked by user + if (telegramError.error_code === 400 || telegramError.error_code === 403) { + return false; + } + // For other errors, assume user might be reachable + return true; + } + } + + async getUserInfo(recipientId: string): Promise<{ + locale?: string; + timezone?: string; + blocked?: boolean; + }> { + const telegramId = parseInt(recipientId); + if (isNaN(telegramId)) { + throw new Error(`Invalid Telegram ID: ${recipientId}`); + } + + try { + const chat = await this.bot.api.getChat(telegramId); + + // Type guard for private chat + if (chat.type === 'private') { + const privateChat = chat as { + type: 'private'; + language_code?: string; + first_name?: string; + last_name?: string; + username?: string; + }; + return { + locale: privateChat.language_code || this.defaultLocale, + }; + } + + return { + locale: this.defaultLocale, + }; + } catch (_error) { + // Return minimal info if we can't get chat details + return { + locale: this.defaultLocale, + }; + } + } + + async formatMessage( + template: NotificationTemplate, + params: Record, + locale: string, + ): Promise { + // Get localized content + const content = template.content[locale] || template.content[this.defaultLocale]; + if (!content) { + throw new Error(`No content found for template ${template.id} in locale ${locale}`); + } + + // Simple template replacement + let text = content.body; + for (const [key, value] of Object.entries(params)) { + text = text.replace(new RegExp(`{{${key}}}`, 'g'), String(value)); + } + + // Type guard for telegram content + const telegramContent = content as TelegramTemplateContent; + + const formatted: TelegramFormattedMessage = { + text, + parseMode: telegramContent.parseMode || 'HTML', + }; + + // Add buttons if provided + if (telegramContent.buttons) { + formatted.inlineKeyboard = telegramContent.buttons.map((row) => + row.map((button) => ({ + text: this.replaceParams(button.text, params), + callbackData: button.callbackData + ? this.replaceParams(button.callbackData, params) + : undefined, + url: button.url ? this.replaceParams(button.url, params) : undefined, + })), + ); + } + + return formatted; + } + + isRetryableError(error: unknown): boolean { + const telegramError = error as TelegramError; + if (!telegramError.error_code) { + return true; // Retry on unknown errors + } + + const errorCode = telegramError.error_code; + + // Don't retry on these errors + const nonRetryableErrors = [ + 400, // Bad Request + 403, // Forbidden (user blocked bot) + 404, // Not Found + ]; + + return !nonRetryableErrors.includes(errorCode); + } + + private convertToTelegramKeyboard(keyboard: TelegramButton[][]): InlineKeyboardButton[][] { + return keyboard.map((row) => + row.map((button) => { + if (button.url) { + return { + text: button.text, + url: button.url, + }; + } else if (button.callbackData) { + return { + text: button.text, + callback_data: button.callbackData, + }; + } else { + // Default to callback button with text as data + return { + text: button.text, + callback_data: button.text, + }; + } + }), + ); + } + + private replaceParams(text: string, params: Record): string { + let result = text; + for (const [key, value] of Object.entries(params)) { + result = result.replace(new RegExp(`{{${key}}}`, 'g'), String(value)); + } + return result; + } +} diff --git a/src/connectors/__tests__/notification-connector.test.ts b/src/connectors/__tests__/notification-connector.test.ts new file mode 100644 index 0000000..7fe3fec --- /dev/null +++ b/src/connectors/__tests__/notification-connector.test.ts @@ -0,0 +1,265 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; + +import { NotificationConnector } from '../notification-connector'; +import type { INotificationAdapter } from '../../core/interfaces/notification'; +import { NotificationStatus } from '../../core/interfaces/notification'; +import type { ILogger } from '../../core/interfaces/logger'; +import type { IEventBus } from '../../core/interfaces/event-bus'; +import type { IKeyValueStore } from '../../core/interfaces/storage'; + +describe('NotificationConnector', () => { + let connector: NotificationConnector; + let mockAdapter: INotificationAdapter; + let mockLogger: ILogger; + let mockEventBus: IEventBus; + let mockStorage: IKeyValueStore; + + beforeEach(() => { + vi.clearAllMocks(); + + mockAdapter = { + formatMessage: vi.fn().mockResolvedValue({ text: 'formatted message' }), + deliver: vi.fn().mockResolvedValue(undefined), + checkReachability: vi.fn().mockResolvedValue(true), + isRetryableError: vi.fn().mockReturnValue(true), + getUserInfo: vi.fn().mockResolvedValue({ locale: 'en' }), + }; + + mockLogger = { + debug: vi.fn(), + info: vi.fn(), + warn: vi.fn(), + error: vi.fn(), + child: vi.fn().mockReturnThis(), + }; + + mockEventBus = { + emit: vi.fn(), + on: vi.fn(), + off: vi.fn(), + once: vi.fn(), + }; + + mockStorage = { + get: vi.fn().mockResolvedValue(null), + put: vi.fn().mockResolvedValue(undefined), + delete: vi.fn().mockResolvedValue(undefined), + list: vi.fn().mockResolvedValue({ keys: [] }), + }; + + connector = new NotificationConnector({ + adapter: mockAdapter, + storage: mockStorage, + logger: mockLogger, + eventBus: mockEventBus, + }); + }); + + describe('send', () => { + it('should send notification successfully', async () => { + const message = { + id: 'msg_123', + recipientId: 'user_123', + template: 'welcome', + category: 'system' as const, + priority: 'medium' as const, + params: { name: 'John' }, + }; + + const result = await connector.send(message); + + expect(result.status).toBe(NotificationStatus.SENT); + expect(result.messageId).toBe('msg_123'); + expect(result.deliveredAt).toBeDefined(); + expect(mockAdapter.deliver).toHaveBeenCalledWith('user_123', { text: 'formatted message' }); + expect(mockEventBus.emit).toHaveBeenCalledWith('notification:sent', expect.any(Object)); + }); + + it('should handle unreachable recipient', async () => { + vi.mocked(mockAdapter.checkReachability).mockResolvedValueOnce(false); + + const message = { + id: 'msg_123', + recipientId: 'user_123', + template: 'welcome', + category: 'system' as const, + priority: 'medium' as const, + }; + + const result = await connector.send(message); + + expect(result.status).toBe(NotificationStatus.BLOCKED); + expect(result.error).toBe('Recipient is not reachable'); + expect(mockAdapter.deliver).not.toHaveBeenCalled(); + expect(mockEventBus.emit).toHaveBeenCalledWith('notification:blocked', expect.any(Object)); + }); + + it('should retry on retryable error', async () => { + const error = new Error('Temporary error'); + vi.mocked(mockAdapter.deliver).mockRejectedValueOnce(error); + vi.mocked(mockAdapter.isRetryableError).mockReturnValueOnce(true); + + const message = { + id: 'msg_123', + recipientId: 'user_123', + template: 'welcome', + category: 'system' as const, + priority: 'medium' as const, + }; + + const result = await connector.send(message); + + expect(result.status).toBe(NotificationStatus.RETRY); + expect(result.retryCount).toBe(1); + expect(result.nextRetryAt).toBeDefined(); + expect(mockEventBus.emit).toHaveBeenCalledWith( + 'notification:failed', + expect.objectContaining({ + willRetry: true, + }), + ); + }); + + it('should fail after max retries', async () => { + const error = new Error('Persistent error'); + vi.mocked(mockAdapter.deliver).mockRejectedValue(error); + vi.mocked(mockAdapter.isRetryableError).mockReturnValue(false); + + const message = { + id: 'msg_123', + recipientId: 'user_123', + template: 'welcome', + category: 'system' as const, + priority: 'medium' as const, + metadata: { retryCount: 3 }, + }; + + const result = await connector.send(message); + + expect(result.status).toBe(NotificationStatus.FAILED); + expect(result.error).toBe('Persistent error'); + expect(mockEventBus.emit).toHaveBeenCalledWith( + 'notification:failed', + expect.objectContaining({ + willRetry: false, + }), + ); + }); + }); + + describe('sendBatch', () => { + it('should send notifications in batches', async () => { + const messages = Array.from({ length: 25 }, (_, i) => ({ + id: `msg_${i}`, + recipientId: `user_${i}`, + template: 'announcement', + category: 'system' as const, + priority: 'low' as const, + })); + + const results = await connector.sendBatch(messages, { + batchSize: 10, + delayBetweenBatches: 100, + }); + + expect(results).toHaveLength(25); + expect(results.every((r) => r.status === NotificationStatus.SENT)).toBe(true); + expect(mockAdapter.deliver).toHaveBeenCalledTimes(25); + expect(mockEventBus.emit).toHaveBeenCalledWith( + 'notification:batch:started', + expect.any(Object), + ); + expect(mockEventBus.emit).toHaveBeenCalledWith( + 'notification:batch:completed', + expect.any(Object), + ); + }); + + it('should handle failures in batch gracefully', async () => { + vi.mocked(mockAdapter.deliver) + .mockResolvedValueOnce(undefined) + .mockRejectedValueOnce(new Error('Failed')) + .mockResolvedValueOnce(undefined); + + const messages = [ + { + id: 'msg_1', + recipientId: 'user_1', + template: 'test', + category: 'system' as const, + priority: 'low' as const, + }, + { + id: 'msg_2', + recipientId: 'user_2', + template: 'test', + category: 'system' as const, + priority: 'low' as const, + }, + { + id: 'msg_3', + recipientId: 'user_3', + template: 'test', + category: 'system' as const, + priority: 'low' as const, + }, + ]; + + const results = await connector.sendBatch(messages, { + batchSize: 10, + delayBetweenBatches: 0, + }); + + expect(results).toHaveLength(3); + expect(results[0].status).toBe(NotificationStatus.SENT); + expect(results[1].status).toBe(NotificationStatus.RETRY); + expect(results[2].status).toBe(NotificationStatus.SENT); + }); + }); + + describe('isReachable', () => { + it('should return adapter reachability check result', async () => { + const result = await connector.isReachable('user_123'); + expect(result).toBe(true); + expect(mockAdapter.checkReachability).toHaveBeenCalledWith('user_123'); + }); + + it('should handle adapter errors gracefully', async () => { + vi.mocked(mockAdapter.checkReachability).mockRejectedValueOnce(new Error('Check failed')); + + const result = await connector.isReachable('user_123'); + expect(result).toBe(false); + expect(mockLogger.error).toHaveBeenCalled(); + }); + }); + + describe('getStatus', () => { + it('should retrieve stored notification status', async () => { + const storedResult = { + messageId: 'msg_123', + status: NotificationStatus.SENT, + deliveredAt: new Date().toISOString(), + }; + vi.mocked(mockStorage.get).mockResolvedValueOnce(JSON.stringify(storedResult)); + + const result = await connector.getStatus('msg_123'); + expect(result).toEqual(storedResult); + }); + + it('should return null when status not found', async () => { + const result = await connector.getStatus('msg_123'); + expect(result).toBeNull(); + }); + + it('should return null when storage not available', async () => { + const connectorWithoutStorage = new NotificationConnector({ + adapter: mockAdapter, + logger: mockLogger, + eventBus: mockEventBus, + }); + + const result = await connectorWithoutStorage.getStatus('msg_123'); + expect(result).toBeNull(); + }); + }); +}); diff --git a/src/connectors/notification-connector.ts b/src/connectors/notification-connector.ts new file mode 100644 index 0000000..ea1ed6b --- /dev/null +++ b/src/connectors/notification-connector.ts @@ -0,0 +1,328 @@ +/** + * Base notification connector with retry logic and batch processing + * Platform-agnostic implementation for reliable message delivery + */ + +import * as Sentry from '@sentry/cloudflare'; + +import { NotificationStatus } from '../core/interfaces/notification'; +import type { + INotificationConnector, + INotificationAdapter, + NotificationMessage, + NotificationResult, + BatchNotificationOptions, + RetryConfig, +} from '../core/interfaces/notification'; +import type { IKeyValueStore } from '../core/interfaces/storage'; +import type { ILogger } from '../core/interfaces/logger'; +import type { IEventBus } from '../core/interfaces/event-bus'; + +interface NotificationConnectorDeps { + adapter: INotificationAdapter; + storage?: IKeyValueStore; + logger: ILogger; + eventBus: IEventBus; + retryConfig?: RetryConfig; +} + +export class NotificationConnector implements INotificationConnector { + private adapter: INotificationAdapter; + private storage?: IKeyValueStore; + private logger: ILogger; + private eventBus: IEventBus; + private retryConfig: RetryConfig; + private activeRetries = new Map(); + + constructor(deps: NotificationConnectorDeps) { + this.adapter = deps.adapter; + this.storage = deps.storage; + this.logger = deps.logger; + this.eventBus = deps.eventBus; + this.retryConfig = deps.retryConfig || { + maxAttempts: 3, + initialDelay: 1000, + maxDelay: 60000, + backoffMultiplier: 2, + }; + } + + async send(message: NotificationMessage): Promise { + const span = Sentry.startInactiveSpan({ + op: 'notification.send', + name: `Send ${message.category} notification`, + }); + + try { + // Check if recipient is reachable + const reachable = await this.isReachable(message.recipientId); + if (!reachable) { + const result: NotificationResult = { + messageId: message.id, + status: NotificationStatus.BLOCKED, + error: 'Recipient is not reachable', + }; + + this.eventBus.emit('notification:blocked', { + recipientId: message.recipientId, + reason: 'User blocked or unavailable', + }); + + return result; + } + + // Get user info for locale + const userInfo = await this.adapter.getUserInfo(message.recipientId); + const locale = userInfo.locale || 'en'; + + // Format message using adapter + const formattedMessage = await this.adapter.formatMessage( + { + id: message.template, + name: message.template, + category: message.category, + content: { + [locale]: { + body: message.template, + }, + }, + }, + message.params || {}, + locale, + ); + + // Deliver message + await this.adapter.deliver(message.recipientId, formattedMessage); + + const result: NotificationResult = { + messageId: message.id, + status: NotificationStatus.SENT, + deliveredAt: new Date(), + }; + + // Store result if storage available + if (this.storage) { + await this.storage.put( + `notification:${message.id}`, + JSON.stringify(result), + { expirationTtl: 86400 }, // 24 hours + ); + } + + this.eventBus.emit('notification:sent', { + messageId: message.id, + recipientId: message.recipientId, + category: message.category, + templateId: message.template, + }); + + span.end(); + return result; + } catch (error) { + span.setAttribute('error', true); + span.end(); + + const errorMessage = error instanceof Error ? error.message : 'Unknown error'; + const isRetryable = this.adapter.isRetryableError(error); + const retryCount = (message.metadata?.retryCount as number) || 0; + + if (isRetryable && retryCount < this.retryConfig.maxAttempts) { + return this.scheduleRetry(message, errorMessage); + } + + const result: NotificationResult = { + messageId: message.id, + status: NotificationStatus.FAILED, + error: errorMessage, + retryCount: retryCount, + }; + + this.eventBus.emit('notification:failed', { + messageId: message.id, + recipientId: message.recipientId, + error: errorMessage, + willRetry: false, + }); + + Sentry.captureException(error, { + tags: { + component: 'notification-connector', + messageId: message.id, + category: message.category, + }, + }); + + return result; + } + } + + async sendBatch( + messages: NotificationMessage[], + options: BatchNotificationOptions, + ): Promise { + const batchId = `batch-${Date.now()}`; + const results: NotificationResult[] = []; + let processed = 0; + let failed = 0; + + this.eventBus.emit('notification:batch:started', { + batchId, + totalMessages: messages.length, + }); + + // Process in batches + for (let i = 0; i < messages.length; i += options.batchSize) { + const batch = messages.slice(i, i + options.batchSize); + + // Send batch concurrently + const batchResults = await Promise.all( + batch.map(async (message) => { + try { + const result = await this.send(message); + if (result.status === NotificationStatus.FAILED) { + failed++; + } + return result; + } catch (error) { + failed++; + return { + messageId: message.id, + status: NotificationStatus.FAILED, + error: error instanceof Error ? error.message : 'Unknown error', + }; + } + }), + ); + + results.push(...batchResults); + processed += batch.length; + + this.eventBus.emit('notification:batch:progress', { + batchId, + processed, + total: messages.length, + failed, + }); + + // Delay between batches + if (i + options.batchSize < messages.length && options.delayBetweenBatches > 0) { + await this.delay(options.delayBetweenBatches); + } + } + + this.eventBus.emit('notification:batch:completed', { + batchId, + sent: processed - failed, + failed, + duration: Date.now() - parseInt(batchId.split('-')[1] || '0'), + }); + + return results; + } + + async isReachable(recipientId: string): Promise { + try { + return await this.adapter.checkReachability(recipientId); + } catch (error) { + this.logger.error('Failed to check reachability', { + recipientId, + error: error instanceof Error ? error.message : 'Unknown error', + }); + return false; + } + } + + async getStatus(messageId: string): Promise { + if (!this.storage) { + return null; + } + + try { + const stored = await this.storage.get(`notification:${messageId}`); + if (!stored) { + return null; + } + + return JSON.parse(stored) as NotificationResult; + } catch (error) { + this.logger.error('Failed to get notification status', { + messageId, + error: error instanceof Error ? error.message : 'Unknown error', + }); + return null; + } + } + + private async scheduleRetry( + message: NotificationMessage, + error: string, + ): Promise { + const retryCount = ((message.metadata?.retryCount as number) || 0) + 1; + const delay = this.calculateRetryDelay(retryCount); + + const result: NotificationResult = { + messageId: message.id, + status: NotificationStatus.RETRY, + error, + retryCount, + nextRetryAt: new Date(Date.now() + delay), + }; + + this.eventBus.emit('notification:failed', { + messageId: message.id, + recipientId: message.recipientId, + error, + willRetry: true, + }); + + // Store retry info + if (this.storage) { + await this.storage.put(`notification:${message.id}`, JSON.stringify(result), { + expirationTtl: 86400, + }); + } + + // Schedule retry + const timeout = setTimeout(async () => { + this.activeRetries.delete(message.id); + + const retryMessage = { + ...message, + metadata: { + ...message.metadata, + retryCount, + }, + }; + + await this.send(retryMessage); + }, delay); + + this.activeRetries.set(message.id, timeout); + + return result; + } + + private calculateRetryDelay(attempt: number): number { + const delay = Math.min( + this.retryConfig.initialDelay * Math.pow(this.retryConfig.backoffMultiplier, attempt - 1), + this.retryConfig.maxDelay, + ); + + // Add jitter (ยฑ10%) + const jitter = delay * 0.1; + return Math.floor(delay + (Math.random() * 2 - 1) * jitter); + } + + private delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + async stop(): Promise { + // Cancel all active retries + for (const [messageId, timeout] of this.activeRetries) { + clearTimeout(timeout); + this.logger.info('Cancelled retry for message', { messageId }); + } + this.activeRetries.clear(); + } +} diff --git a/src/core/interfaces/event-bus.ts b/src/core/interfaces/event-bus.ts new file mode 100644 index 0000000..fd116cb --- /dev/null +++ b/src/core/interfaces/event-bus.ts @@ -0,0 +1,33 @@ +/** + * Event bus interface for decoupled communication + */ + +export interface EventBusEvents { + [key: string]: unknown; +} + +export interface IEventBus { + /** + * Emit an event + */ + emit(event: K, data: EventBusEvents[K]): void; + emit(event: string, data: unknown): void; + + /** + * Subscribe to an event + */ + on(event: K, handler: (data: EventBusEvents[K]) => void): void; + on(event: string, handler: (data: unknown) => void): void; + + /** + * Unsubscribe from an event + */ + off(event: K, handler: (data: EventBusEvents[K]) => void): void; + off(event: string, handler: (data: unknown) => void): void; + + /** + * Subscribe to an event once + */ + once(event: K, handler: (data: EventBusEvents[K]) => void): void; + once(event: string, handler: (data: unknown) => void): void; +} diff --git a/src/core/interfaces/logger.ts b/src/core/interfaces/logger.ts new file mode 100644 index 0000000..2e9d2ba --- /dev/null +++ b/src/core/interfaces/logger.ts @@ -0,0 +1,30 @@ +/** + * Logger interface for universal logging across platforms + */ + +export interface ILogger { + /** + * Debug level logging + */ + debug(message: string, meta?: Record): void; + + /** + * Info level logging + */ + info(message: string, meta?: Record): void; + + /** + * Warning level logging + */ + warn(message: string, meta?: Record): void; + + /** + * Error level logging + */ + error(message: string, meta?: Record): void; + + /** + * Create child logger with additional context + */ + child(meta: Record): ILogger; +} diff --git a/src/core/interfaces/notification.ts b/src/core/interfaces/notification.ts new file mode 100644 index 0000000..c4873d3 --- /dev/null +++ b/src/core/interfaces/notification.ts @@ -0,0 +1,286 @@ +/** + * Universal notification system interfaces + * Platform-agnostic contracts for sending notifications across different messaging platforms + */ + +import type { EventBusEvents } from './event-bus.js'; + +/** + * Notification categories that users can control + */ +export enum NotificationCategory { + AUCTION = 'auction', + BALANCE = 'balance', + SERVICE = 'service', + SYSTEM = 'system', +} + +/** + * Notification priority levels + */ +export enum NotificationPriority { + LOW = 'low', + MEDIUM = 'medium', + HIGH = 'high', + URGENT = 'urgent', +} + +/** + * Notification delivery status + */ +export enum NotificationStatus { + PENDING = 'pending', + SENT = 'sent', + FAILED = 'failed', + BLOCKED = 'blocked', + RETRY = 'retry', +} + +/** + * Base notification message interface + */ +export interface NotificationMessage { + id: string; + recipientId: string; + category: NotificationCategory; + priority: NotificationPriority; + template: string; + params?: Record; + metadata?: Record; + scheduledAt?: Date; + expiresAt?: Date; +} + +/** + * Notification delivery result + */ +export interface NotificationResult { + messageId: string; + status: NotificationStatus; + deliveredAt?: Date; + error?: string; + retryCount?: number; + nextRetryAt?: Date; +} + +/** + * User notification preferences + */ +export interface NotificationPreferences { + userId: string; + categories: { + [K in NotificationCategory]?: boolean; + }; + quiet_hours?: { + enabled: boolean; + start: string; // HH:MM format + end: string; // HH:MM format + timezone?: string; + }; + blockedUntil?: Date; +} + +/** + * Batch notification options + */ +export interface BatchNotificationOptions { + batchSize: number; + delayBetweenBatches: number; + priority?: NotificationPriority; + skipBlocked?: boolean; + respectQuietHours?: boolean; +} + +/** + * Retry configuration + */ +export interface RetryConfig { + maxAttempts: number; + initialDelay: number; + maxDelay: number; + backoffMultiplier: number; + retryableErrors?: string[]; +} + +/** + * Notification template + */ +export interface NotificationTemplate { + id: string; + name: string; + category: NotificationCategory; + content: { + [locale: string]: { + subject?: string; + body: string; + actions?: Array<{ + type: string; + label: string; + url?: string; + data?: Record; + }>; + }; + }; + variables?: Array<{ + name: string; + type: 'string' | 'number' | 'boolean' | 'date'; + required: boolean; + format?: string; + }>; +} + +/** + * Notification connector interface - low-level message delivery + */ +export interface INotificationConnector { + /** + * Send a single notification + */ + send(message: NotificationMessage): Promise; + + /** + * Send notifications in batches + */ + sendBatch( + messages: NotificationMessage[], + options: BatchNotificationOptions, + ): Promise; + + /** + * Check if recipient is reachable + */ + isReachable(recipientId: string): Promise; + + /** + * Get delivery status for a message + */ + getStatus(messageId: string): Promise; +} + +/** + * Notification service interface - high-level business logic + */ +export interface INotificationService { + /** + * Send notification using template + */ + notify( + recipientId: string, + templateId: string, + params?: Record, + options?: { + priority?: NotificationPriority; + scheduledAt?: Date; + expiresAt?: Date; + }, + ): Promise; + + /** + * Send bulk notifications + */ + notifyBulk( + recipientIds: string[], + templateId: string, + params?: Record, + options?: BatchNotificationOptions, + ): Promise; + + /** + * Get user preferences + */ + getUserPreferences(userId: string): Promise; + + /** + * Update user preferences + */ + updateUserPreferences( + userId: string, + preferences: Partial, + ): Promise; + + /** + * Register notification template + */ + registerTemplate(template: NotificationTemplate): Promise; + + /** + * Get all templates + */ + getTemplates(category?: NotificationCategory): Promise; +} + +/** + * Events emitted by notification system + */ +export interface NotificationEvents extends EventBusEvents { + 'notification:sent': { + messageId: string; + recipientId: string; + category: NotificationCategory; + templateId: string; + }; + 'notification:failed': { + messageId: string; + recipientId: string; + error: string; + willRetry: boolean; + }; + 'notification:blocked': { + recipientId: string; + reason: string; + }; + 'notification:batch:started': { + batchId: string; + totalMessages: number; + }; + 'notification:batch:progress': { + batchId: string; + processed: number; + total: number; + failed: number; + }; + 'notification:batch:completed': { + batchId: string; + sent: number; + failed: number; + duration: number; + }; +} + +/** + * Platform-specific notification adapter + */ +export interface INotificationAdapter { + /** + * Format message for specific platform + */ + formatMessage( + template: NotificationTemplate, + params: Record, + locale: string, + ): Promise; + + /** + * Handle platform-specific delivery + */ + deliver(recipientId: string, message: unknown): Promise; + + /** + * Check platform-specific reachability + */ + checkReachability(recipientId: string): Promise; + + /** + * Handle platform-specific errors + */ + isRetryableError(error: unknown): boolean; + + /** + * Get platform-specific user info + */ + getUserInfo(recipientId: string): Promise<{ + locale?: string; + timezone?: string; + blocked?: boolean; + }>; +} diff --git a/src/core/interfaces/user-preference.ts b/src/core/interfaces/user-preference.ts new file mode 100644 index 0000000..9ed0d93 --- /dev/null +++ b/src/core/interfaces/user-preference.ts @@ -0,0 +1,25 @@ +/** + * User preference interfaces + * For managing notification preferences + */ + +import type { NotificationCategory } from './notification'; + +export interface NotificationPreferences { + enabled: boolean; + categories: Record; + quiet_hours?: { + enabled: boolean; + start: string; // HH:MM format + end: string; // HH:MM format + timezone: string; + }; +} + +export interface IUserPreferenceService { + getNotificationPreferences(userId: string): Promise; + updateNotificationPreferences( + userId: string, + preferences: Partial, + ): Promise; +} \ No newline at end of file diff --git a/src/core/services/notification-service.ts b/src/core/services/notification-service.ts new file mode 100644 index 0000000..c40b9fe --- /dev/null +++ b/src/core/services/notification-service.ts @@ -0,0 +1,219 @@ +/** + * Universal notification service + * Platform-agnostic implementation for sending notifications + */ + +import type { + INotificationConnector, + NotificationMessage, + BatchNotificationOptions, +} from '../interfaces/notification'; +import { NotificationCategory, NotificationPriority } from '../interfaces/notification'; +import type { IUserPreferenceService } from '../interfaces/user-preference'; +import type { ILogger } from '../interfaces/logger'; +import type { IEventBus } from '../interfaces/event-bus'; + +export interface NotificationContext { + type: string; + data: Record; + locale?: string; +} + +export interface NotificationServiceDeps { + connector: INotificationConnector; + userPreferenceService?: IUserPreferenceService; + logger: ILogger; + eventBus: IEventBus; + defaultLocale?: string; +} + +export interface INotificationService { + send( + recipientId: string, + template: string, + context: NotificationContext, + category?: NotificationCategory, + ): Promise; + + sendBatch( + recipientIds: string[], + template: string, + context: NotificationContext, + options?: BatchNotificationOptions, + ): Promise; + + sendBulk(recipientIds: string[], message: string, category?: NotificationCategory): Promise; +} + +export class NotificationService implements INotificationService { + private connector: INotificationConnector; + private userPreferenceService?: IUserPreferenceService; + private logger: ILogger; + private eventBus: IEventBus; + private defaultLocale: string; + + constructor(deps: NotificationServiceDeps) { + this.connector = deps.connector; + this.userPreferenceService = deps.userPreferenceService; + this.logger = deps.logger; + this.eventBus = deps.eventBus; + this.defaultLocale = deps.defaultLocale || 'en'; + } + + async send( + recipientId: string, + template: string, + context: NotificationContext, + category: NotificationCategory = NotificationCategory.SYSTEM, + ): Promise { + try { + // Check user preferences if service is available + if (this.userPreferenceService) { + const preferences = + await this.userPreferenceService.getNotificationPreferences(recipientId); + if (!preferences.categories[category]) { + this.logger.debug('Notification blocked by user preference', { + recipientId, + category, + template, + }); + return; + } + } + + // Create notification message + const message: NotificationMessage = { + id: `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`, + recipientId, + template, + params: context.data, + category, + priority: this.getPriorityForCategory(category), + metadata: { + type: context.type, + locale: context.locale || this.defaultLocale, + }, + }; + + // Send via connector + const result = await this.connector.send(message); + + // Emit event + this.eventBus.emit('notification:processed', { + recipientId, + template, + category, + status: result.status, + }); + } catch (error) { + this.logger.error('Failed to send notification', { + recipientId, + template, + error: error instanceof Error ? error.message : 'Unknown error', + }); + + // Re-throw to let caller handle + throw error; + } + } + + async sendBatch( + recipientIds: string[], + template: string, + context: NotificationContext, + options: BatchNotificationOptions = { batchSize: 50, delayBetweenBatches: 1000 }, + ): Promise { + const messages: NotificationMessage[] = []; + const category = (context.data.category as NotificationCategory) || NotificationCategory.SYSTEM; + + // Filter recipients based on preferences + const allowedRecipients = await this.filterRecipientsByPreferences(recipientIds, category); + + // Create messages for allowed recipients + for (const recipientId of allowedRecipients) { + messages.push({ + id: `${Date.now()}-${Math.random().toString(36).substring(2, 9)}`, + recipientId, + template, + params: context.data, + category, + priority: this.getPriorityForCategory(category), + metadata: { + type: context.type, + locale: context.locale || this.defaultLocale, + batchId: `batch-${Date.now()}`, + }, + }); + } + + if (messages.length === 0) { + this.logger.info('No recipients to notify after preference filtering', { + originalCount: recipientIds.length, + category, + }); + return; + } + + // Send batch via connector + await this.connector.sendBatch(messages, options); + } + + async sendBulk( + recipientIds: string[], + message: string, + category: NotificationCategory = NotificationCategory.SYSTEM, + ): Promise { + await this.sendBatch( + recipientIds, + 'bulk-message', + { + type: 'bulk', + data: { message, category }, + }, + { batchSize: 100, delayBetweenBatches: 500 }, + ); + } + + private async filterRecipientsByPreferences( + recipientIds: string[], + category: NotificationCategory, + ): Promise { + if (!this.userPreferenceService) { + return recipientIds; + } + + const allowed: string[] = []; + + for (const recipientId of recipientIds) { + try { + const preferences = + await this.userPreferenceService.getNotificationPreferences(recipientId); + if (preferences.categories[category]) { + allowed.push(recipientId); + } + } catch (error) { + // If we can't get preferences, assume notifications are allowed + this.logger.warn('Failed to get user preferences, allowing notification', { + recipientId, + error: error instanceof Error ? error.message : 'Unknown error', + }); + allowed.push(recipientId); + } + } + + return allowed; + } + + private getPriorityForCategory(category: NotificationCategory): NotificationPriority { + switch (category) { + case NotificationCategory.SYSTEM: + return NotificationPriority.HIGH; + case NotificationCategory.BALANCE: + return NotificationPriority.MEDIUM; + case NotificationCategory.SERVICE: + return NotificationPriority.MEDIUM; + default: + return NotificationPriority.LOW; + } + } +}