diff --git a/.dockerignore b/.dockerignore
new file mode 100644
index 0000000..bfcddef
--- /dev/null
+++ b/.dockerignore
@@ -0,0 +1,39 @@
+# Git
+.git
+.gitignore
+
+# IDE
+.idea
+.vscode
+*.swp
+*.swo
+
+# Build artifacts
+bin/
+*.o
+*.a
+*.so
+
+# Test files
+*_test.go
+testdata/
+
+# Documentation
+*.md
+docs/
+
+# CI/CD
+.github/
+.gitlab-ci.yml
+
+# Kubernetes
+k8s/
+*.yaml
+!config.yaml
+
+# Development
+.env
+.env.local
+
+# macOS
+.DS_Store
diff --git a/.idea/.gitignore b/.idea/.gitignore
new file mode 100644
index 0000000..13566b8
--- /dev/null
+++ b/.idea/.gitignore
@@ -0,0 +1,8 @@
+# Default ignored files
+/shelf/
+/workspace.xml
+# Editor-based HTTP Client requests
+/httpRequests/
+# Datasource local storage ignored files
+/dataSources/
+/dataSources.local.xml
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..ede7042
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/notifier.iml b/.idea/notifier.iml
new file mode 100644
index 0000000..5e764c4
--- /dev/null
+++ b/.idea/notifier.iml
@@ -0,0 +1,9 @@
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..35eb1dd
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md
new file mode 100644
index 0000000..6c35dc2
--- /dev/null
+++ b/ARCHITECTURE.md
@@ -0,0 +1,320 @@
+# Notifier Microservice Architecture
+
+## Overview
+
+The Notifier microservice is designed to provide a flexible, scalable notification delivery system that supports multiple notification channels (SMTP, Slack, Ntfy, Stdout) with both REST and gRPC APIs.
+
+## Architecture Principles
+
+- **Clean Architecture**: Domain-driven design with clear separation of concerns
+- **Interface-based Design**: All core components use interfaces for maximum flexibility
+- **Pluggable Notifiers**: Easy to add new notification providers
+- **Queue Abstraction**: Supports both local and distributed queues (Kafka)
+- **Configuration-driven**: Viper-based configuration with environment variable support
+- **Cloud-native**: Containerized with Kubernetes support
+
+## Core Components
+
+### 1. Domain Layer (`internal/domain/`)
+
+The domain layer defines the core business logic and interfaces:
+
+#### `notification.go`
+- **Notification**: Core notification entity with metadata, status tracking, and retry logic
+- **Priority**: Enumeration for notification urgency (Low, Normal, High, Critical)
+- **NotificationType**: Supported notification channels (Email, Slack, Ntfy, Stdout)
+- **NotificationStatus**: Lifecycle states (Pending, Queued, Processing, Sent, Failed, Retrying)
+- **NotificationResult**: Outcome of notification delivery attempts
+- **NotificationFilter**: Query interface for retrieving notifications
+
+#### `notifier.go`
+- **Notifier**: Core interface that all notification implementations must satisfy
+- **NotifierFactory**: Factory pattern for creating notifier instances
+- **NotificationService**: High-level service interface for notification operations
+- **NotificationStats**: Statistics and metrics about notification processing
+
+#### `queue.go`
+- **Queue**: Interface for notification queue implementations
+- **QueueMessage**: Wrapper around notifications with queue-specific metadata
+- **QueueConfig**: Configuration for queue implementations
+- **LocalQueueConfig**: In-memory queue configuration
+- **KafkaQueueConfig**: Distributed Kafka queue configuration
+
+### 2. Queue Implementation (`internal/queue/`)
+
+#### `local.go`
+- In-memory queue implementation using Go channels
+- Optional disk persistence for durability
+- Thread-safe with mutex protection
+- Supports enqueue, dequeue, ack, and nack operations
+- Configurable buffer size and retry behavior
+
+### 3. Notifier Implementations (`internal/notifier/`)
+
+#### `notifier.go`
+- **Factory**: Manages and creates notifier instances
+- **BaseNotifier**: Common functionality shared by all notifiers
+- Validation and context checking utilities
+
+#### `stdout.go`
+- Simple stdout notifier for debugging and development
+- Prints notifications to console with formatted output
+
+#### `smtp.go`
+- Email notifications via SMTP
+- Supports TLS/SSL
+- Configurable SMTP server, port, and authentication
+- RFC-compliant email message formatting
+
+#### `ntfy.go`
+- Integration with ntfy.sh push notification service
+- Supports custom ntfy servers
+- Priority mapping and metadata support
+- Rich notification features (tags, click actions, attachments)
+
+#### `slack.go`
+- Slack webhook integration
+- Channel-specific webhook support
+- Rich message formatting with blocks
+- Priority indicators and custom branding
+
+### 4. Configuration (`internal/config/`)
+
+#### `config.go`
+- Viper-based configuration management
+- Support for YAML config files and environment variables
+- Default value handling
+- Comprehensive validation
+- Hierarchical configuration structure:
+ - Server settings (ports, host, mode)
+ - Queue configuration
+ - Notifier credentials
+ - Logging settings
+ - Metrics and observability
+ - Health check configuration
+
+### 5. API Layer
+
+#### gRPC API (`api/grpc/`)
+
+**`notifier.proto`**
+- Protocol buffer definitions for the gRPC service
+- Operations:
+ - `SendNotification`: Send single notification
+ - `SendBatchNotifications`: Send multiple notifications
+ - `GetNotification`: Retrieve notification by ID
+ - `ListNotifications`: Query notifications with filters
+ - `CancelNotification`: Cancel pending notification
+ - `RetryNotification`: Retry failed notification
+ - `GetStats`: Retrieve service statistics
+ - `HealthCheck`: Service health verification
+
+#### REST API (`api/rest/`)
+
+**`handlers.go`**
+- HTTP handlers for REST endpoints
+- Request validation and error handling
+- JSON serialization/deserialization
+
+**`router.go`**
+- Gorilla mux router configuration
+- Middleware for logging and CORS
+- Route definitions matching gRPC operations
+
+**`types.go`**
+- REST API request/response types
+- Domain model conversions
+- Validation logic
+
+### 6. Entry Points (`cmd/`)
+
+#### `grpcserver/main.go`
+- Standalone gRPC server
+- Service initialization and dependency injection
+- Graceful shutdown handling
+
+#### `restserver/main.go`
+- Standalone REST server
+- HTTP server configuration
+- Graceful shutdown handling
+
+## Data Flow
+
+### Sending a Notification
+
+```
+Client Request (REST/gRPC)
+ ↓
+API Handler
+ ↓
+NotificationService.Send()
+ ↓
+Queue.Enqueue()
+ ↓
+[Notification queued]
+ ↓
+Worker dequeues (Queue.Dequeue())
+ ↓
+NotifierFactory.Create()
+ ↓
+Notifier.Send()
+ ↓
+Provider API (SMTP/Slack/Ntfy/Stdout)
+ ↓
+Queue.Ack() or Queue.Nack()
+ ↓
+Update notification status
+ ↓
+[Notification sent or failed]
+```
+
+## Queue Strategies
+
+### Local Queue
+- In-memory implementation using Go channels
+- Fast and simple for single-instance deployments
+- Optional disk persistence for durability
+- Suitable for development and small-scale production
+
+### Kafka Queue (Future)
+- Distributed queue for multi-instance deployments
+- Guarantees delivery across service restarts
+- Horizontal scalability
+- Exactly-once semantics with idempotence
+- Suitable for high-throughput production environments
+
+## Notification Lifecycle
+
+1. **Pending**: Notification created but not yet queued
+2. **Queued**: Added to queue, waiting for processing
+3. **Processing**: Worker has dequeued and is sending
+4. **Sent**: Successfully delivered to provider
+5. **Failed**: Delivery failed after max retries
+6. **Retrying**: Temporarily failed, will retry
+
+## Retry Strategy
+
+- Configurable max retries (default: 3)
+- Pluggable backoff strategies:
+ - **Exponential**: 2^n delay between retries
+ - **Linear**: Fixed increment between retries
+ - **Fixed**: Constant delay between retries
+
+## Configuration Management
+
+### Hierarchy (highest to lowest priority)
+1. Environment variables (prefixed with `NOTIFIER_`)
+2. Configuration file (config.yaml)
+3. Default values
+
+### Example Environment Variables
+```bash
+NOTIFIER_SERVER_GRPC_PORT=50051
+NOTIFIER_SERVER_REST_PORT=8080
+NOTIFIER_QUEUE_TYPE=local
+NOTIFIER_NOTIFIERS_SMTP_HOST=smtp.gmail.com
+NOTIFIER_NOTIFIERS_SLACK_WEBHOOK_URL=https://hooks.slack.com/...
+```
+
+## Deployment Strategies
+
+### Docker Compose
+- Single-host deployment
+- All components in one compose file
+- Optional Kafka, Prometheus, and Grafana services
+- Volume mounts for configuration and persistence
+
+### Kubernetes
+- Multi-instance deployment with HPA
+- Separate services for REST and gRPC
+- ConfigMaps for configuration
+- Secrets for credentials
+- Ingress for external access
+- Health checks and readiness probes
+- Resource limits and requests
+
+### Scaling Considerations
+
+#### Horizontal Scaling
+- Multiple instances can run concurrently
+- Use Kafka queue for distributed message processing
+- Stateless design allows easy scaling
+
+#### Vertical Scaling
+- Increase worker count per instance
+- Tune queue buffer sizes
+- Adjust resource limits
+
+## Observability
+
+### Metrics (Prometheus)
+- Total notifications sent/failed
+- Notifications by type and status
+- Average latency
+- Queue size
+- Worker utilization
+
+### Health Checks
+- Dedicated health endpoint
+- Component-level health reporting
+- Kubernetes liveness/readiness probes
+
+### Logging
+- Structured JSON logging
+- Configurable log levels
+- Request/response logging
+- Error tracking
+
+## Security
+
+### Authentication
+- API keys for REST endpoints (to be implemented)
+- mTLS for gRPC (to be implemented)
+- Kubernetes RBAC for service account
+
+### Authorization
+- Per-notifier credential management
+- Secrets stored in Kubernetes Secrets or external secret manager
+- No credentials in configuration files
+
+### Network Security
+- Non-root container user
+- Read-only root filesystem where possible
+- Minimal container image (Alpine-based)
+- Security context restrictions
+
+## Extension Points
+
+### Adding a New Notifier
+
+1. Create new file in `internal/notifier/`
+2. Implement the `domain.Notifier` interface:
+ - `Send(ctx, notification) -> result`
+ - `Type() -> NotificationType`
+ - `Validate(notification) -> error`
+ - `Close() -> error`
+3. Add configuration struct to `internal/config/`
+4. Register in factory during initialization
+5. Update protobuf and REST API types
+6. Add configuration example to `config.yaml`
+
+### Adding a New Queue Implementation
+
+1. Create new file in `internal/queue/`
+2. Implement the `domain.Queue` interface
+3. Add configuration to `domain.QueueConfig`
+4. Update queue factory/initialization logic
+5. Add configuration example
+
+## Next Steps for Implementation
+
+1. **Generate Protocol Buffers**: Run `make proto-gen` to generate gRPC code
+2. **Implement Service Layer**: Create the main service that ties everything together
+3. **Implement Server Main Functions**: Wire up dependencies in `cmd/`
+4. **Add Tests**: Unit tests for notifiers, integration tests for queue
+5. **Add Kafka Queue**: Implement distributed queue using Kafka
+6. **Add Metrics**: Prometheus instrumentation
+7. **Add Authentication**: API key or OAuth support
+8. **Add Rate Limiting**: Prevent abuse and manage provider quotas
+9. **Add Notification Templates**: Support for templated messages
+10. **Add Webhooks**: Allow callbacks on notification status changes
diff --git a/Dockerfile b/Dockerfile
index e69de29..872f2d1 100644
--- a/Dockerfile
+++ b/Dockerfile
@@ -0,0 +1,55 @@
+# Build stage
+FROM golang:1.23.2-alpine AS builder
+
+# Install build dependencies
+RUN apk add --no-cache git make
+
+# Set working directory
+WORKDIR /build
+
+# Copy go mod files
+COPY go.mod go.sum ./
+
+# Download dependencies
+RUN go mod download
+
+# Copy source code
+COPY . .
+
+# Build binaries
+RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o grpcserver ./cmd/grpcserver
+RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o restserver ./cmd/restserver
+
+# Runtime stage
+FROM alpine:latest
+
+# Install runtime dependencies
+RUN apk --no-cache add ca-certificates tzdata
+
+# Create non-root user
+RUN addgroup -g 1000 notifier && \
+ adduser -D -u 1000 -G notifier notifier
+
+# Set working directory
+WORKDIR /app
+
+# Copy binaries from builder
+COPY --from=builder /build/grpcserver /app/
+COPY --from=builder /build/restserver /app/
+
+# Copy default config (can be overridden with volume mount)
+COPY config.yaml /app/config.yaml
+
+# Create directory for queue persistence
+RUN mkdir -p /var/lib/notifier && \
+ chown -R notifier:notifier /var/lib/notifier
+
+# Change to non-root user
+USER notifier
+
+# Expose ports
+EXPOSE 8080 50051 9090 8081
+
+# Default to running both servers
+# Can be overridden with docker run command
+CMD ["sh", "-c", "/app/grpcserver & /app/restserver"]
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..631216a
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,98 @@
+.PHONY: proto proto-gen proto-clean deps build run-grpc run-rest run-both test lint docker-build docker-run clean
+
+# Variables
+PROTO_DIR=api/grpc
+PROTO_FILE=$(PROTO_DIR)/notifier.proto
+PROTO_OUT=$(PROTO_DIR)/pb
+GO_MODULE=$(shell head -n 1 go.mod | awk '{print $$2}')
+
+# Generate protobuf code
+proto-gen:
+ @echo "Generating protobuf code..."
+ @mkdir -p $(PROTO_OUT)
+ protoc --go_out=$(PROTO_OUT) --go_opt=paths=source_relative \
+ --go-grpc_out=$(PROTO_OUT) --go-grpc_opt=paths=source_relative \
+ $(PROTO_FILE)
+ @echo "Protobuf code generated successfully"
+
+# Clean generated protobuf code
+proto-clean:
+ @echo "Cleaning generated protobuf code..."
+ @rm -rf $(PROTO_OUT)
+ @echo "Cleaned"
+
+# Install dependencies
+deps:
+ @echo "Installing dependencies..."
+ go mod download
+ go mod tidy
+ @echo "Dependencies installed"
+
+# Install protoc plugins
+proto-deps:
+ @echo "Installing protoc plugins..."
+ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
+ go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
+ @echo "Protoc plugins installed"
+
+# Build binaries
+build:
+ @echo "Building binaries..."
+ @mkdir -p bin
+ go build -o bin/grpcserver ./cmd/grpcserver
+ go build -o bin/restserver ./cmd/restserver
+ @echo "Binaries built successfully"
+
+# Run gRPC server
+run-grpc:
+ @echo "Running gRPC server..."
+ go run ./cmd/grpcserver/main.go
+
+# Run REST server
+run-rest:
+ @echo "Running REST server..."
+ go run ./cmd/restserver/main.go
+
+# Run tests
+test:
+ @echo "Running tests..."
+ go test -v -race -cover ./...
+
+# Run linter
+lint:
+ @echo "Running linter..."
+ golangci-lint run ./...
+
+# Build Docker image
+docker-build:
+ @echo "Building Docker image..."
+ docker build -t notifier:latest .
+ @echo "Docker image built successfully"
+
+# Run Docker container
+docker-run:
+ @echo "Running Docker container..."
+ docker run -p 8080:8080 -p 50051:50051 -v $(PWD)/config.yaml:/app/config.yaml notifier:latest
+
+# Clean build artifacts
+clean:
+ @echo "Cleaning build artifacts..."
+ @rm -rf bin/
+ @rm -rf $(PROTO_OUT)
+ @echo "Cleaned"
+
+# Help
+help:
+ @echo "Available targets:"
+ @echo " proto-gen - Generate protobuf code"
+ @echo " proto-clean - Clean generated protobuf code"
+ @echo " proto-deps - Install protoc plugins"
+ @echo " deps - Install Go dependencies"
+ @echo " build - Build binaries"
+ @echo " run-grpc - Run gRPC server"
+ @echo " run-rest - Run REST server"
+ @echo " test - Run tests"
+ @echo " lint - Run linter"
+ @echo " docker-build - Build Docker image"
+ @echo " docker-run - Run Docker container"
+ @echo " clean - Clean build artifacts"
diff --git a/QUICKSTART.md b/QUICKSTART.md
new file mode 100644
index 0000000..bde2bc3
--- /dev/null
+++ b/QUICKSTART.md
@@ -0,0 +1,329 @@
+# Quick Start Guide
+
+## Prerequisites
+
+1. Go 1.23.2 or higher installed
+2. Dependencies installed: `go mod tidy`
+
+## Running the REST Server
+
+### Option 1: Using go run
+```bash
+go run ./cmd/restserver/main.go
+```
+
+### Option 2: Build and run
+```bash
+go build -o bin/restserver ./cmd/restserver
+./bin/restserver
+```
+
+### Option 3: Using Make
+```bash
+make build
+make run-rest
+```
+
+The server will start on `http://localhost:8080` by default.
+
+## Testing with cURL
+
+### 1. Health Check
+```bash
+curl http://localhost:8080/health
+```
+
+Expected response:
+```json
+{
+ "status": "healthy",
+ "service": "notifier",
+ "time": "2025-10-16T21:05:27Z"
+}
+```
+
+### 2. Send a Simple Notification
+```bash
+curl -X POST http://localhost:8080/api/v1/notifications \
+ -H "Content-Type: application/json" \
+ -d '{
+ "type": "stdout",
+ "subject": "Hello World",
+ "body": "This is a test notification!",
+ "recipients": ["console"]
+ }'
+```
+
+Expected response:
+```json
+{
+ "result": {
+ "notification_id": "abc123...",
+ "success": true,
+ "message": "notification queued successfully",
+ "sent_at": "2025-10-16T21:05:27Z"
+ }
+}
+```
+
+The notification will be printed to the server's stdout.
+
+### 3. Send with Priority
+```bash
+curl -X POST http://localhost:8080/api/v1/notifications \
+ -H "Content-Type: application/json" \
+ -d '{
+ "type": "stdout",
+ "priority": 4,
+ "subject": "CRITICAL Alert",
+ "body": "This is a critical notification!",
+ "recipients": ["console"],
+ "max_retries": 5
+ }'
+```
+
+Priority levels:
+- `0` - Low
+- `1` - Normal (default)
+- `2` - High
+- `3` - Critical
+
+### 4. Send Batch Notifications
+```bash
+curl -X POST http://localhost:8080/api/v1/notifications/batch \
+ -H "Content-Type: application/json" \
+ -d '{
+ "notifications": [
+ {
+ "type": "stdout",
+ "subject": "Notification 1",
+ "body": "First notification",
+ "recipients": ["console"]
+ },
+ {
+ "type": "stdout",
+ "subject": "Notification 2",
+ "body": "Second notification",
+ "recipients": ["console"]
+ }
+ ]
+ }'
+```
+
+### 5. Get Notification Statistics
+```bash
+curl http://localhost:8080/api/v1/stats
+```
+
+Expected response:
+```json
+{
+ "total_sent": 5,
+ "total_failed": 0,
+ "total_pending": 0,
+ "total_queued": 0,
+ "by_type": {
+ "stdout": 5
+ },
+ "by_status": {
+ "sent": 5
+ },
+ "average_latency_ms": 0
+}
+```
+
+### 6. List Notifications
+```bash
+# List all notifications
+curl http://localhost:8080/api/v1/notifications
+
+# List with filters
+curl "http://localhost:8080/api/v1/notifications?type=stdout&status=sent&limit=10"
+```
+
+### 7. Get Specific Notification
+```bash
+curl http://localhost:8080/api/v1/notifications/{notification-id}
+```
+
+## Testing with Other Notifiers
+
+### SMTP (Email)
+Update `config.yaml`:
+```yaml
+notifiers:
+ smtp:
+ host: "smtp.gmail.com"
+ port: 587
+ username: "your-email@gmail.com"
+ password: "your-app-password"
+ from: "notifications@yourservice.com"
+ use_tls: true
+```
+
+Then send:
+```bash
+curl -X POST http://localhost:8080/api/v1/notifications \
+ -H "Content-Type: application/json" \
+ -d '{
+ "type": "email",
+ "subject": "Test Email",
+ "body": "This is a test email!",
+ "recipients": ["recipient@example.com"]
+ }'
+```
+
+### Slack
+Update `config.yaml`:
+```yaml
+notifiers:
+ slack:
+ webhook_url: "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
+ username: "Notifier Bot"
+ icon_emoji: ":bell:"
+```
+
+Then send:
+```bash
+curl -X POST http://localhost:8080/api/v1/notifications \
+ -H "Content-Type: application/json" \
+ -d '{
+ "type": "slack",
+ "subject": "Deployment Alert",
+ "body": "Application deployed successfully to production!",
+ "recipients": ["#alerts"]
+ }'
+```
+
+### Ntfy
+Update `config.yaml`:
+```yaml
+notifiers:
+ ntfy:
+ server_url: "https://ntfy.sh"
+```
+
+Then send:
+```bash
+curl -X POST http://localhost:8080/api/v1/notifications \
+ -H "Content-Type: application/json" \
+ -d '{
+ "type": "ntfy",
+ "subject": "Mobile Alert",
+ "body": "This will appear on your phone!",
+ "recipients": ["mytopic"],
+ "metadata": {
+ "tags": ["warning", "skull"]
+ }
+ }'
+```
+
+## Configuration
+
+### Using Environment Variables
+```bash
+export NOTIFIER_SERVER_REST_PORT=9000
+export NOTIFIER_QUEUE_WORKER_COUNT=20
+export NOTIFIER_NOTIFIERS_SMTP_HOST=smtp.gmail.com
+export NOTIFIER_NOTIFIERS_SMTP_PASSWORD=secret
+
+./bin/restserver
+```
+
+### Using config.yaml
+Create or modify `config.yaml` in the project root:
+```yaml
+server:
+ rest_port: 8080
+ host: "0.0.0.0"
+
+queue:
+ type: "local"
+ worker_count: 10
+
+notifiers:
+ stdout: true
+```
+
+## Docker Deployment
+
+### Build Docker Image
+```bash
+docker build -t notifier:latest .
+```
+
+### Run with Docker
+```bash
+docker run -p 8080:8080 \
+ -v $(pwd)/config.yaml:/app/config.yaml \
+ notifier:latest
+```
+
+### Using Docker Compose
+```bash
+docker-compose up
+```
+
+## Kubernetes Deployment
+
+### Deploy to Kubernetes
+```bash
+# Apply all resources
+kubectl apply -f k8s/
+
+# Check status
+kubectl get pods -l app=notifier
+kubectl get svc -l app=notifier
+
+# View logs
+kubectl logs -f -l app=notifier
+
+# Port forward to access locally
+kubectl port-forward svc/notifier-rest 8080:8080
+```
+
+## Next Steps
+
+1. **Add Authentication**: Implement API key or OAuth authentication
+2. **Add Email Templates**: Support for templated notifications
+3. **Add Webhooks**: Get callbacks when notifications are sent/failed
+4. **Add Persistence**: Store notifications in a database
+5. **Add Kafka Queue**: For distributed processing
+6. **Add Metrics**: Instrument with Prometheus metrics
+7. **Add Tests**: Write unit and integration tests
+
+## Troubleshooting
+
+### Server won't start
+- Check if port 8080 is already in use: `lsof -i :8080`
+- Check config.yaml syntax
+- Verify all dependencies are installed: `go mod tidy`
+
+### Notifications not sending
+- Check server logs for errors
+- Verify the notifier is enabled in config.yaml
+- For SMTP: Verify credentials and allow less secure apps
+- For Slack: Verify webhook URL is correct
+- For Ntfy: Ensure topic name is valid
+
+### Queue filling up
+- Increase worker count in config.yaml
+- Check if notifiers are failing
+- Review retry configuration
+
+## API Reference
+
+Full API documentation available in `ARCHITECTURE.md`.
+
+### Endpoints
+
+| Method | Path | Description |
+|--------|------|-------------|
+| GET | /health | Health check |
+| POST | /api/v1/notifications | Send notification |
+| POST | /api/v1/notifications/batch | Send batch |
+| GET | /api/v1/notifications | List notifications |
+| GET | /api/v1/notifications/{id} | Get notification |
+| DELETE | /api/v1/notifications/{id} | Cancel notification |
+| POST | /api/v1/notifications/{id}/retry | Retry notification |
+| GET | /api/v1/stats | Get statistics |
diff --git a/README.md b/README.md
index f679d23..a042f07 100644
--- a/README.md
+++ b/README.md
@@ -1,153 +1,623 @@
+# Notifier Microservice
-# Notifier Module
+A production-ready notification delivery microservice that supports multiple notification channels (Email, Slack, Ntfy, Stdout) with both REST and gRPC APIs running simultaneously.
-The **Notifier** module provides a flexible notification system that supports multiple notification modes (e.g., SMTP, SMS, Slack, Ntfy, and Stdout). It can operate as a standalone gRPC or REST microservice, or be used as an imported module in other projects.
+[](https://golang.org)
+[](LICENSE)
+
+## Overview
+
+Notifier is a cloud-native microservice designed to handle all your application's notification needs through a single, unified API. Send emails, Slack messages, push notifications, and more with a consistent interface and robust delivery guarantees.
+
+Perfect for:
+- Microservices architectures needing centralized notifications
+- Applications requiring multiple notification channels
+- Systems needing reliable async notification delivery
+- Teams wanting to standardize notification handling
## Features
-- **Multi-Mode Notification**: Supports different notification methods including SMTP email, SMS, Slack, Ntfy, and standard output.
-- **Extensible Design**: Add new notification modes by implementing the `Notifier` interface.
-- **gRPC and REST APIs**: Accessible through both gRPC and RESTful APIs, allowing easy integration into various systems.
-- **Configuration Management**: Easily configurable for different environments and API keys.
+### Core Capabilities
+- 🔔 **Multi-Channel Support**: Email (SMTP), Slack, Ntfy.sh, and Stdout
+- 🚀 **Dual API**: REST and gRPC running simultaneously in one process
+- 📦 **Queue-Based**: Async processing with configurable worker pools
+- 🔄 **Retry Logic**: Exponential backoff with configurable attempts
+- ⚡ **Priority Levels**: Low, Normal, High, and Critical
+- 📊 **Batch Operations**: Send multiple notifications efficiently
+- 🎯 **Status Tracking**: Monitor notification lifecycle and delivery
+
+### Production Features
+- ⚙️ **Configuration**: Viper-based with environment variable support
+- 🐳 **Containerized**: Multi-stage Docker builds with non-root user
+- ☸️ **Kubernetes Ready**: Complete manifests with HPA, health checks, and RBAC
+- 🔒 **Secure**: Token-based auth for ntfy, TLS support, secret management
+- 📈 **Observable**: Health endpoints, metrics support, structured logging
+- 🔌 **Extensible**: Clean interfaces for adding new notifiers
+
+## Quick Start
+
+### 1. Install and Run
+
+```bash
+# Clone and install dependencies
+git clone https://github.com/igodwin/notifier
+cd notifier
+go mod tidy
+
+# Build and run
+go build -o bin/server ./cmd/server
+./bin/server
+```
+
+Server starts with:
+- **REST API** on `http://localhost:8080`
+- **gRPC API** on `localhost:50051`
+- **Stdout notifier** enabled by default
+
+### 2. Send Your First Notification
+
+```bash
+curl -X POST http://localhost:8080/api/v1/notifications \
+ -H "Content-Type: application/json" \
+ -d '{
+ "type": "stdout",
+ "subject": "Hello from Notifier!",
+ "body": "Your first notification",
+ "recipients": ["console"]
+ }'
+```
+
+The notification appears immediately in the server output. ✅
+
+### 3. Check Status
+
+```bash
+# Health check
+curl http://localhost:8080/health
+
+# Statistics
+curl http://localhost:8080/api/v1/stats
+```
+
+## Configuration
+
+### Basic Setup
+
+Create `config.yaml` in the project root:
+
+```yaml
+server:
+ mode: "both" # Options: both, rest, grpc
+ rest_port: 8080
+ grpc_port: 50051
+ host: "0.0.0.0"
+
+queue:
+ type: "local" # Local in-memory queue
+ worker_count: 10 # Concurrent workers
+ retry_attempts: 3
+ retry_backoff: "exponential"
+
+notifiers:
+ stdout: true # Always enabled for testing
+```
+
+### Email Notifications (SMTP)
+
+```yaml
+notifiers:
+ smtp:
+ host: "smtp.gmail.com"
+ port: 587
+ username: "your-email@gmail.com"
+ password: "your-app-password"
+ from: "notifications@yourservice.com"
+ use_tls: true
+```
+
+**Usage:**
+```bash
+curl -X POST http://localhost:8080/api/v1/notifications \
+ -H "Content-Type: application/json" \
+ -d '{
+ "type": "email",
+ "subject": "Welcome!",
+ "body": "Thanks for signing up",
+ "recipients": ["user@example.com"]
+ }'
+```
+
+### Slack Notifications
+
+```yaml
+notifiers:
+ slack:
+ webhook_url: "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
+ username: "Notifier Bot"
+ icon_emoji: ":bell:"
+```
+
+**Usage:**
+```bash
+curl -X POST http://localhost:8080/api/v1/notifications \
+ -H "Content-Type: application/json" \
+ -d '{
+ "type": "slack",
+ "subject": "Deployment Complete",
+ "body": "v2.0 deployed to production",
+ "recipients": ["#alerts"]
+ }'
+```
+
+### Ntfy Push Notifications
+
+```yaml
+notifiers:
+ ntfy:
+ server_url: "https://ntfy.sh"
+ token: "tk_your_access_token" # Optional, for private topics
+ default_topic: "myapp-alerts"
+```
+
+**Usage:**
+```bash
+curl -X POST http://localhost:8080/api/v1/notifications \
+ -H "Content-Type: application/json" \
+ -d '{
+ "type": "ntfy",
+ "priority": 3,
+ "subject": "Critical Alert",
+ "body": "Server CPU at 95%",
+ "recipients": ["alerts"],
+ "metadata": {
+ "tags": ["warning", "rotating_light"],
+ "click": "https://dashboard.example.com"
+ }
+ }'
+```
+
+See [docs/NTFY_GUIDE.md](docs/NTFY_GUIDE.md) for advanced ntfy features (action buttons, attachments, delays, etc.).
+
+### Environment Variables
+
+Override any config with environment variables:
+
+```bash
+export NOTIFIER_SERVER_MODE=both
+export NOTIFIER_SERVER_REST_PORT=8080
+export NOTIFIER_QUEUE_WORKER_COUNT=20
+export NOTIFIER_NOTIFIERS_SMTP_HOST=smtp.gmail.com
+export NOTIFIER_NOTIFIERS_SMTP_PASSWORD=secret
+export NOTIFIER_NOTIFIERS_NTFY_TOKEN=tk_your_token
+```
+
+Format: `NOTIFIER__` (use `_` for nested keys)
+
+## API Reference
+
+### REST Endpoints
+
+| Method | Endpoint | Description |
+|--------|----------|-------------|
+| `GET` | `/health` | Health check |
+| `POST` | `/api/v1/notifications` | Send single notification |
+| `POST` | `/api/v1/notifications/batch` | Send multiple notifications |
+| `GET` | `/api/v1/notifications` | List notifications (with filters) |
+| `GET` | `/api/v1/notifications/{id}` | Get notification by ID |
+| `DELETE` | `/api/v1/notifications/{id}` | Cancel pending notification |
+| `POST` | `/api/v1/notifications/{id}/retry` | Retry failed notification |
+| `GET` | `/api/v1/stats` | Get service statistics |
+
+### Request Format
+
+```json
+{
+ "type": "email|slack|ntfy|stdout",
+ "priority": 0-3,
+ "subject": "Notification title",
+ "body": "Notification body",
+ "recipients": ["email@example.com", "#channel", "topic"],
+ "metadata": {
+ "key": "value"
+ },
+ "max_retries": 3
+}
+```
+
+### Response Format
+
+```json
+{
+ "result": {
+ "notification_id": "uuid",
+ "success": true,
+ "message": "notification queued successfully",
+ "sent_at": "2025-10-16T21:05:27Z"
+ }
+}
+```
+
+### Priority Levels
+
+- `0` - Low (background notifications)
+- `1` - Normal (default)
+- `2` - High (important updates)
+- `3` - Critical (urgent alerts)
+
+### Batch Operations
+
+```bash
+curl -X POST http://localhost:8080/api/v1/notifications/batch \
+ -H "Content-Type: application/json" \
+ -d '{
+ "notifications": [
+ {"type": "email", "subject": "Welcome", ...},
+ {"type": "slack", "subject": "Alert", ...}
+ ]
+ }'
+```
+
+### Filtering Notifications
+
+```bash
+# Get all sent email notifications
+curl "http://localhost:8080/api/v1/notifications?type=email&status=sent&limit=10"
+
+# Get recent failures
+curl "http://localhost:8080/api/v1/notifications?status=failed&limit=20"
+```
+
+## gRPC API
+
+The gRPC service mirrors the REST API with full feature parity. See [api/grpc/notifier.proto](api/grpc/notifier.proto) for definitions.
+
+**Generate Go code:**
+```bash
+make proto-gen
+# or
+protoc --go_out=. --go-grpc_out=. api/grpc/notifier.proto
+```
+
+**Note:** gRPC server is running but handler implementation is pending. Protobuf definitions are complete.
+
+## Deployment
+
+### Docker
+
+**Build:**
+```bash
+docker build -t notifier:latest .
+```
+
+**Run:**
+```bash
+docker run -d \
+ --name notifier \
+ -p 8080:8080 \
+ -p 50051:50051 \
+ -v $(pwd)/config.yaml:/app/config.yaml:ro \
+ notifier:latest
+```
+
+**Docker Compose:**
+```bash
+docker-compose up -d
+```
+
+Includes optional services: Kafka, Prometheus, Grafana (commented out by default)
+
+### Kubernetes
+
+**Deploy:**
+```bash
+kubectl apply -f k8s/
+```
+
+**Included resources:**
+- Deployment (3 replicas with rolling updates)
+- Services (separate for REST and gRPC)
+- ConfigMap (configuration management)
+- HPA (auto-scaling 3-10 pods based on CPU/memory)
+- Ingress (external access with TLS)
+- Secret template (for credentials)
+
+**Access locally:**
+```bash
+kubectl port-forward svc/notifier-rest 8080:8080
+kubectl port-forward svc/notifier-grpc 50051:50051
+```
+
+**Using Kustomize:**
+```bash
+kubectl apply -k k8s/
+```
+
+## Architecture
+
+### High-Level Design
+
+```
+┌──────────────┐ ┌──────────────┐
+│ REST Client │────▶│ REST API │
+└──────────────┘ │ (port 8080) │
+ └──────┬───────┘
+┌──────────────┐ │
+│ gRPC Client │────▶│ gRPC API │
+└──────────────┘ │ (port 50051) │
+ └──────┬───────┘
+ ▼
+ ┌──────────────────┐
+ │ Notification │
+ │ Service │
+ └────────┬─────────┘
+ ▼
+ ┌──────────────────┐
+ │ Queue (Local) │◀──┐
+ └────────┬─────────┘ │
+ ▼ │
+ ┌──────────────────┐ │
+ │ Worker Pool │───┘
+ │ (10 workers) │
+ └────────┬─────────┘
+ ▼
+ ┌─────────────────┼─────────────────┐
+ ▼ ▼ ▼
+ ┌────────┐ ┌──────────┐ ┌──────────┐
+ │ SMTP │ │ Slack │ │ Ntfy │
+ │Notifier│ │ Notifier │ │ Notifier │
+ └────────┘ └──────────┘ └──────────┘
+```
+
+### Key Components
+
+- **API Layer**: REST (Gorilla mux) and gRPC (Protocol Buffers)
+- **Service Layer**: Business logic, validation, orchestration
+- **Queue**: In-memory with optional disk persistence (Kafka planned)
+- **Workers**: Concurrent processors with retry logic
+- **Notifiers**: Pluggable providers implementing `domain.Notifier`
+- **Config**: Viper-based with file + env var support
+
+See [ARCHITECTURE.md](ARCHITECTURE.md) for detailed design documentation.
## Project Structure
-```plaintext
+```
notifier/
├── api/
-│ ├── grpc/ # gRPC service definitions and generated code
-│ └── rest/ # REST API handlers and router
+│ ├── grpc/
+│ │ └── notifier.proto # gRPC service definition
+│ └── rest/
+│ ├── handlers.go # HTTP handlers
+│ ├── router.go # Route configuration
+│ └── types.go # Request/response types
├── cmd/
-│ ├── grpcserver/ # Entrypoint for running the gRPC server
-│ └── restserver/ # Entrypoint for running the REST server
+│ ├── server/main.go # Combined server (recommended)
+│ ├── grpcserver/main.go # gRPC only
+│ └── restserver/main.go # REST only
├── internal/
-│ ├── config/ # Configuration loading
-│ └── notifier/ # Core notification logic with multiple implementations
-├── pkg/
-│ ├── grpcclient/ # gRPC client for interacting with the notifier service
-│ └── restclient/ # REST client for interacting with the notifier service
-├── Dockerfile # Docker configuration for deploying as a microservice
-├── LICENSE # License file
-├── README.md # Documentation
-└── go.mod # Defines the module's dependencies, module path, and Go version
+│ ├── config/
+│ │ └── config.go # Configuration management
+│ ├── domain/
+│ │ ├── notification.go # Core types
+│ │ ├── notifier.go # Notifier interface
+│ │ └── queue.go # Queue interface
+│ ├── notifier/
+│ │ ├── notifier.go # Factory & base
+│ │ ├── smtp.go # Email notifier
+│ │ ├── slack.go # Slack notifier
+│ │ ├── ntfy.go # Ntfy notifier
+│ │ └── stdout.go # Stdout notifier
+│ ├── queue/
+│ │ └── local.go # In-memory queue
+│ └── service/
+│ └── service.go # Business logic
+├── k8s/
+│ ├── deployment.yaml
+│ ├── service.yaml
+│ ├── configmap.yaml
+│ ├── ingress.yaml
+│ ├── hpa.yaml
+│ └── kustomization.yaml
+├── docs/
+│ └── NTFY_GUIDE.md # Ntfy integration guide
+├── config.yaml # Default configuration
+├── docker-compose.yaml
+├── Dockerfile
+├── Makefile
+├── QUICKSTART.md # API examples
+├── ARCHITECTURE.md # Design documentation
+└── README.md # This file
```
-## Getting Started
+## Development
-### Prerequisites
-
-- **Go**: Install Go 1.18 or higher.
-- **Protocol Buffers**: Required if you want to regenerate gRPC code.
-- **Docker** (optional): For containerized deployment.
-
-### Installation
-
-To install the notifier module as a dependency in another Go project, run:
+### Build Commands
```bash
-go get github.com/igodwin/notifier
+make build # Build binaries
+make run-rest # Run REST server
+make run-grpc # Run gRPC server (when implemented)
+make test # Run tests
+make lint # Run linter
+make proto-gen # Generate protobuf code
+make docker-build # Build Docker image
+make clean # Clean artifacts
```
-### Running the Service
+### Adding a New Notifier
-#### gRPC Server
-
-To start the gRPC server:
-
-```bash
-cd cmd/grpcserver
-go run main.go
-```
-
-#### REST Server
-
-To start the REST server:
-
-```bash
-cd cmd/restserver
-go run main.go
-```
-
-### Configuration
-
-Configure notification modes via environment variables or a configuration file (e.g., `config.yaml`). Configuration settings may include:
-
-- **SMTP**: SMTP server details, port, credentials.
-- **SMS**: SMS provider API keys.
-- **Slack**: Slack webhook URLs.
-- **Ntfy**: Ntfy service URL and options.
-
-For example, using a `config.yaml`:
-
-```yaml
-smtp:
- server: "smtp.example.com"
- port: 587
- username: "user@example.com"
- password: "password"
-
-slack:
- webhook_url: "https://hooks.slack.com/services/..."
-```
-
-### Usage
-
-#### Using gRPC API
-
-1. Define a gRPC client using the `pkg/grpcclient` package.
-2. Call `SendNotification` to send a message through a chosen notification method.
+1. Create `internal/notifier/mynotifier.go`
+2. Implement `domain.Notifier` interface
+3. Add config struct to `internal/config/config.go`
+4. Register in `cmd/server/main.go`
+5. Update `config.yaml` with example config
+6. Add tests
Example:
-
```go
-client, err := grpcclient.NewClient("localhost:50051")
-if err != nil {
- log.Fatalf("Failed to create gRPC client: %v", err)
+type MyNotifier struct {
+ BaseNotifier
+ config *MyNotifierConfig
}
-resp, err := client.SendNotification("recipient@example.com", "Hello via gRPC!")
-if err != nil {
- log.Fatalf("Failed to send notification: %v", err)
+func (m *MyNotifier) Send(ctx context.Context, n *domain.Notification) (*domain.NotificationResult, error) {
+ // Implementation
}
```
-#### Using REST API
+See [ARCHITECTURE.md](ARCHITECTURE.md) for detailed guide.
-1. Define a REST client using the `pkg/restclient` package.
-2. Call `SendNotification` via REST.
+## Documentation
-Example:
+- [QUICKSTART.md](QUICKSTART.md) - Quick start guide with examples
+- [ARCHITECTURE.md](ARCHITECTURE.md) - Architecture and design details
+- [docs/NTFY_GUIDE.md](docs/NTFY_GUIDE.md) - Ntfy integration guide
+- [api/grpc/notifier.proto](api/grpc/notifier.proto) - gRPC API definition
-```go
-client := restclient.NewClient("http://localhost:8080")
-err := client.SendNotification("recipient@example.com", "Hello via REST!")
-if err != nil {
- log.Fatalf("Failed to send notification: %v", err)
+## Testing
+
+```bash
+# Run all tests
+go test ./...
+
+# Run with coverage
+go test -cover ./...
+
+# Run specific package
+go test ./internal/notifier/...
+```
+
+**Current Status:** Core implementation complete, tests pending.
+
+## Monitoring & Operations
+
+### Health Checks
+
+```bash
+curl http://localhost:8080/health
+```
+
+Returns:
+```json
+{
+ "status": "healthy",
+ "service": "notifier",
+ "time": "2025-10-16T21:05:27Z"
}
```
-### Implementing New Notification Modes
+### Statistics
-To add a new notification method, implement the `Notifier` interface in `internal/notifier/`:
+```bash
+curl http://localhost:8080/api/v1/stats
+```
-```go
-type Notifier interface {
- Send(notification Notification) error
+Returns:
+```json
+{
+ "total_sent": 1234,
+ "total_failed": 5,
+ "total_pending": 0,
+ "total_queued": 2,
+ "by_type": {
+ "email": 800,
+ "slack": 400,
+ "ntfy": 34
+ },
+ "by_status": {
+ "sent": 1234,
+ "failed": 5
+ }
}
```
-Add the new mode (e.g., `sms.go`) with the `Send` method to implement custom logic.
+### Graceful Shutdown
+
+The server handles `SIGINT` and `SIGTERM` gracefully:
+- Stops accepting new requests
+- Completes in-flight notifications
+- Drains the queue
+- Closes all connections
+- 30-second timeout
+
+## Roadmap
+
+### Completed ✅
+- [x] Core notification system
+- [x] REST API (fully functional)
+- [x] gRPC API (protobuf defined)
+- [x] Local queue with workers
+- [x] Multiple notifiers (SMTP, Slack, Ntfy, Stdout)
+- [x] Priority and retry logic
+- [x] Batch operations
+- [x] Docker support
+- [x] Kubernetes manifests with HPA
+- [x] Health checks and stats
+- [x] Configuration management
+
+### Planned 🚧
+- [ ] gRPC handler implementation
+- [ ] Kafka queue adapter
+- [ ] Database persistence (PostgreSQL)
+- [ ] Notification templates
+- [ ] Webhook callbacks
+- [ ] Authentication/Authorization (API keys, OAuth)
+- [ ] Rate limiting (per client, per notifier)
+- [ ] Prometheus metrics
+- [ ] OpenTelemetry tracing
+- [ ] Comprehensive test suite
+- [ ] Circuit breakers for notifiers
+- [ ] Dead letter queue
+- [ ] Admin dashboard
## Contributing
-Contributions are welcome! To contribute, fork the repository, make your changes, and submit a pull request.
+Contributions are welcome! Please:
-1. Fork the repository.
-2. Create a feature branch (`git checkout -b feature/NewMode`).
-3. Commit your changes (`git commit -am 'Add new notification mode'`).
-4. Push to the branch (`git push origin feature/NewMode`).
-5. Create a new Pull Request.
+1. Fork the repository
+2. Create a feature branch (`git checkout -b feature/amazing-feature`)
+3. Write tests for your changes
+4. Ensure tests pass (`go test ./...`)
+5. Commit your changes (`git commit -m 'Add amazing feature'`)
+6. Push to the branch (`git push origin feature/amazing-feature`)
+7. Open a Pull Request
+
+Please follow Go best practices and maintain test coverage.
+
+## Security
+
+### Reporting Vulnerabilities
+
+Please report security vulnerabilities privately via GitHub Security Advisories.
+
+### Best Practices
+
+- Store credentials in environment variables or secrets managers
+- Use TLS for production deployments
+- Enable authentication for ntfy private topics
+- Rotate tokens regularly
+- Follow principle of least privilege for K8s RBAC
+- Keep dependencies updated
## License
-This project is licensed under the MIT License. See the [LICENSE](LICENSE) file for details.
+This project is licensed under the MIT License - see [LICENSE](LICENSE) for details.
+
+## Support & Community
+
+- **Issues**: [GitHub Issues](https://github.com/igodwin/notifier/issues)
+- **Discussions**: [GitHub Discussions](https://github.com/igodwin/notifier/discussions)
+- **Documentation**: See `docs/` directory
+- **Examples**: See [QUICKSTART.md](QUICKSTART.md)
+
+## Acknowledgments
+
+Built with:
+- [Gorilla Mux](https://github.com/gorilla/mux) - HTTP routing
+- [Viper](https://github.com/spf13/viper) - Configuration
+- [gRPC-Go](https://github.com/grpc/grpc-go) - RPC framework
+- [Ntfy](https://ntfy.sh) - Push notifications
+
+---
+
+**Made with ❤️ for reliable notifications**
diff --git a/api/grpc/notifier.proto b/api/grpc/notifier.proto
index e69de29..4f89f35 100644
--- a/api/grpc/notifier.proto
+++ b/api/grpc/notifier.proto
@@ -0,0 +1,196 @@
+syntax = "proto3";
+
+package notifier.v1;
+
+option go_package = "github.com/igodwin/notifier/api/grpc/pb";
+
+import "google/protobuf/timestamp.proto";
+
+// NotifierService handles notification operations
+service NotifierService {
+ // SendNotification sends a single notification
+ rpc SendNotification(SendNotificationRequest) returns (SendNotificationResponse);
+
+ // SendBatchNotifications sends multiple notifications
+ rpc SendBatchNotifications(SendBatchNotificationsRequest) returns (SendBatchNotificationsResponse);
+
+ // GetNotification retrieves a notification by ID
+ rpc GetNotification(GetNotificationRequest) returns (GetNotificationResponse);
+
+ // ListNotifications retrieves notifications matching a filter
+ rpc ListNotifications(ListNotificationsRequest) returns (ListNotificationsResponse);
+
+ // CancelNotification cancels a pending notification
+ rpc CancelNotification(CancelNotificationRequest) returns (CancelNotificationResponse);
+
+ // RetryNotification retries a failed notification
+ rpc RetryNotification(RetryNotificationRequest) returns (RetryNotificationResponse);
+
+ // GetStats returns notification statistics
+ rpc GetStats(GetStatsRequest) returns (GetStatsResponse);
+
+ // HealthCheck verifies the service is operational
+ rpc HealthCheck(HealthCheckRequest) returns (HealthCheckResponse);
+}
+
+// NotificationType defines the channel for notification delivery
+enum NotificationType {
+ NOTIFICATION_TYPE_UNSPECIFIED = 0;
+ NOTIFICATION_TYPE_EMAIL = 1;
+ NOTIFICATION_TYPE_SLACK = 2;
+ NOTIFICATION_TYPE_NTFY = 3;
+ NOTIFICATION_TYPE_STDOUT = 4;
+}
+
+// Priority defines the urgency level
+enum Priority {
+ PRIORITY_UNSPECIFIED = 0;
+ PRIORITY_LOW = 1;
+ PRIORITY_NORMAL = 2;
+ PRIORITY_HIGH = 3;
+ PRIORITY_CRITICAL = 4;
+}
+
+// NotificationStatus represents the state of a notification
+enum NotificationStatus {
+ NOTIFICATION_STATUS_UNSPECIFIED = 0;
+ NOTIFICATION_STATUS_PENDING = 1;
+ NOTIFICATION_STATUS_QUEUED = 2;
+ NOTIFICATION_STATUS_PROCESSING = 3;
+ NOTIFICATION_STATUS_SENT = 4;
+ NOTIFICATION_STATUS_FAILED = 5;
+ NOTIFICATION_STATUS_RETRYING = 6;
+}
+
+// Notification represents a notification message
+message Notification {
+ string id = 1;
+ NotificationType type = 2;
+ Priority priority = 3;
+ NotificationStatus status = 4;
+ string subject = 5;
+ string body = 6;
+ repeated string recipients = 7;
+ map metadata = 8;
+ google.protobuf.Timestamp created_at = 9;
+ google.protobuf.Timestamp scheduled_for = 10;
+ google.protobuf.Timestamp sent_at = 11;
+ int32 retry_count = 12;
+ int32 max_retries = 13;
+ string last_error = 14;
+}
+
+// NotificationResult represents the outcome of sending a notification
+message NotificationResult {
+ string notification_id = 1;
+ bool success = 2;
+ string message = 3;
+ string error = 4;
+ google.protobuf.Timestamp sent_at = 5;
+ map provider_response = 6;
+}
+
+// SendNotificationRequest sends a single notification
+message SendNotificationRequest {
+ NotificationType type = 1;
+ Priority priority = 2;
+ string subject = 3;
+ string body = 4;
+ repeated string recipients = 5;
+ map metadata = 6;
+ google.protobuf.Timestamp scheduled_for = 7;
+ int32 max_retries = 8;
+}
+
+// SendNotificationResponse returns the result of sending a notification
+message SendNotificationResponse {
+ NotificationResult result = 1;
+}
+
+// SendBatchNotificationsRequest sends multiple notifications
+message SendBatchNotificationsRequest {
+ repeated SendNotificationRequest notifications = 1;
+}
+
+// SendBatchNotificationsResponse returns the results of sending multiple notifications
+message SendBatchNotificationsResponse {
+ repeated NotificationResult results = 1;
+}
+
+// GetNotificationRequest retrieves a notification by ID
+message GetNotificationRequest {
+ string id = 1;
+}
+
+// GetNotificationResponse returns a notification
+message GetNotificationResponse {
+ Notification notification = 1;
+}
+
+// NotificationFilter is used for querying notifications
+message NotificationFilter {
+ repeated string ids = 1;
+ repeated NotificationType types = 2;
+ repeated NotificationStatus statuses = 3;
+ repeated string recipients = 4;
+ google.protobuf.Timestamp created_after = 5;
+ google.protobuf.Timestamp created_before = 6;
+ int32 limit = 7;
+ int32 offset = 8;
+}
+
+// ListNotificationsRequest retrieves notifications matching a filter
+message ListNotificationsRequest {
+ NotificationFilter filter = 1;
+}
+
+// ListNotificationsResponse returns a list of notifications
+message ListNotificationsResponse {
+ repeated Notification notifications = 1;
+ int64 total = 2;
+}
+
+// CancelNotificationRequest cancels a pending notification
+message CancelNotificationRequest {
+ string id = 1;
+}
+
+// CancelNotificationResponse returns the result of canceling a notification
+message CancelNotificationResponse {
+ bool success = 1;
+ string message = 2;
+}
+
+// RetryNotificationRequest retries a failed notification
+message RetryNotificationRequest {
+ string id = 1;
+}
+
+// RetryNotificationResponse returns the result of retrying a notification
+message RetryNotificationResponse {
+ NotificationResult result = 1;
+}
+
+// GetStatsRequest requests notification statistics
+message GetStatsRequest {}
+
+// GetStatsResponse returns notification statistics
+message GetStatsResponse {
+ int64 total_sent = 1;
+ int64 total_failed = 2;
+ int64 total_pending = 3;
+ int64 total_queued = 4;
+ map by_type = 5;
+ map by_status = 6;
+ double average_latency_ms = 7;
+}
+
+// HealthCheckRequest requests health status
+message HealthCheckRequest {}
+
+// HealthCheckResponse returns health status
+message HealthCheckResponse {
+ bool healthy = 1;
+ string status = 2;
+ map components = 3;
+}
diff --git a/api/grpc/pb/api/grpc/notifier.pb.go b/api/grpc/pb/api/grpc/notifier.pb.go
new file mode 100644
index 0000000..7ca86da
--- /dev/null
+++ b/api/grpc/pb/api/grpc/notifier.pb.go
@@ -0,0 +1,1639 @@
+// Code generated by protoc-gen-go. DO NOT EDIT.
+// versions:
+// protoc-gen-go v1.36.10
+// protoc v6.33.0
+// source: api/grpc/notifier.proto
+
+package pb
+
+import (
+ protoreflect "google.golang.org/protobuf/reflect/protoreflect"
+ protoimpl "google.golang.org/protobuf/runtime/protoimpl"
+ timestamppb "google.golang.org/protobuf/types/known/timestamppb"
+ reflect "reflect"
+ sync "sync"
+ unsafe "unsafe"
+)
+
+const (
+ // Verify that this generated code is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
+ // Verify that runtime/protoimpl is sufficiently up-to-date.
+ _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
+)
+
+// NotificationType defines the channel for notification delivery
+type NotificationType int32
+
+const (
+ NotificationType_NOTIFICATION_TYPE_UNSPECIFIED NotificationType = 0
+ NotificationType_NOTIFICATION_TYPE_EMAIL NotificationType = 1
+ NotificationType_NOTIFICATION_TYPE_SLACK NotificationType = 2
+ NotificationType_NOTIFICATION_TYPE_NTFY NotificationType = 3
+ NotificationType_NOTIFICATION_TYPE_STDOUT NotificationType = 4
+)
+
+// Enum value maps for NotificationType.
+var (
+ NotificationType_name = map[int32]string{
+ 0: "NOTIFICATION_TYPE_UNSPECIFIED",
+ 1: "NOTIFICATION_TYPE_EMAIL",
+ 2: "NOTIFICATION_TYPE_SLACK",
+ 3: "NOTIFICATION_TYPE_NTFY",
+ 4: "NOTIFICATION_TYPE_STDOUT",
+ }
+ NotificationType_value = map[string]int32{
+ "NOTIFICATION_TYPE_UNSPECIFIED": 0,
+ "NOTIFICATION_TYPE_EMAIL": 1,
+ "NOTIFICATION_TYPE_SLACK": 2,
+ "NOTIFICATION_TYPE_NTFY": 3,
+ "NOTIFICATION_TYPE_STDOUT": 4,
+ }
+)
+
+func (x NotificationType) Enum() *NotificationType {
+ p := new(NotificationType)
+ *p = x
+ return p
+}
+
+func (x NotificationType) String() string {
+ return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (NotificationType) Descriptor() protoreflect.EnumDescriptor {
+ return file_api_grpc_notifier_proto_enumTypes[0].Descriptor()
+}
+
+func (NotificationType) Type() protoreflect.EnumType {
+ return &file_api_grpc_notifier_proto_enumTypes[0]
+}
+
+func (x NotificationType) Number() protoreflect.EnumNumber {
+ return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use NotificationType.Descriptor instead.
+func (NotificationType) EnumDescriptor() ([]byte, []int) {
+ return file_api_grpc_notifier_proto_rawDescGZIP(), []int{0}
+}
+
+// Priority defines the urgency level
+type Priority int32
+
+const (
+ Priority_PRIORITY_UNSPECIFIED Priority = 0
+ Priority_PRIORITY_LOW Priority = 1
+ Priority_PRIORITY_NORMAL Priority = 2
+ Priority_PRIORITY_HIGH Priority = 3
+ Priority_PRIORITY_CRITICAL Priority = 4
+)
+
+// Enum value maps for Priority.
+var (
+ Priority_name = map[int32]string{
+ 0: "PRIORITY_UNSPECIFIED",
+ 1: "PRIORITY_LOW",
+ 2: "PRIORITY_NORMAL",
+ 3: "PRIORITY_HIGH",
+ 4: "PRIORITY_CRITICAL",
+ }
+ Priority_value = map[string]int32{
+ "PRIORITY_UNSPECIFIED": 0,
+ "PRIORITY_LOW": 1,
+ "PRIORITY_NORMAL": 2,
+ "PRIORITY_HIGH": 3,
+ "PRIORITY_CRITICAL": 4,
+ }
+)
+
+func (x Priority) Enum() *Priority {
+ p := new(Priority)
+ *p = x
+ return p
+}
+
+func (x Priority) String() string {
+ return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (Priority) Descriptor() protoreflect.EnumDescriptor {
+ return file_api_grpc_notifier_proto_enumTypes[1].Descriptor()
+}
+
+func (Priority) Type() protoreflect.EnumType {
+ return &file_api_grpc_notifier_proto_enumTypes[1]
+}
+
+func (x Priority) Number() protoreflect.EnumNumber {
+ return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use Priority.Descriptor instead.
+func (Priority) EnumDescriptor() ([]byte, []int) {
+ return file_api_grpc_notifier_proto_rawDescGZIP(), []int{1}
+}
+
+// NotificationStatus represents the state of a notification
+type NotificationStatus int32
+
+const (
+ NotificationStatus_NOTIFICATION_STATUS_UNSPECIFIED NotificationStatus = 0
+ NotificationStatus_NOTIFICATION_STATUS_PENDING NotificationStatus = 1
+ NotificationStatus_NOTIFICATION_STATUS_QUEUED NotificationStatus = 2
+ NotificationStatus_NOTIFICATION_STATUS_PROCESSING NotificationStatus = 3
+ NotificationStatus_NOTIFICATION_STATUS_SENT NotificationStatus = 4
+ NotificationStatus_NOTIFICATION_STATUS_FAILED NotificationStatus = 5
+ NotificationStatus_NOTIFICATION_STATUS_RETRYING NotificationStatus = 6
+)
+
+// Enum value maps for NotificationStatus.
+var (
+ NotificationStatus_name = map[int32]string{
+ 0: "NOTIFICATION_STATUS_UNSPECIFIED",
+ 1: "NOTIFICATION_STATUS_PENDING",
+ 2: "NOTIFICATION_STATUS_QUEUED",
+ 3: "NOTIFICATION_STATUS_PROCESSING",
+ 4: "NOTIFICATION_STATUS_SENT",
+ 5: "NOTIFICATION_STATUS_FAILED",
+ 6: "NOTIFICATION_STATUS_RETRYING",
+ }
+ NotificationStatus_value = map[string]int32{
+ "NOTIFICATION_STATUS_UNSPECIFIED": 0,
+ "NOTIFICATION_STATUS_PENDING": 1,
+ "NOTIFICATION_STATUS_QUEUED": 2,
+ "NOTIFICATION_STATUS_PROCESSING": 3,
+ "NOTIFICATION_STATUS_SENT": 4,
+ "NOTIFICATION_STATUS_FAILED": 5,
+ "NOTIFICATION_STATUS_RETRYING": 6,
+ }
+)
+
+func (x NotificationStatus) Enum() *NotificationStatus {
+ p := new(NotificationStatus)
+ *p = x
+ return p
+}
+
+func (x NotificationStatus) String() string {
+ return protoimpl.X.EnumStringOf(x.Descriptor(), protoreflect.EnumNumber(x))
+}
+
+func (NotificationStatus) Descriptor() protoreflect.EnumDescriptor {
+ return file_api_grpc_notifier_proto_enumTypes[2].Descriptor()
+}
+
+func (NotificationStatus) Type() protoreflect.EnumType {
+ return &file_api_grpc_notifier_proto_enumTypes[2]
+}
+
+func (x NotificationStatus) Number() protoreflect.EnumNumber {
+ return protoreflect.EnumNumber(x)
+}
+
+// Deprecated: Use NotificationStatus.Descriptor instead.
+func (NotificationStatus) EnumDescriptor() ([]byte, []int) {
+ return file_api_grpc_notifier_proto_rawDescGZIP(), []int{2}
+}
+
+// Notification represents a notification message
+type Notification struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
+ Type NotificationType `protobuf:"varint,2,opt,name=type,proto3,enum=notifier.v1.NotificationType" json:"type,omitempty"`
+ Priority Priority `protobuf:"varint,3,opt,name=priority,proto3,enum=notifier.v1.Priority" json:"priority,omitempty"`
+ Status NotificationStatus `protobuf:"varint,4,opt,name=status,proto3,enum=notifier.v1.NotificationStatus" json:"status,omitempty"`
+ Subject string `protobuf:"bytes,5,opt,name=subject,proto3" json:"subject,omitempty"`
+ Body string `protobuf:"bytes,6,opt,name=body,proto3" json:"body,omitempty"`
+ Recipients []string `protobuf:"bytes,7,rep,name=recipients,proto3" json:"recipients,omitempty"`
+ Metadata map[string]string `protobuf:"bytes,8,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
+ CreatedAt *timestamppb.Timestamp `protobuf:"bytes,9,opt,name=created_at,json=createdAt,proto3" json:"created_at,omitempty"`
+ ScheduledFor *timestamppb.Timestamp `protobuf:"bytes,10,opt,name=scheduled_for,json=scheduledFor,proto3" json:"scheduled_for,omitempty"`
+ SentAt *timestamppb.Timestamp `protobuf:"bytes,11,opt,name=sent_at,json=sentAt,proto3" json:"sent_at,omitempty"`
+ RetryCount int32 `protobuf:"varint,12,opt,name=retry_count,json=retryCount,proto3" json:"retry_count,omitempty"`
+ MaxRetries int32 `protobuf:"varint,13,opt,name=max_retries,json=maxRetries,proto3" json:"max_retries,omitempty"`
+ LastError string `protobuf:"bytes,14,opt,name=last_error,json=lastError,proto3" json:"last_error,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *Notification) Reset() {
+ *x = Notification{}
+ mi := &file_api_grpc_notifier_proto_msgTypes[0]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *Notification) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*Notification) ProtoMessage() {}
+
+func (x *Notification) ProtoReflect() protoreflect.Message {
+ mi := &file_api_grpc_notifier_proto_msgTypes[0]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use Notification.ProtoReflect.Descriptor instead.
+func (*Notification) Descriptor() ([]byte, []int) {
+ return file_api_grpc_notifier_proto_rawDescGZIP(), []int{0}
+}
+
+func (x *Notification) GetId() string {
+ if x != nil {
+ return x.Id
+ }
+ return ""
+}
+
+func (x *Notification) GetType() NotificationType {
+ if x != nil {
+ return x.Type
+ }
+ return NotificationType_NOTIFICATION_TYPE_UNSPECIFIED
+}
+
+func (x *Notification) GetPriority() Priority {
+ if x != nil {
+ return x.Priority
+ }
+ return Priority_PRIORITY_UNSPECIFIED
+}
+
+func (x *Notification) GetStatus() NotificationStatus {
+ if x != nil {
+ return x.Status
+ }
+ return NotificationStatus_NOTIFICATION_STATUS_UNSPECIFIED
+}
+
+func (x *Notification) GetSubject() string {
+ if x != nil {
+ return x.Subject
+ }
+ return ""
+}
+
+func (x *Notification) GetBody() string {
+ if x != nil {
+ return x.Body
+ }
+ return ""
+}
+
+func (x *Notification) GetRecipients() []string {
+ if x != nil {
+ return x.Recipients
+ }
+ return nil
+}
+
+func (x *Notification) GetMetadata() map[string]string {
+ if x != nil {
+ return x.Metadata
+ }
+ return nil
+}
+
+func (x *Notification) GetCreatedAt() *timestamppb.Timestamp {
+ if x != nil {
+ return x.CreatedAt
+ }
+ return nil
+}
+
+func (x *Notification) GetScheduledFor() *timestamppb.Timestamp {
+ if x != nil {
+ return x.ScheduledFor
+ }
+ return nil
+}
+
+func (x *Notification) GetSentAt() *timestamppb.Timestamp {
+ if x != nil {
+ return x.SentAt
+ }
+ return nil
+}
+
+func (x *Notification) GetRetryCount() int32 {
+ if x != nil {
+ return x.RetryCount
+ }
+ return 0
+}
+
+func (x *Notification) GetMaxRetries() int32 {
+ if x != nil {
+ return x.MaxRetries
+ }
+ return 0
+}
+
+func (x *Notification) GetLastError() string {
+ if x != nil {
+ return x.LastError
+ }
+ return ""
+}
+
+// NotificationResult represents the outcome of sending a notification
+type NotificationResult struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ NotificationId string `protobuf:"bytes,1,opt,name=notification_id,json=notificationId,proto3" json:"notification_id,omitempty"`
+ Success bool `protobuf:"varint,2,opt,name=success,proto3" json:"success,omitempty"`
+ Message string `protobuf:"bytes,3,opt,name=message,proto3" json:"message,omitempty"`
+ Error string `protobuf:"bytes,4,opt,name=error,proto3" json:"error,omitempty"`
+ SentAt *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=sent_at,json=sentAt,proto3" json:"sent_at,omitempty"`
+ ProviderResponse map[string]string `protobuf:"bytes,6,rep,name=provider_response,json=providerResponse,proto3" json:"provider_response,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *NotificationResult) Reset() {
+ *x = NotificationResult{}
+ mi := &file_api_grpc_notifier_proto_msgTypes[1]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *NotificationResult) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*NotificationResult) ProtoMessage() {}
+
+func (x *NotificationResult) ProtoReflect() protoreflect.Message {
+ mi := &file_api_grpc_notifier_proto_msgTypes[1]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use NotificationResult.ProtoReflect.Descriptor instead.
+func (*NotificationResult) Descriptor() ([]byte, []int) {
+ return file_api_grpc_notifier_proto_rawDescGZIP(), []int{1}
+}
+
+func (x *NotificationResult) GetNotificationId() string {
+ if x != nil {
+ return x.NotificationId
+ }
+ return ""
+}
+
+func (x *NotificationResult) GetSuccess() bool {
+ if x != nil {
+ return x.Success
+ }
+ return false
+}
+
+func (x *NotificationResult) GetMessage() string {
+ if x != nil {
+ return x.Message
+ }
+ return ""
+}
+
+func (x *NotificationResult) GetError() string {
+ if x != nil {
+ return x.Error
+ }
+ return ""
+}
+
+func (x *NotificationResult) GetSentAt() *timestamppb.Timestamp {
+ if x != nil {
+ return x.SentAt
+ }
+ return nil
+}
+
+func (x *NotificationResult) GetProviderResponse() map[string]string {
+ if x != nil {
+ return x.ProviderResponse
+ }
+ return nil
+}
+
+// SendNotificationRequest sends a single notification
+type SendNotificationRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Type NotificationType `protobuf:"varint,1,opt,name=type,proto3,enum=notifier.v1.NotificationType" json:"type,omitempty"`
+ Priority Priority `protobuf:"varint,2,opt,name=priority,proto3,enum=notifier.v1.Priority" json:"priority,omitempty"`
+ Subject string `protobuf:"bytes,3,opt,name=subject,proto3" json:"subject,omitempty"`
+ Body string `protobuf:"bytes,4,opt,name=body,proto3" json:"body,omitempty"`
+ Recipients []string `protobuf:"bytes,5,rep,name=recipients,proto3" json:"recipients,omitempty"`
+ Metadata map[string]string `protobuf:"bytes,6,rep,name=metadata,proto3" json:"metadata,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
+ ScheduledFor *timestamppb.Timestamp `protobuf:"bytes,7,opt,name=scheduled_for,json=scheduledFor,proto3" json:"scheduled_for,omitempty"`
+ MaxRetries int32 `protobuf:"varint,8,opt,name=max_retries,json=maxRetries,proto3" json:"max_retries,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *SendNotificationRequest) Reset() {
+ *x = SendNotificationRequest{}
+ mi := &file_api_grpc_notifier_proto_msgTypes[2]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *SendNotificationRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SendNotificationRequest) ProtoMessage() {}
+
+func (x *SendNotificationRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_api_grpc_notifier_proto_msgTypes[2]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use SendNotificationRequest.ProtoReflect.Descriptor instead.
+func (*SendNotificationRequest) Descriptor() ([]byte, []int) {
+ return file_api_grpc_notifier_proto_rawDescGZIP(), []int{2}
+}
+
+func (x *SendNotificationRequest) GetType() NotificationType {
+ if x != nil {
+ return x.Type
+ }
+ return NotificationType_NOTIFICATION_TYPE_UNSPECIFIED
+}
+
+func (x *SendNotificationRequest) GetPriority() Priority {
+ if x != nil {
+ return x.Priority
+ }
+ return Priority_PRIORITY_UNSPECIFIED
+}
+
+func (x *SendNotificationRequest) GetSubject() string {
+ if x != nil {
+ return x.Subject
+ }
+ return ""
+}
+
+func (x *SendNotificationRequest) GetBody() string {
+ if x != nil {
+ return x.Body
+ }
+ return ""
+}
+
+func (x *SendNotificationRequest) GetRecipients() []string {
+ if x != nil {
+ return x.Recipients
+ }
+ return nil
+}
+
+func (x *SendNotificationRequest) GetMetadata() map[string]string {
+ if x != nil {
+ return x.Metadata
+ }
+ return nil
+}
+
+func (x *SendNotificationRequest) GetScheduledFor() *timestamppb.Timestamp {
+ if x != nil {
+ return x.ScheduledFor
+ }
+ return nil
+}
+
+func (x *SendNotificationRequest) GetMaxRetries() int32 {
+ if x != nil {
+ return x.MaxRetries
+ }
+ return 0
+}
+
+// SendNotificationResponse returns the result of sending a notification
+type SendNotificationResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Result *NotificationResult `protobuf:"bytes,1,opt,name=result,proto3" json:"result,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *SendNotificationResponse) Reset() {
+ *x = SendNotificationResponse{}
+ mi := &file_api_grpc_notifier_proto_msgTypes[3]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *SendNotificationResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SendNotificationResponse) ProtoMessage() {}
+
+func (x *SendNotificationResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_api_grpc_notifier_proto_msgTypes[3]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use SendNotificationResponse.ProtoReflect.Descriptor instead.
+func (*SendNotificationResponse) Descriptor() ([]byte, []int) {
+ return file_api_grpc_notifier_proto_rawDescGZIP(), []int{3}
+}
+
+func (x *SendNotificationResponse) GetResult() *NotificationResult {
+ if x != nil {
+ return x.Result
+ }
+ return nil
+}
+
+// SendBatchNotificationsRequest sends multiple notifications
+type SendBatchNotificationsRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Notifications []*SendNotificationRequest `protobuf:"bytes,1,rep,name=notifications,proto3" json:"notifications,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *SendBatchNotificationsRequest) Reset() {
+ *x = SendBatchNotificationsRequest{}
+ mi := &file_api_grpc_notifier_proto_msgTypes[4]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *SendBatchNotificationsRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SendBatchNotificationsRequest) ProtoMessage() {}
+
+func (x *SendBatchNotificationsRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_api_grpc_notifier_proto_msgTypes[4]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use SendBatchNotificationsRequest.ProtoReflect.Descriptor instead.
+func (*SendBatchNotificationsRequest) Descriptor() ([]byte, []int) {
+ return file_api_grpc_notifier_proto_rawDescGZIP(), []int{4}
+}
+
+func (x *SendBatchNotificationsRequest) GetNotifications() []*SendNotificationRequest {
+ if x != nil {
+ return x.Notifications
+ }
+ return nil
+}
+
+// SendBatchNotificationsResponse returns the results of sending multiple notifications
+type SendBatchNotificationsResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Results []*NotificationResult `protobuf:"bytes,1,rep,name=results,proto3" json:"results,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *SendBatchNotificationsResponse) Reset() {
+ *x = SendBatchNotificationsResponse{}
+ mi := &file_api_grpc_notifier_proto_msgTypes[5]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *SendBatchNotificationsResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*SendBatchNotificationsResponse) ProtoMessage() {}
+
+func (x *SendBatchNotificationsResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_api_grpc_notifier_proto_msgTypes[5]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use SendBatchNotificationsResponse.ProtoReflect.Descriptor instead.
+func (*SendBatchNotificationsResponse) Descriptor() ([]byte, []int) {
+ return file_api_grpc_notifier_proto_rawDescGZIP(), []int{5}
+}
+
+func (x *SendBatchNotificationsResponse) GetResults() []*NotificationResult {
+ if x != nil {
+ return x.Results
+ }
+ return nil
+}
+
+// GetNotificationRequest retrieves a notification by ID
+type GetNotificationRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *GetNotificationRequest) Reset() {
+ *x = GetNotificationRequest{}
+ mi := &file_api_grpc_notifier_proto_msgTypes[6]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *GetNotificationRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetNotificationRequest) ProtoMessage() {}
+
+func (x *GetNotificationRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_api_grpc_notifier_proto_msgTypes[6]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetNotificationRequest.ProtoReflect.Descriptor instead.
+func (*GetNotificationRequest) Descriptor() ([]byte, []int) {
+ return file_api_grpc_notifier_proto_rawDescGZIP(), []int{6}
+}
+
+func (x *GetNotificationRequest) GetId() string {
+ if x != nil {
+ return x.Id
+ }
+ return ""
+}
+
+// GetNotificationResponse returns a notification
+type GetNotificationResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Notification *Notification `protobuf:"bytes,1,opt,name=notification,proto3" json:"notification,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *GetNotificationResponse) Reset() {
+ *x = GetNotificationResponse{}
+ mi := &file_api_grpc_notifier_proto_msgTypes[7]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *GetNotificationResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetNotificationResponse) ProtoMessage() {}
+
+func (x *GetNotificationResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_api_grpc_notifier_proto_msgTypes[7]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetNotificationResponse.ProtoReflect.Descriptor instead.
+func (*GetNotificationResponse) Descriptor() ([]byte, []int) {
+ return file_api_grpc_notifier_proto_rawDescGZIP(), []int{7}
+}
+
+func (x *GetNotificationResponse) GetNotification() *Notification {
+ if x != nil {
+ return x.Notification
+ }
+ return nil
+}
+
+// NotificationFilter is used for querying notifications
+type NotificationFilter struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Ids []string `protobuf:"bytes,1,rep,name=ids,proto3" json:"ids,omitempty"`
+ Types []NotificationType `protobuf:"varint,2,rep,packed,name=types,proto3,enum=notifier.v1.NotificationType" json:"types,omitempty"`
+ Statuses []NotificationStatus `protobuf:"varint,3,rep,packed,name=statuses,proto3,enum=notifier.v1.NotificationStatus" json:"statuses,omitempty"`
+ Recipients []string `protobuf:"bytes,4,rep,name=recipients,proto3" json:"recipients,omitempty"`
+ CreatedAfter *timestamppb.Timestamp `protobuf:"bytes,5,opt,name=created_after,json=createdAfter,proto3" json:"created_after,omitempty"`
+ CreatedBefore *timestamppb.Timestamp `protobuf:"bytes,6,opt,name=created_before,json=createdBefore,proto3" json:"created_before,omitempty"`
+ Limit int32 `protobuf:"varint,7,opt,name=limit,proto3" json:"limit,omitempty"`
+ Offset int32 `protobuf:"varint,8,opt,name=offset,proto3" json:"offset,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *NotificationFilter) Reset() {
+ *x = NotificationFilter{}
+ mi := &file_api_grpc_notifier_proto_msgTypes[8]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *NotificationFilter) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*NotificationFilter) ProtoMessage() {}
+
+func (x *NotificationFilter) ProtoReflect() protoreflect.Message {
+ mi := &file_api_grpc_notifier_proto_msgTypes[8]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use NotificationFilter.ProtoReflect.Descriptor instead.
+func (*NotificationFilter) Descriptor() ([]byte, []int) {
+ return file_api_grpc_notifier_proto_rawDescGZIP(), []int{8}
+}
+
+func (x *NotificationFilter) GetIds() []string {
+ if x != nil {
+ return x.Ids
+ }
+ return nil
+}
+
+func (x *NotificationFilter) GetTypes() []NotificationType {
+ if x != nil {
+ return x.Types
+ }
+ return nil
+}
+
+func (x *NotificationFilter) GetStatuses() []NotificationStatus {
+ if x != nil {
+ return x.Statuses
+ }
+ return nil
+}
+
+func (x *NotificationFilter) GetRecipients() []string {
+ if x != nil {
+ return x.Recipients
+ }
+ return nil
+}
+
+func (x *NotificationFilter) GetCreatedAfter() *timestamppb.Timestamp {
+ if x != nil {
+ return x.CreatedAfter
+ }
+ return nil
+}
+
+func (x *NotificationFilter) GetCreatedBefore() *timestamppb.Timestamp {
+ if x != nil {
+ return x.CreatedBefore
+ }
+ return nil
+}
+
+func (x *NotificationFilter) GetLimit() int32 {
+ if x != nil {
+ return x.Limit
+ }
+ return 0
+}
+
+func (x *NotificationFilter) GetOffset() int32 {
+ if x != nil {
+ return x.Offset
+ }
+ return 0
+}
+
+// ListNotificationsRequest retrieves notifications matching a filter
+type ListNotificationsRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Filter *NotificationFilter `protobuf:"bytes,1,opt,name=filter,proto3" json:"filter,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *ListNotificationsRequest) Reset() {
+ *x = ListNotificationsRequest{}
+ mi := &file_api_grpc_notifier_proto_msgTypes[9]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ListNotificationsRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListNotificationsRequest) ProtoMessage() {}
+
+func (x *ListNotificationsRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_api_grpc_notifier_proto_msgTypes[9]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListNotificationsRequest.ProtoReflect.Descriptor instead.
+func (*ListNotificationsRequest) Descriptor() ([]byte, []int) {
+ return file_api_grpc_notifier_proto_rawDescGZIP(), []int{9}
+}
+
+func (x *ListNotificationsRequest) GetFilter() *NotificationFilter {
+ if x != nil {
+ return x.Filter
+ }
+ return nil
+}
+
+// ListNotificationsResponse returns a list of notifications
+type ListNotificationsResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Notifications []*Notification `protobuf:"bytes,1,rep,name=notifications,proto3" json:"notifications,omitempty"`
+ Total int64 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *ListNotificationsResponse) Reset() {
+ *x = ListNotificationsResponse{}
+ mi := &file_api_grpc_notifier_proto_msgTypes[10]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *ListNotificationsResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*ListNotificationsResponse) ProtoMessage() {}
+
+func (x *ListNotificationsResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_api_grpc_notifier_proto_msgTypes[10]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use ListNotificationsResponse.ProtoReflect.Descriptor instead.
+func (*ListNotificationsResponse) Descriptor() ([]byte, []int) {
+ return file_api_grpc_notifier_proto_rawDescGZIP(), []int{10}
+}
+
+func (x *ListNotificationsResponse) GetNotifications() []*Notification {
+ if x != nil {
+ return x.Notifications
+ }
+ return nil
+}
+
+func (x *ListNotificationsResponse) GetTotal() int64 {
+ if x != nil {
+ return x.Total
+ }
+ return 0
+}
+
+// CancelNotificationRequest cancels a pending notification
+type CancelNotificationRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *CancelNotificationRequest) Reset() {
+ *x = CancelNotificationRequest{}
+ mi := &file_api_grpc_notifier_proto_msgTypes[11]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *CancelNotificationRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CancelNotificationRequest) ProtoMessage() {}
+
+func (x *CancelNotificationRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_api_grpc_notifier_proto_msgTypes[11]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CancelNotificationRequest.ProtoReflect.Descriptor instead.
+func (*CancelNotificationRequest) Descriptor() ([]byte, []int) {
+ return file_api_grpc_notifier_proto_rawDescGZIP(), []int{11}
+}
+
+func (x *CancelNotificationRequest) GetId() string {
+ if x != nil {
+ return x.Id
+ }
+ return ""
+}
+
+// CancelNotificationResponse returns the result of canceling a notification
+type CancelNotificationResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Success bool `protobuf:"varint,1,opt,name=success,proto3" json:"success,omitempty"`
+ Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *CancelNotificationResponse) Reset() {
+ *x = CancelNotificationResponse{}
+ mi := &file_api_grpc_notifier_proto_msgTypes[12]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *CancelNotificationResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*CancelNotificationResponse) ProtoMessage() {}
+
+func (x *CancelNotificationResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_api_grpc_notifier_proto_msgTypes[12]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use CancelNotificationResponse.ProtoReflect.Descriptor instead.
+func (*CancelNotificationResponse) Descriptor() ([]byte, []int) {
+ return file_api_grpc_notifier_proto_rawDescGZIP(), []int{12}
+}
+
+func (x *CancelNotificationResponse) GetSuccess() bool {
+ if x != nil {
+ return x.Success
+ }
+ return false
+}
+
+func (x *CancelNotificationResponse) GetMessage() string {
+ if x != nil {
+ return x.Message
+ }
+ return ""
+}
+
+// RetryNotificationRequest retries a failed notification
+type RetryNotificationRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Id string `protobuf:"bytes,1,opt,name=id,proto3" json:"id,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *RetryNotificationRequest) Reset() {
+ *x = RetryNotificationRequest{}
+ mi := &file_api_grpc_notifier_proto_msgTypes[13]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *RetryNotificationRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RetryNotificationRequest) ProtoMessage() {}
+
+func (x *RetryNotificationRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_api_grpc_notifier_proto_msgTypes[13]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use RetryNotificationRequest.ProtoReflect.Descriptor instead.
+func (*RetryNotificationRequest) Descriptor() ([]byte, []int) {
+ return file_api_grpc_notifier_proto_rawDescGZIP(), []int{13}
+}
+
+func (x *RetryNotificationRequest) GetId() string {
+ if x != nil {
+ return x.Id
+ }
+ return ""
+}
+
+// RetryNotificationResponse returns the result of retrying a notification
+type RetryNotificationResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Result *NotificationResult `protobuf:"bytes,1,opt,name=result,proto3" json:"result,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *RetryNotificationResponse) Reset() {
+ *x = RetryNotificationResponse{}
+ mi := &file_api_grpc_notifier_proto_msgTypes[14]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *RetryNotificationResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*RetryNotificationResponse) ProtoMessage() {}
+
+func (x *RetryNotificationResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_api_grpc_notifier_proto_msgTypes[14]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use RetryNotificationResponse.ProtoReflect.Descriptor instead.
+func (*RetryNotificationResponse) Descriptor() ([]byte, []int) {
+ return file_api_grpc_notifier_proto_rawDescGZIP(), []int{14}
+}
+
+func (x *RetryNotificationResponse) GetResult() *NotificationResult {
+ if x != nil {
+ return x.Result
+ }
+ return nil
+}
+
+// GetStatsRequest requests notification statistics
+type GetStatsRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *GetStatsRequest) Reset() {
+ *x = GetStatsRequest{}
+ mi := &file_api_grpc_notifier_proto_msgTypes[15]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *GetStatsRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetStatsRequest) ProtoMessage() {}
+
+func (x *GetStatsRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_api_grpc_notifier_proto_msgTypes[15]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetStatsRequest.ProtoReflect.Descriptor instead.
+func (*GetStatsRequest) Descriptor() ([]byte, []int) {
+ return file_api_grpc_notifier_proto_rawDescGZIP(), []int{15}
+}
+
+// GetStatsResponse returns notification statistics
+type GetStatsResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ TotalSent int64 `protobuf:"varint,1,opt,name=total_sent,json=totalSent,proto3" json:"total_sent,omitempty"`
+ TotalFailed int64 `protobuf:"varint,2,opt,name=total_failed,json=totalFailed,proto3" json:"total_failed,omitempty"`
+ TotalPending int64 `protobuf:"varint,3,opt,name=total_pending,json=totalPending,proto3" json:"total_pending,omitempty"`
+ TotalQueued int64 `protobuf:"varint,4,opt,name=total_queued,json=totalQueued,proto3" json:"total_queued,omitempty"`
+ ByType map[string]int64 `protobuf:"bytes,5,rep,name=by_type,json=byType,proto3" json:"by_type,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"varint,2,opt,name=value"`
+ ByStatus map[string]int64 `protobuf:"bytes,6,rep,name=by_status,json=byStatus,proto3" json:"by_status,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"varint,2,opt,name=value"`
+ AverageLatencyMs float64 `protobuf:"fixed64,7,opt,name=average_latency_ms,json=averageLatencyMs,proto3" json:"average_latency_ms,omitempty"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *GetStatsResponse) Reset() {
+ *x = GetStatsResponse{}
+ mi := &file_api_grpc_notifier_proto_msgTypes[16]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *GetStatsResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*GetStatsResponse) ProtoMessage() {}
+
+func (x *GetStatsResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_api_grpc_notifier_proto_msgTypes[16]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use GetStatsResponse.ProtoReflect.Descriptor instead.
+func (*GetStatsResponse) Descriptor() ([]byte, []int) {
+ return file_api_grpc_notifier_proto_rawDescGZIP(), []int{16}
+}
+
+func (x *GetStatsResponse) GetTotalSent() int64 {
+ if x != nil {
+ return x.TotalSent
+ }
+ return 0
+}
+
+func (x *GetStatsResponse) GetTotalFailed() int64 {
+ if x != nil {
+ return x.TotalFailed
+ }
+ return 0
+}
+
+func (x *GetStatsResponse) GetTotalPending() int64 {
+ if x != nil {
+ return x.TotalPending
+ }
+ return 0
+}
+
+func (x *GetStatsResponse) GetTotalQueued() int64 {
+ if x != nil {
+ return x.TotalQueued
+ }
+ return 0
+}
+
+func (x *GetStatsResponse) GetByType() map[string]int64 {
+ if x != nil {
+ return x.ByType
+ }
+ return nil
+}
+
+func (x *GetStatsResponse) GetByStatus() map[string]int64 {
+ if x != nil {
+ return x.ByStatus
+ }
+ return nil
+}
+
+func (x *GetStatsResponse) GetAverageLatencyMs() float64 {
+ if x != nil {
+ return x.AverageLatencyMs
+ }
+ return 0
+}
+
+// HealthCheckRequest requests health status
+type HealthCheckRequest struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *HealthCheckRequest) Reset() {
+ *x = HealthCheckRequest{}
+ mi := &file_api_grpc_notifier_proto_msgTypes[17]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *HealthCheckRequest) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*HealthCheckRequest) ProtoMessage() {}
+
+func (x *HealthCheckRequest) ProtoReflect() protoreflect.Message {
+ mi := &file_api_grpc_notifier_proto_msgTypes[17]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use HealthCheckRequest.ProtoReflect.Descriptor instead.
+func (*HealthCheckRequest) Descriptor() ([]byte, []int) {
+ return file_api_grpc_notifier_proto_rawDescGZIP(), []int{17}
+}
+
+// HealthCheckResponse returns health status
+type HealthCheckResponse struct {
+ state protoimpl.MessageState `protogen:"open.v1"`
+ Healthy bool `protobuf:"varint,1,opt,name=healthy,proto3" json:"healthy,omitempty"`
+ Status string `protobuf:"bytes,2,opt,name=status,proto3" json:"status,omitempty"`
+ Components map[string]string `protobuf:"bytes,3,rep,name=components,proto3" json:"components,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
+ unknownFields protoimpl.UnknownFields
+ sizeCache protoimpl.SizeCache
+}
+
+func (x *HealthCheckResponse) Reset() {
+ *x = HealthCheckResponse{}
+ mi := &file_api_grpc_notifier_proto_msgTypes[18]
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ ms.StoreMessageInfo(mi)
+}
+
+func (x *HealthCheckResponse) String() string {
+ return protoimpl.X.MessageStringOf(x)
+}
+
+func (*HealthCheckResponse) ProtoMessage() {}
+
+func (x *HealthCheckResponse) ProtoReflect() protoreflect.Message {
+ mi := &file_api_grpc_notifier_proto_msgTypes[18]
+ if x != nil {
+ ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+ if ms.LoadMessageInfo() == nil {
+ ms.StoreMessageInfo(mi)
+ }
+ return ms
+ }
+ return mi.MessageOf(x)
+}
+
+// Deprecated: Use HealthCheckResponse.ProtoReflect.Descriptor instead.
+func (*HealthCheckResponse) Descriptor() ([]byte, []int) {
+ return file_api_grpc_notifier_proto_rawDescGZIP(), []int{18}
+}
+
+func (x *HealthCheckResponse) GetHealthy() bool {
+ if x != nil {
+ return x.Healthy
+ }
+ return false
+}
+
+func (x *HealthCheckResponse) GetStatus() string {
+ if x != nil {
+ return x.Status
+ }
+ return ""
+}
+
+func (x *HealthCheckResponse) GetComponents() map[string]string {
+ if x != nil {
+ return x.Components
+ }
+ return nil
+}
+
+var File_api_grpc_notifier_proto protoreflect.FileDescriptor
+
+const file_api_grpc_notifier_proto_rawDesc = "" +
+ "\n" +
+ "\x17api/grpc/notifier.proto\x12\vnotifier.v1\x1a\x1fgoogle/protobuf/timestamp.proto\"\x9f\x05\n" +
+ "\fNotification\x12\x0e\n" +
+ "\x02id\x18\x01 \x01(\tR\x02id\x121\n" +
+ "\x04type\x18\x02 \x01(\x0e2\x1d.notifier.v1.NotificationTypeR\x04type\x121\n" +
+ "\bpriority\x18\x03 \x01(\x0e2\x15.notifier.v1.PriorityR\bpriority\x127\n" +
+ "\x06status\x18\x04 \x01(\x0e2\x1f.notifier.v1.NotificationStatusR\x06status\x12\x18\n" +
+ "\asubject\x18\x05 \x01(\tR\asubject\x12\x12\n" +
+ "\x04body\x18\x06 \x01(\tR\x04body\x12\x1e\n" +
+ "\n" +
+ "recipients\x18\a \x03(\tR\n" +
+ "recipients\x12C\n" +
+ "\bmetadata\x18\b \x03(\v2'.notifier.v1.Notification.MetadataEntryR\bmetadata\x129\n" +
+ "\n" +
+ "created_at\x18\t \x01(\v2\x1a.google.protobuf.TimestampR\tcreatedAt\x12?\n" +
+ "\rscheduled_for\x18\n" +
+ " \x01(\v2\x1a.google.protobuf.TimestampR\fscheduledFor\x123\n" +
+ "\asent_at\x18\v \x01(\v2\x1a.google.protobuf.TimestampR\x06sentAt\x12\x1f\n" +
+ "\vretry_count\x18\f \x01(\x05R\n" +
+ "retryCount\x12\x1f\n" +
+ "\vmax_retries\x18\r \x01(\x05R\n" +
+ "maxRetries\x12\x1d\n" +
+ "\n" +
+ "last_error\x18\x0e \x01(\tR\tlastError\x1a;\n" +
+ "\rMetadataEntry\x12\x10\n" +
+ "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
+ "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xe5\x02\n" +
+ "\x12NotificationResult\x12'\n" +
+ "\x0fnotification_id\x18\x01 \x01(\tR\x0enotificationId\x12\x18\n" +
+ "\asuccess\x18\x02 \x01(\bR\asuccess\x12\x18\n" +
+ "\amessage\x18\x03 \x01(\tR\amessage\x12\x14\n" +
+ "\x05error\x18\x04 \x01(\tR\x05error\x123\n" +
+ "\asent_at\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\x06sentAt\x12b\n" +
+ "\x11provider_response\x18\x06 \x03(\v25.notifier.v1.NotificationResult.ProviderResponseEntryR\x10providerResponse\x1aC\n" +
+ "\x15ProviderResponseEntry\x12\x10\n" +
+ "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
+ "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"\xbc\x03\n" +
+ "\x17SendNotificationRequest\x121\n" +
+ "\x04type\x18\x01 \x01(\x0e2\x1d.notifier.v1.NotificationTypeR\x04type\x121\n" +
+ "\bpriority\x18\x02 \x01(\x0e2\x15.notifier.v1.PriorityR\bpriority\x12\x18\n" +
+ "\asubject\x18\x03 \x01(\tR\asubject\x12\x12\n" +
+ "\x04body\x18\x04 \x01(\tR\x04body\x12\x1e\n" +
+ "\n" +
+ "recipients\x18\x05 \x03(\tR\n" +
+ "recipients\x12N\n" +
+ "\bmetadata\x18\x06 \x03(\v22.notifier.v1.SendNotificationRequest.MetadataEntryR\bmetadata\x12?\n" +
+ "\rscheduled_for\x18\a \x01(\v2\x1a.google.protobuf.TimestampR\fscheduledFor\x12\x1f\n" +
+ "\vmax_retries\x18\b \x01(\x05R\n" +
+ "maxRetries\x1a;\n" +
+ "\rMetadataEntry\x12\x10\n" +
+ "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
+ "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01\"S\n" +
+ "\x18SendNotificationResponse\x127\n" +
+ "\x06result\x18\x01 \x01(\v2\x1f.notifier.v1.NotificationResultR\x06result\"k\n" +
+ "\x1dSendBatchNotificationsRequest\x12J\n" +
+ "\rnotifications\x18\x01 \x03(\v2$.notifier.v1.SendNotificationRequestR\rnotifications\"[\n" +
+ "\x1eSendBatchNotificationsResponse\x129\n" +
+ "\aresults\x18\x01 \x03(\v2\x1f.notifier.v1.NotificationResultR\aresults\"(\n" +
+ "\x16GetNotificationRequest\x12\x0e\n" +
+ "\x02id\x18\x01 \x01(\tR\x02id\"X\n" +
+ "\x17GetNotificationResponse\x12=\n" +
+ "\fnotification\x18\x01 \x01(\v2\x19.notifier.v1.NotificationR\fnotification\"\xea\x02\n" +
+ "\x12NotificationFilter\x12\x10\n" +
+ "\x03ids\x18\x01 \x03(\tR\x03ids\x123\n" +
+ "\x05types\x18\x02 \x03(\x0e2\x1d.notifier.v1.NotificationTypeR\x05types\x12;\n" +
+ "\bstatuses\x18\x03 \x03(\x0e2\x1f.notifier.v1.NotificationStatusR\bstatuses\x12\x1e\n" +
+ "\n" +
+ "recipients\x18\x04 \x03(\tR\n" +
+ "recipients\x12?\n" +
+ "\rcreated_after\x18\x05 \x01(\v2\x1a.google.protobuf.TimestampR\fcreatedAfter\x12A\n" +
+ "\x0ecreated_before\x18\x06 \x01(\v2\x1a.google.protobuf.TimestampR\rcreatedBefore\x12\x14\n" +
+ "\x05limit\x18\a \x01(\x05R\x05limit\x12\x16\n" +
+ "\x06offset\x18\b \x01(\x05R\x06offset\"S\n" +
+ "\x18ListNotificationsRequest\x127\n" +
+ "\x06filter\x18\x01 \x01(\v2\x1f.notifier.v1.NotificationFilterR\x06filter\"r\n" +
+ "\x19ListNotificationsResponse\x12?\n" +
+ "\rnotifications\x18\x01 \x03(\v2\x19.notifier.v1.NotificationR\rnotifications\x12\x14\n" +
+ "\x05total\x18\x02 \x01(\x03R\x05total\"+\n" +
+ "\x19CancelNotificationRequest\x12\x0e\n" +
+ "\x02id\x18\x01 \x01(\tR\x02id\"P\n" +
+ "\x1aCancelNotificationResponse\x12\x18\n" +
+ "\asuccess\x18\x01 \x01(\bR\asuccess\x12\x18\n" +
+ "\amessage\x18\x02 \x01(\tR\amessage\"*\n" +
+ "\x18RetryNotificationRequest\x12\x0e\n" +
+ "\x02id\x18\x01 \x01(\tR\x02id\"T\n" +
+ "\x19RetryNotificationResponse\x127\n" +
+ "\x06result\x18\x01 \x01(\v2\x1f.notifier.v1.NotificationResultR\x06result\"\x11\n" +
+ "\x0fGetStatsRequest\"\xd0\x03\n" +
+ "\x10GetStatsResponse\x12\x1d\n" +
+ "\n" +
+ "total_sent\x18\x01 \x01(\x03R\ttotalSent\x12!\n" +
+ "\ftotal_failed\x18\x02 \x01(\x03R\vtotalFailed\x12#\n" +
+ "\rtotal_pending\x18\x03 \x01(\x03R\ftotalPending\x12!\n" +
+ "\ftotal_queued\x18\x04 \x01(\x03R\vtotalQueued\x12B\n" +
+ "\aby_type\x18\x05 \x03(\v2).notifier.v1.GetStatsResponse.ByTypeEntryR\x06byType\x12H\n" +
+ "\tby_status\x18\x06 \x03(\v2+.notifier.v1.GetStatsResponse.ByStatusEntryR\bbyStatus\x12,\n" +
+ "\x12average_latency_ms\x18\a \x01(\x01R\x10averageLatencyMs\x1a9\n" +
+ "\vByTypeEntry\x12\x10\n" +
+ "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
+ "\x05value\x18\x02 \x01(\x03R\x05value:\x028\x01\x1a;\n" +
+ "\rByStatusEntry\x12\x10\n" +
+ "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
+ "\x05value\x18\x02 \x01(\x03R\x05value:\x028\x01\"\x14\n" +
+ "\x12HealthCheckRequest\"\xd8\x01\n" +
+ "\x13HealthCheckResponse\x12\x18\n" +
+ "\ahealthy\x18\x01 \x01(\bR\ahealthy\x12\x16\n" +
+ "\x06status\x18\x02 \x01(\tR\x06status\x12P\n" +
+ "\n" +
+ "components\x18\x03 \x03(\v20.notifier.v1.HealthCheckResponse.ComponentsEntryR\n" +
+ "components\x1a=\n" +
+ "\x0fComponentsEntry\x12\x10\n" +
+ "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" +
+ "\x05value\x18\x02 \x01(\tR\x05value:\x028\x01*\xa9\x01\n" +
+ "\x10NotificationType\x12!\n" +
+ "\x1dNOTIFICATION_TYPE_UNSPECIFIED\x10\x00\x12\x1b\n" +
+ "\x17NOTIFICATION_TYPE_EMAIL\x10\x01\x12\x1b\n" +
+ "\x17NOTIFICATION_TYPE_SLACK\x10\x02\x12\x1a\n" +
+ "\x16NOTIFICATION_TYPE_NTFY\x10\x03\x12\x1c\n" +
+ "\x18NOTIFICATION_TYPE_STDOUT\x10\x04*u\n" +
+ "\bPriority\x12\x18\n" +
+ "\x14PRIORITY_UNSPECIFIED\x10\x00\x12\x10\n" +
+ "\fPRIORITY_LOW\x10\x01\x12\x13\n" +
+ "\x0fPRIORITY_NORMAL\x10\x02\x12\x11\n" +
+ "\rPRIORITY_HIGH\x10\x03\x12\x15\n" +
+ "\x11PRIORITY_CRITICAL\x10\x04*\xfe\x01\n" +
+ "\x12NotificationStatus\x12#\n" +
+ "\x1fNOTIFICATION_STATUS_UNSPECIFIED\x10\x00\x12\x1f\n" +
+ "\x1bNOTIFICATION_STATUS_PENDING\x10\x01\x12\x1e\n" +
+ "\x1aNOTIFICATION_STATUS_QUEUED\x10\x02\x12\"\n" +
+ "\x1eNOTIFICATION_STATUS_PROCESSING\x10\x03\x12\x1c\n" +
+ "\x18NOTIFICATION_STATUS_SENT\x10\x04\x12\x1e\n" +
+ "\x1aNOTIFICATION_STATUS_FAILED\x10\x05\x12 \n" +
+ "\x1cNOTIFICATION_STATUS_RETRYING\x10\x062\x8d\x06\n" +
+ "\x0fNotifierService\x12_\n" +
+ "\x10SendNotification\x12$.notifier.v1.SendNotificationRequest\x1a%.notifier.v1.SendNotificationResponse\x12q\n" +
+ "\x16SendBatchNotifications\x12*.notifier.v1.SendBatchNotificationsRequest\x1a+.notifier.v1.SendBatchNotificationsResponse\x12\\\n" +
+ "\x0fGetNotification\x12#.notifier.v1.GetNotificationRequest\x1a$.notifier.v1.GetNotificationResponse\x12b\n" +
+ "\x11ListNotifications\x12%.notifier.v1.ListNotificationsRequest\x1a&.notifier.v1.ListNotificationsResponse\x12e\n" +
+ "\x12CancelNotification\x12&.notifier.v1.CancelNotificationRequest\x1a'.notifier.v1.CancelNotificationResponse\x12b\n" +
+ "\x11RetryNotification\x12%.notifier.v1.RetryNotificationRequest\x1a&.notifier.v1.RetryNotificationResponse\x12G\n" +
+ "\bGetStats\x12\x1c.notifier.v1.GetStatsRequest\x1a\x1d.notifier.v1.GetStatsResponse\x12P\n" +
+ "\vHealthCheck\x12\x1f.notifier.v1.HealthCheckRequest\x1a .notifier.v1.HealthCheckResponseB)Z'github.com/igodwin/notifier/api/grpc/pbb\x06proto3"
+
+var (
+ file_api_grpc_notifier_proto_rawDescOnce sync.Once
+ file_api_grpc_notifier_proto_rawDescData []byte
+)
+
+func file_api_grpc_notifier_proto_rawDescGZIP() []byte {
+ file_api_grpc_notifier_proto_rawDescOnce.Do(func() {
+ file_api_grpc_notifier_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_api_grpc_notifier_proto_rawDesc), len(file_api_grpc_notifier_proto_rawDesc)))
+ })
+ return file_api_grpc_notifier_proto_rawDescData
+}
+
+var file_api_grpc_notifier_proto_enumTypes = make([]protoimpl.EnumInfo, 3)
+var file_api_grpc_notifier_proto_msgTypes = make([]protoimpl.MessageInfo, 25)
+var file_api_grpc_notifier_proto_goTypes = []any{
+ (NotificationType)(0), // 0: notifier.v1.NotificationType
+ (Priority)(0), // 1: notifier.v1.Priority
+ (NotificationStatus)(0), // 2: notifier.v1.NotificationStatus
+ (*Notification)(nil), // 3: notifier.v1.Notification
+ (*NotificationResult)(nil), // 4: notifier.v1.NotificationResult
+ (*SendNotificationRequest)(nil), // 5: notifier.v1.SendNotificationRequest
+ (*SendNotificationResponse)(nil), // 6: notifier.v1.SendNotificationResponse
+ (*SendBatchNotificationsRequest)(nil), // 7: notifier.v1.SendBatchNotificationsRequest
+ (*SendBatchNotificationsResponse)(nil), // 8: notifier.v1.SendBatchNotificationsResponse
+ (*GetNotificationRequest)(nil), // 9: notifier.v1.GetNotificationRequest
+ (*GetNotificationResponse)(nil), // 10: notifier.v1.GetNotificationResponse
+ (*NotificationFilter)(nil), // 11: notifier.v1.NotificationFilter
+ (*ListNotificationsRequest)(nil), // 12: notifier.v1.ListNotificationsRequest
+ (*ListNotificationsResponse)(nil), // 13: notifier.v1.ListNotificationsResponse
+ (*CancelNotificationRequest)(nil), // 14: notifier.v1.CancelNotificationRequest
+ (*CancelNotificationResponse)(nil), // 15: notifier.v1.CancelNotificationResponse
+ (*RetryNotificationRequest)(nil), // 16: notifier.v1.RetryNotificationRequest
+ (*RetryNotificationResponse)(nil), // 17: notifier.v1.RetryNotificationResponse
+ (*GetStatsRequest)(nil), // 18: notifier.v1.GetStatsRequest
+ (*GetStatsResponse)(nil), // 19: notifier.v1.GetStatsResponse
+ (*HealthCheckRequest)(nil), // 20: notifier.v1.HealthCheckRequest
+ (*HealthCheckResponse)(nil), // 21: notifier.v1.HealthCheckResponse
+ nil, // 22: notifier.v1.Notification.MetadataEntry
+ nil, // 23: notifier.v1.NotificationResult.ProviderResponseEntry
+ nil, // 24: notifier.v1.SendNotificationRequest.MetadataEntry
+ nil, // 25: notifier.v1.GetStatsResponse.ByTypeEntry
+ nil, // 26: notifier.v1.GetStatsResponse.ByStatusEntry
+ nil, // 27: notifier.v1.HealthCheckResponse.ComponentsEntry
+ (*timestamppb.Timestamp)(nil), // 28: google.protobuf.Timestamp
+}
+var file_api_grpc_notifier_proto_depIdxs = []int32{
+ 0, // 0: notifier.v1.Notification.type:type_name -> notifier.v1.NotificationType
+ 1, // 1: notifier.v1.Notification.priority:type_name -> notifier.v1.Priority
+ 2, // 2: notifier.v1.Notification.status:type_name -> notifier.v1.NotificationStatus
+ 22, // 3: notifier.v1.Notification.metadata:type_name -> notifier.v1.Notification.MetadataEntry
+ 28, // 4: notifier.v1.Notification.created_at:type_name -> google.protobuf.Timestamp
+ 28, // 5: notifier.v1.Notification.scheduled_for:type_name -> google.protobuf.Timestamp
+ 28, // 6: notifier.v1.Notification.sent_at:type_name -> google.protobuf.Timestamp
+ 28, // 7: notifier.v1.NotificationResult.sent_at:type_name -> google.protobuf.Timestamp
+ 23, // 8: notifier.v1.NotificationResult.provider_response:type_name -> notifier.v1.NotificationResult.ProviderResponseEntry
+ 0, // 9: notifier.v1.SendNotificationRequest.type:type_name -> notifier.v1.NotificationType
+ 1, // 10: notifier.v1.SendNotificationRequest.priority:type_name -> notifier.v1.Priority
+ 24, // 11: notifier.v1.SendNotificationRequest.metadata:type_name -> notifier.v1.SendNotificationRequest.MetadataEntry
+ 28, // 12: notifier.v1.SendNotificationRequest.scheduled_for:type_name -> google.protobuf.Timestamp
+ 4, // 13: notifier.v1.SendNotificationResponse.result:type_name -> notifier.v1.NotificationResult
+ 5, // 14: notifier.v1.SendBatchNotificationsRequest.notifications:type_name -> notifier.v1.SendNotificationRequest
+ 4, // 15: notifier.v1.SendBatchNotificationsResponse.results:type_name -> notifier.v1.NotificationResult
+ 3, // 16: notifier.v1.GetNotificationResponse.notification:type_name -> notifier.v1.Notification
+ 0, // 17: notifier.v1.NotificationFilter.types:type_name -> notifier.v1.NotificationType
+ 2, // 18: notifier.v1.NotificationFilter.statuses:type_name -> notifier.v1.NotificationStatus
+ 28, // 19: notifier.v1.NotificationFilter.created_after:type_name -> google.protobuf.Timestamp
+ 28, // 20: notifier.v1.NotificationFilter.created_before:type_name -> google.protobuf.Timestamp
+ 11, // 21: notifier.v1.ListNotificationsRequest.filter:type_name -> notifier.v1.NotificationFilter
+ 3, // 22: notifier.v1.ListNotificationsResponse.notifications:type_name -> notifier.v1.Notification
+ 4, // 23: notifier.v1.RetryNotificationResponse.result:type_name -> notifier.v1.NotificationResult
+ 25, // 24: notifier.v1.GetStatsResponse.by_type:type_name -> notifier.v1.GetStatsResponse.ByTypeEntry
+ 26, // 25: notifier.v1.GetStatsResponse.by_status:type_name -> notifier.v1.GetStatsResponse.ByStatusEntry
+ 27, // 26: notifier.v1.HealthCheckResponse.components:type_name -> notifier.v1.HealthCheckResponse.ComponentsEntry
+ 5, // 27: notifier.v1.NotifierService.SendNotification:input_type -> notifier.v1.SendNotificationRequest
+ 7, // 28: notifier.v1.NotifierService.SendBatchNotifications:input_type -> notifier.v1.SendBatchNotificationsRequest
+ 9, // 29: notifier.v1.NotifierService.GetNotification:input_type -> notifier.v1.GetNotificationRequest
+ 12, // 30: notifier.v1.NotifierService.ListNotifications:input_type -> notifier.v1.ListNotificationsRequest
+ 14, // 31: notifier.v1.NotifierService.CancelNotification:input_type -> notifier.v1.CancelNotificationRequest
+ 16, // 32: notifier.v1.NotifierService.RetryNotification:input_type -> notifier.v1.RetryNotificationRequest
+ 18, // 33: notifier.v1.NotifierService.GetStats:input_type -> notifier.v1.GetStatsRequest
+ 20, // 34: notifier.v1.NotifierService.HealthCheck:input_type -> notifier.v1.HealthCheckRequest
+ 6, // 35: notifier.v1.NotifierService.SendNotification:output_type -> notifier.v1.SendNotificationResponse
+ 8, // 36: notifier.v1.NotifierService.SendBatchNotifications:output_type -> notifier.v1.SendBatchNotificationsResponse
+ 10, // 37: notifier.v1.NotifierService.GetNotification:output_type -> notifier.v1.GetNotificationResponse
+ 13, // 38: notifier.v1.NotifierService.ListNotifications:output_type -> notifier.v1.ListNotificationsResponse
+ 15, // 39: notifier.v1.NotifierService.CancelNotification:output_type -> notifier.v1.CancelNotificationResponse
+ 17, // 40: notifier.v1.NotifierService.RetryNotification:output_type -> notifier.v1.RetryNotificationResponse
+ 19, // 41: notifier.v1.NotifierService.GetStats:output_type -> notifier.v1.GetStatsResponse
+ 21, // 42: notifier.v1.NotifierService.HealthCheck:output_type -> notifier.v1.HealthCheckResponse
+ 35, // [35:43] is the sub-list for method output_type
+ 27, // [27:35] is the sub-list for method input_type
+ 27, // [27:27] is the sub-list for extension type_name
+ 27, // [27:27] is the sub-list for extension extendee
+ 0, // [0:27] is the sub-list for field type_name
+}
+
+func init() { file_api_grpc_notifier_proto_init() }
+func file_api_grpc_notifier_proto_init() {
+ if File_api_grpc_notifier_proto != nil {
+ return
+ }
+ type x struct{}
+ out := protoimpl.TypeBuilder{
+ File: protoimpl.DescBuilder{
+ GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
+ RawDescriptor: unsafe.Slice(unsafe.StringData(file_api_grpc_notifier_proto_rawDesc), len(file_api_grpc_notifier_proto_rawDesc)),
+ NumEnums: 3,
+ NumMessages: 25,
+ NumExtensions: 0,
+ NumServices: 1,
+ },
+ GoTypes: file_api_grpc_notifier_proto_goTypes,
+ DependencyIndexes: file_api_grpc_notifier_proto_depIdxs,
+ EnumInfos: file_api_grpc_notifier_proto_enumTypes,
+ MessageInfos: file_api_grpc_notifier_proto_msgTypes,
+ }.Build()
+ File_api_grpc_notifier_proto = out.File
+ file_api_grpc_notifier_proto_goTypes = nil
+ file_api_grpc_notifier_proto_depIdxs = nil
+}
diff --git a/api/grpc/pb/api/grpc/notifier_grpc.pb.go b/api/grpc/pb/api/grpc/notifier_grpc.pb.go
new file mode 100644
index 0000000..d386292
--- /dev/null
+++ b/api/grpc/pb/api/grpc/notifier_grpc.pb.go
@@ -0,0 +1,407 @@
+// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
+// versions:
+// - protoc-gen-go-grpc v1.5.1
+// - protoc v6.33.0
+// source: api/grpc/notifier.proto
+
+package pb
+
+import (
+ context "context"
+ grpc "google.golang.org/grpc"
+ codes "google.golang.org/grpc/codes"
+ status "google.golang.org/grpc/status"
+)
+
+// This is a compile-time assertion to ensure that this generated file
+// is compatible with the grpc package it is being compiled against.
+// Requires gRPC-Go v1.64.0 or later.
+const _ = grpc.SupportPackageIsVersion9
+
+const (
+ NotifierService_SendNotification_FullMethodName = "/notifier.v1.NotifierService/SendNotification"
+ NotifierService_SendBatchNotifications_FullMethodName = "/notifier.v1.NotifierService/SendBatchNotifications"
+ NotifierService_GetNotification_FullMethodName = "/notifier.v1.NotifierService/GetNotification"
+ NotifierService_ListNotifications_FullMethodName = "/notifier.v1.NotifierService/ListNotifications"
+ NotifierService_CancelNotification_FullMethodName = "/notifier.v1.NotifierService/CancelNotification"
+ NotifierService_RetryNotification_FullMethodName = "/notifier.v1.NotifierService/RetryNotification"
+ NotifierService_GetStats_FullMethodName = "/notifier.v1.NotifierService/GetStats"
+ NotifierService_HealthCheck_FullMethodName = "/notifier.v1.NotifierService/HealthCheck"
+)
+
+// NotifierServiceClient is the client API for NotifierService service.
+//
+// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
+//
+// NotifierService handles notification operations
+type NotifierServiceClient interface {
+ // SendNotification sends a single notification
+ SendNotification(ctx context.Context, in *SendNotificationRequest, opts ...grpc.CallOption) (*SendNotificationResponse, error)
+ // SendBatchNotifications sends multiple notifications
+ SendBatchNotifications(ctx context.Context, in *SendBatchNotificationsRequest, opts ...grpc.CallOption) (*SendBatchNotificationsResponse, error)
+ // GetNotification retrieves a notification by ID
+ GetNotification(ctx context.Context, in *GetNotificationRequest, opts ...grpc.CallOption) (*GetNotificationResponse, error)
+ // ListNotifications retrieves notifications matching a filter
+ ListNotifications(ctx context.Context, in *ListNotificationsRequest, opts ...grpc.CallOption) (*ListNotificationsResponse, error)
+ // CancelNotification cancels a pending notification
+ CancelNotification(ctx context.Context, in *CancelNotificationRequest, opts ...grpc.CallOption) (*CancelNotificationResponse, error)
+ // RetryNotification retries a failed notification
+ RetryNotification(ctx context.Context, in *RetryNotificationRequest, opts ...grpc.CallOption) (*RetryNotificationResponse, error)
+ // GetStats returns notification statistics
+ GetStats(ctx context.Context, in *GetStatsRequest, opts ...grpc.CallOption) (*GetStatsResponse, error)
+ // HealthCheck verifies the service is operational
+ HealthCheck(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error)
+}
+
+type notifierServiceClient struct {
+ cc grpc.ClientConnInterface
+}
+
+func NewNotifierServiceClient(cc grpc.ClientConnInterface) NotifierServiceClient {
+ return ¬ifierServiceClient{cc}
+}
+
+func (c *notifierServiceClient) SendNotification(ctx context.Context, in *SendNotificationRequest, opts ...grpc.CallOption) (*SendNotificationResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(SendNotificationResponse)
+ err := c.cc.Invoke(ctx, NotifierService_SendNotification_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *notifierServiceClient) SendBatchNotifications(ctx context.Context, in *SendBatchNotificationsRequest, opts ...grpc.CallOption) (*SendBatchNotificationsResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(SendBatchNotificationsResponse)
+ err := c.cc.Invoke(ctx, NotifierService_SendBatchNotifications_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *notifierServiceClient) GetNotification(ctx context.Context, in *GetNotificationRequest, opts ...grpc.CallOption) (*GetNotificationResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(GetNotificationResponse)
+ err := c.cc.Invoke(ctx, NotifierService_GetNotification_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *notifierServiceClient) ListNotifications(ctx context.Context, in *ListNotificationsRequest, opts ...grpc.CallOption) (*ListNotificationsResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(ListNotificationsResponse)
+ err := c.cc.Invoke(ctx, NotifierService_ListNotifications_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *notifierServiceClient) CancelNotification(ctx context.Context, in *CancelNotificationRequest, opts ...grpc.CallOption) (*CancelNotificationResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(CancelNotificationResponse)
+ err := c.cc.Invoke(ctx, NotifierService_CancelNotification_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *notifierServiceClient) RetryNotification(ctx context.Context, in *RetryNotificationRequest, opts ...grpc.CallOption) (*RetryNotificationResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(RetryNotificationResponse)
+ err := c.cc.Invoke(ctx, NotifierService_RetryNotification_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *notifierServiceClient) GetStats(ctx context.Context, in *GetStatsRequest, opts ...grpc.CallOption) (*GetStatsResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(GetStatsResponse)
+ err := c.cc.Invoke(ctx, NotifierService_GetStats_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+func (c *notifierServiceClient) HealthCheck(ctx context.Context, in *HealthCheckRequest, opts ...grpc.CallOption) (*HealthCheckResponse, error) {
+ cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
+ out := new(HealthCheckResponse)
+ err := c.cc.Invoke(ctx, NotifierService_HealthCheck_FullMethodName, in, out, cOpts...)
+ if err != nil {
+ return nil, err
+ }
+ return out, nil
+}
+
+// NotifierServiceServer is the server API for NotifierService service.
+// All implementations must embed UnimplementedNotifierServiceServer
+// for forward compatibility.
+//
+// NotifierService handles notification operations
+type NotifierServiceServer interface {
+ // SendNotification sends a single notification
+ SendNotification(context.Context, *SendNotificationRequest) (*SendNotificationResponse, error)
+ // SendBatchNotifications sends multiple notifications
+ SendBatchNotifications(context.Context, *SendBatchNotificationsRequest) (*SendBatchNotificationsResponse, error)
+ // GetNotification retrieves a notification by ID
+ GetNotification(context.Context, *GetNotificationRequest) (*GetNotificationResponse, error)
+ // ListNotifications retrieves notifications matching a filter
+ ListNotifications(context.Context, *ListNotificationsRequest) (*ListNotificationsResponse, error)
+ // CancelNotification cancels a pending notification
+ CancelNotification(context.Context, *CancelNotificationRequest) (*CancelNotificationResponse, error)
+ // RetryNotification retries a failed notification
+ RetryNotification(context.Context, *RetryNotificationRequest) (*RetryNotificationResponse, error)
+ // GetStats returns notification statistics
+ GetStats(context.Context, *GetStatsRequest) (*GetStatsResponse, error)
+ // HealthCheck verifies the service is operational
+ HealthCheck(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error)
+ mustEmbedUnimplementedNotifierServiceServer()
+}
+
+// UnimplementedNotifierServiceServer must be embedded to have
+// forward compatible implementations.
+//
+// NOTE: this should be embedded by value instead of pointer to avoid a nil
+// pointer dereference when methods are called.
+type UnimplementedNotifierServiceServer struct{}
+
+func (UnimplementedNotifierServiceServer) SendNotification(context.Context, *SendNotificationRequest) (*SendNotificationResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method SendNotification not implemented")
+}
+func (UnimplementedNotifierServiceServer) SendBatchNotifications(context.Context, *SendBatchNotificationsRequest) (*SendBatchNotificationsResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method SendBatchNotifications not implemented")
+}
+func (UnimplementedNotifierServiceServer) GetNotification(context.Context, *GetNotificationRequest) (*GetNotificationResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method GetNotification not implemented")
+}
+func (UnimplementedNotifierServiceServer) ListNotifications(context.Context, *ListNotificationsRequest) (*ListNotificationsResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method ListNotifications not implemented")
+}
+func (UnimplementedNotifierServiceServer) CancelNotification(context.Context, *CancelNotificationRequest) (*CancelNotificationResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method CancelNotification not implemented")
+}
+func (UnimplementedNotifierServiceServer) RetryNotification(context.Context, *RetryNotificationRequest) (*RetryNotificationResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method RetryNotification not implemented")
+}
+func (UnimplementedNotifierServiceServer) GetStats(context.Context, *GetStatsRequest) (*GetStatsResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method GetStats not implemented")
+}
+func (UnimplementedNotifierServiceServer) HealthCheck(context.Context, *HealthCheckRequest) (*HealthCheckResponse, error) {
+ return nil, status.Errorf(codes.Unimplemented, "method HealthCheck not implemented")
+}
+func (UnimplementedNotifierServiceServer) mustEmbedUnimplementedNotifierServiceServer() {}
+func (UnimplementedNotifierServiceServer) testEmbeddedByValue() {}
+
+// UnsafeNotifierServiceServer may be embedded to opt out of forward compatibility for this service.
+// Use of this interface is not recommended, as added methods to NotifierServiceServer will
+// result in compilation errors.
+type UnsafeNotifierServiceServer interface {
+ mustEmbedUnimplementedNotifierServiceServer()
+}
+
+func RegisterNotifierServiceServer(s grpc.ServiceRegistrar, srv NotifierServiceServer) {
+ // If the following call pancis, it indicates UnimplementedNotifierServiceServer was
+ // embedded by pointer and is nil. This will cause panics if an
+ // unimplemented method is ever invoked, so we test this at initialization
+ // time to prevent it from happening at runtime later due to I/O.
+ if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
+ t.testEmbeddedByValue()
+ }
+ s.RegisterService(&NotifierService_ServiceDesc, srv)
+}
+
+func _NotifierService_SendNotification_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(SendNotificationRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(NotifierServiceServer).SendNotification(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: NotifierService_SendNotification_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(NotifierServiceServer).SendNotification(ctx, req.(*SendNotificationRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _NotifierService_SendBatchNotifications_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(SendBatchNotificationsRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(NotifierServiceServer).SendBatchNotifications(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: NotifierService_SendBatchNotifications_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(NotifierServiceServer).SendBatchNotifications(ctx, req.(*SendBatchNotificationsRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _NotifierService_GetNotification_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(GetNotificationRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(NotifierServiceServer).GetNotification(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: NotifierService_GetNotification_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(NotifierServiceServer).GetNotification(ctx, req.(*GetNotificationRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _NotifierService_ListNotifications_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(ListNotificationsRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(NotifierServiceServer).ListNotifications(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: NotifierService_ListNotifications_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(NotifierServiceServer).ListNotifications(ctx, req.(*ListNotificationsRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _NotifierService_CancelNotification_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(CancelNotificationRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(NotifierServiceServer).CancelNotification(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: NotifierService_CancelNotification_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(NotifierServiceServer).CancelNotification(ctx, req.(*CancelNotificationRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _NotifierService_RetryNotification_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(RetryNotificationRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(NotifierServiceServer).RetryNotification(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: NotifierService_RetryNotification_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(NotifierServiceServer).RetryNotification(ctx, req.(*RetryNotificationRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _NotifierService_GetStats_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(GetStatsRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(NotifierServiceServer).GetStats(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: NotifierService_GetStats_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(NotifierServiceServer).GetStats(ctx, req.(*GetStatsRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+func _NotifierService_HealthCheck_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+ in := new(HealthCheckRequest)
+ if err := dec(in); err != nil {
+ return nil, err
+ }
+ if interceptor == nil {
+ return srv.(NotifierServiceServer).HealthCheck(ctx, in)
+ }
+ info := &grpc.UnaryServerInfo{
+ Server: srv,
+ FullMethod: NotifierService_HealthCheck_FullMethodName,
+ }
+ handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+ return srv.(NotifierServiceServer).HealthCheck(ctx, req.(*HealthCheckRequest))
+ }
+ return interceptor(ctx, in, info, handler)
+}
+
+// NotifierService_ServiceDesc is the grpc.ServiceDesc for NotifierService service.
+// It's only intended for direct use with grpc.RegisterService,
+// and not to be introspected or modified (even as a copy)
+var NotifierService_ServiceDesc = grpc.ServiceDesc{
+ ServiceName: "notifier.v1.NotifierService",
+ HandlerType: (*NotifierServiceServer)(nil),
+ Methods: []grpc.MethodDesc{
+ {
+ MethodName: "SendNotification",
+ Handler: _NotifierService_SendNotification_Handler,
+ },
+ {
+ MethodName: "SendBatchNotifications",
+ Handler: _NotifierService_SendBatchNotifications_Handler,
+ },
+ {
+ MethodName: "GetNotification",
+ Handler: _NotifierService_GetNotification_Handler,
+ },
+ {
+ MethodName: "ListNotifications",
+ Handler: _NotifierService_ListNotifications_Handler,
+ },
+ {
+ MethodName: "CancelNotification",
+ Handler: _NotifierService_CancelNotification_Handler,
+ },
+ {
+ MethodName: "RetryNotification",
+ Handler: _NotifierService_RetryNotification_Handler,
+ },
+ {
+ MethodName: "GetStats",
+ Handler: _NotifierService_GetStats_Handler,
+ },
+ {
+ MethodName: "HealthCheck",
+ Handler: _NotifierService_HealthCheck_Handler,
+ },
+ },
+ Streams: []grpc.StreamDesc{},
+ Metadata: "api/grpc/notifier.proto",
+}
diff --git a/api/rest/handlers.go b/api/rest/handlers.go
index e69de29..f3a3058 100644
--- a/api/rest/handlers.go
+++ b/api/rest/handlers.go
@@ -0,0 +1,239 @@
+package rest
+
+import (
+ "encoding/json"
+ "net/http"
+ "strconv"
+ "time"
+
+ "github.com/gorilla/mux"
+ "github.com/igodwin/notifier/internal/domain"
+)
+
+// Handler handles REST API requests
+type Handler struct {
+ service domain.NotificationService
+}
+
+// NewHandler creates a new REST handler
+func NewHandler(service domain.NotificationService) *Handler {
+ return &Handler{
+ service: service,
+ }
+}
+
+// SendNotification handles POST /api/v1/notifications
+func (h *Handler) SendNotification(w http.ResponseWriter, r *http.Request) {
+ var req SendNotificationRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ respondError(w, http.StatusBadRequest, "invalid request body", err)
+ return
+ }
+
+ // Validate request
+ if err := req.Validate(); err != nil {
+ respondError(w, http.StatusBadRequest, "validation failed", err)
+ return
+ }
+
+ // Convert to domain notification
+ notification := req.ToNotification()
+
+ // Send notification
+ result, err := h.service.Send(r.Context(), notification)
+ if err != nil {
+ respondError(w, http.StatusInternalServerError, "failed to send notification", err)
+ return
+ }
+
+ respondJSON(w, http.StatusAccepted, SendNotificationResponse{
+ Result: NotificationResultFromDomain(result),
+ })
+}
+
+// SendBatchNotifications handles POST /api/v1/notifications/batch
+func (h *Handler) SendBatchNotifications(w http.ResponseWriter, r *http.Request) {
+ var req SendBatchNotificationsRequest
+ if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
+ respondError(w, http.StatusBadRequest, "invalid request body", err)
+ return
+ }
+
+ // Validate and convert to domain notifications
+ notifications := make([]*domain.Notification, 0, len(req.Notifications))
+ for _, notifReq := range req.Notifications {
+ if err := notifReq.Validate(); err != nil {
+ respondError(w, http.StatusBadRequest, "validation failed", err)
+ return
+ }
+ notifications = append(notifications, notifReq.ToNotification())
+ }
+
+ // Send batch
+ results, err := h.service.SendBatch(r.Context(), notifications)
+ if err != nil {
+ respondError(w, http.StatusInternalServerError, "failed to send batch notifications", err)
+ return
+ }
+
+ // Convert results
+ apiResults := make([]NotificationResult, 0, len(results))
+ for _, result := range results {
+ apiResults = append(apiResults, NotificationResultFromDomain(result))
+ }
+
+ respondJSON(w, http.StatusAccepted, SendBatchNotificationsResponse{
+ Results: apiResults,
+ })
+}
+
+// GetNotification handles GET /api/v1/notifications/{id}
+func (h *Handler) GetNotification(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ id := vars["id"]
+
+ notification, err := h.service.GetNotification(r.Context(), id)
+ if err != nil {
+ respondError(w, http.StatusNotFound, "notification not found", err)
+ return
+ }
+
+ respondJSON(w, http.StatusOK, NotificationFromDomain(notification))
+}
+
+// ListNotifications handles GET /api/v1/notifications
+func (h *Handler) ListNotifications(w http.ResponseWriter, r *http.Request) {
+ filter := parseNotificationFilter(r)
+
+ notifications, err := h.service.ListNotifications(r.Context(), filter)
+ if err != nil {
+ respondError(w, http.StatusInternalServerError, "failed to list notifications", err)
+ return
+ }
+
+ // Convert to API format
+ apiNotifications := make([]Notification, 0, len(notifications))
+ for _, notif := range notifications {
+ apiNotifications = append(apiNotifications, NotificationFromDomain(notif))
+ }
+
+ respondJSON(w, http.StatusOK, ListNotificationsResponse{
+ Notifications: apiNotifications,
+ Total: int64(len(apiNotifications)),
+ })
+}
+
+// CancelNotification handles DELETE /api/v1/notifications/{id}
+func (h *Handler) CancelNotification(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ id := vars["id"]
+
+ if err := h.service.CancelNotification(r.Context(), id); err != nil {
+ respondError(w, http.StatusInternalServerError, "failed to cancel notification", err)
+ return
+ }
+
+ respondJSON(w, http.StatusOK, map[string]interface{}{
+ "success": true,
+ "message": "notification canceled successfully",
+ })
+}
+
+// RetryNotification handles POST /api/v1/notifications/{id}/retry
+func (h *Handler) RetryNotification(w http.ResponseWriter, r *http.Request) {
+ vars := mux.Vars(r)
+ id := vars["id"]
+
+ result, err := h.service.RetryNotification(r.Context(), id)
+ if err != nil {
+ respondError(w, http.StatusInternalServerError, "failed to retry notification", err)
+ return
+ }
+
+ respondJSON(w, http.StatusOK, RetryNotificationResponse{
+ Result: NotificationResultFromDomain(result),
+ })
+}
+
+// GetStats handles GET /api/v1/stats
+func (h *Handler) GetStats(w http.ResponseWriter, r *http.Request) {
+ stats, err := h.service.GetStats(r.Context())
+ if err != nil {
+ respondError(w, http.StatusInternalServerError, "failed to get stats", err)
+ return
+ }
+
+ respondJSON(w, http.StatusOK, stats)
+}
+
+// HealthCheck handles GET /health
+func (h *Handler) HealthCheck(w http.ResponseWriter, r *http.Request) {
+ respondJSON(w, http.StatusOK, map[string]interface{}{
+ "status": "healthy",
+ "service": "notifier",
+ "time": time.Now().UTC(),
+ })
+}
+
+// parseNotificationFilter parses query parameters into a NotificationFilter
+func parseNotificationFilter(r *http.Request) *domain.NotificationFilter {
+ query := r.URL.Query()
+ filter := &domain.NotificationFilter{}
+
+ // Parse limit
+ if limitStr := query.Get("limit"); limitStr != "" {
+ if limit, err := strconv.Atoi(limitStr); err == nil {
+ filter.Limit = limit
+ }
+ }
+
+ // Parse offset
+ if offsetStr := query.Get("offset"); offsetStr != "" {
+ if offset, err := strconv.Atoi(offsetStr); err == nil {
+ filter.Offset = offset
+ }
+ }
+
+ // Parse types
+ if types := query["type"]; len(types) > 0 {
+ for _, t := range types {
+ filter.Types = append(filter.Types, domain.NotificationType(t))
+ }
+ }
+
+ // Parse statuses
+ if statuses := query["status"]; len(statuses) > 0 {
+ for _, s := range statuses {
+ filter.Statuses = append(filter.Statuses, domain.NotificationStatus(s))
+ }
+ }
+
+ // Parse recipients
+ if recipients := query["recipient"]; len(recipients) > 0 {
+ filter.Recipients = recipients
+ }
+
+ return filter
+}
+
+// respondJSON sends a JSON response
+func respondJSON(w http.ResponseWriter, status int, data interface{}) {
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(status)
+ if err := json.NewEncoder(w).Encode(data); err != nil {
+ http.Error(w, err.Error(), http.StatusInternalServerError)
+ }
+}
+
+// respondError sends an error response
+func respondError(w http.ResponseWriter, status int, message string, err error) {
+ errMsg := message
+ if err != nil {
+ errMsg = message + ": " + err.Error()
+ }
+
+ respondJSON(w, status, map[string]interface{}{
+ "error": message,
+ "details": errMsg,
+ })
+}
diff --git a/api/rest/router.go b/api/rest/router.go
index e69de29..d575772 100644
--- a/api/rest/router.go
+++ b/api/rest/router.go
@@ -0,0 +1,61 @@
+package rest
+
+import (
+ "net/http"
+
+ "github.com/gorilla/mux"
+ "github.com/igodwin/notifier/internal/domain"
+)
+
+// NewRouter creates a new HTTP router with all routes configured
+func NewRouter(service domain.NotificationService) *mux.Router {
+ handler := NewHandler(service)
+ router := mux.NewRouter()
+
+ // API v1 routes
+ v1 := router.PathPrefix("/api/v1").Subrouter()
+
+ // Notification routes
+ v1.HandleFunc("/notifications", handler.SendNotification).Methods(http.MethodPost)
+ v1.HandleFunc("/notifications/batch", handler.SendBatchNotifications).Methods(http.MethodPost)
+ v1.HandleFunc("/notifications", handler.ListNotifications).Methods(http.MethodGet)
+ v1.HandleFunc("/notifications/{id}", handler.GetNotification).Methods(http.MethodGet)
+ v1.HandleFunc("/notifications/{id}", handler.CancelNotification).Methods(http.MethodDelete)
+ v1.HandleFunc("/notifications/{id}/retry", handler.RetryNotification).Methods(http.MethodPost)
+
+ // Stats route
+ v1.HandleFunc("/stats", handler.GetStats).Methods(http.MethodGet)
+
+ // Health check route
+ router.HandleFunc("/health", handler.HealthCheck).Methods(http.MethodGet)
+
+ // Middleware
+ router.Use(loggingMiddleware)
+ router.Use(corsMiddleware)
+
+ return router
+}
+
+// loggingMiddleware logs incoming requests
+func loggingMiddleware(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ // You can add structured logging here
+ next.ServeHTTP(w, r)
+ })
+}
+
+// corsMiddleware adds CORS headers
+func corsMiddleware(next http.Handler) http.Handler {
+ return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Access-Control-Allow-Origin", "*")
+ w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
+ w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
+
+ if r.Method == http.MethodOptions {
+ w.WriteHeader(http.StatusOK)
+ return
+ }
+
+ next.ServeHTTP(w, r)
+ })
+}
diff --git a/api/rest/types.go b/api/rest/types.go
new file mode 100644
index 0000000..bef6590
--- /dev/null
+++ b/api/rest/types.go
@@ -0,0 +1,147 @@
+package rest
+
+import (
+ "fmt"
+ "time"
+
+ "github.com/google/uuid"
+ "github.com/igodwin/notifier/internal/domain"
+)
+
+// SendNotificationRequest is the REST API request for sending a notification
+type SendNotificationRequest struct {
+ Type string `json:"type"`
+ Priority int `json:"priority,omitempty"`
+ Subject string `json:"subject"`
+ Body string `json:"body"`
+ Recipients []string `json:"recipients"`
+ Metadata map[string]interface{} `json:"metadata,omitempty"`
+ ScheduledFor *time.Time `json:"scheduled_for,omitempty"`
+ MaxRetries int `json:"max_retries,omitempty"`
+}
+
+// Validate validates the request
+func (r *SendNotificationRequest) Validate() error {
+ if r.Type == "" {
+ return fmt.Errorf("type is required")
+ }
+
+ if len(r.Recipients) == 0 {
+ return fmt.Errorf("at least one recipient is required")
+ }
+
+ if r.Body == "" {
+ return fmt.Errorf("body is required")
+ }
+
+ return nil
+}
+
+// ToNotification converts the request to a domain notification
+func (r *SendNotificationRequest) ToNotification() *domain.Notification {
+ maxRetries := r.MaxRetries
+ if maxRetries == 0 {
+ maxRetries = 3 // Default
+ }
+
+ return &domain.Notification{
+ ID: uuid.New().String(),
+ Type: domain.NotificationType(r.Type),
+ Priority: domain.Priority(r.Priority),
+ Status: domain.StatusPending,
+ Subject: r.Subject,
+ Body: r.Body,
+ Recipients: r.Recipients,
+ Metadata: r.Metadata,
+ CreatedAt: time.Now(),
+ ScheduledFor: r.ScheduledFor,
+ MaxRetries: maxRetries,
+ RetryCount: 0,
+ }
+}
+
+// SendNotificationResponse is the REST API response for sending a notification
+type SendNotificationResponse struct {
+ Result NotificationResult `json:"result"`
+}
+
+// SendBatchNotificationsRequest is the REST API request for sending multiple notifications
+type SendBatchNotificationsRequest struct {
+ Notifications []SendNotificationRequest `json:"notifications"`
+}
+
+// SendBatchNotificationsResponse is the REST API response for sending multiple notifications
+type SendBatchNotificationsResponse struct {
+ Results []NotificationResult `json:"results"`
+}
+
+// Notification represents a notification in the REST API
+type Notification struct {
+ ID string `json:"id"`
+ Type string `json:"type"`
+ Priority int `json:"priority"`
+ Status string `json:"status"`
+ Subject string `json:"subject"`
+ Body string `json:"body"`
+ Recipients []string `json:"recipients"`
+ Metadata map[string]interface{} `json:"metadata,omitempty"`
+ CreatedAt time.Time `json:"created_at"`
+ ScheduledFor *time.Time `json:"scheduled_for,omitempty"`
+ SentAt *time.Time `json:"sent_at,omitempty"`
+ RetryCount int `json:"retry_count"`
+ MaxRetries int `json:"max_retries"`
+ LastError string `json:"last_error,omitempty"`
+}
+
+// NotificationFromDomain converts a domain notification to API format
+func NotificationFromDomain(n *domain.Notification) Notification {
+ return Notification{
+ ID: n.ID,
+ Type: string(n.Type),
+ Priority: int(n.Priority),
+ Status: string(n.Status),
+ Subject: n.Subject,
+ Body: n.Body,
+ Recipients: n.Recipients,
+ Metadata: n.Metadata,
+ CreatedAt: n.CreatedAt,
+ ScheduledFor: n.ScheduledFor,
+ SentAt: n.SentAt,
+ RetryCount: n.RetryCount,
+ MaxRetries: n.MaxRetries,
+ LastError: n.LastError,
+ }
+}
+
+// NotificationResult represents the result of a notification operation
+type NotificationResult struct {
+ NotificationID string `json:"notification_id"`
+ Success bool `json:"success"`
+ Message string `json:"message,omitempty"`
+ Error string `json:"error,omitempty"`
+ SentAt time.Time `json:"sent_at"`
+ ProviderResponse map[string]interface{} `json:"provider_response,omitempty"`
+}
+
+// NotificationResultFromDomain converts a domain result to API format
+func NotificationResultFromDomain(r *domain.NotificationResult) NotificationResult {
+ return NotificationResult{
+ NotificationID: r.NotificationID,
+ Success: r.Success,
+ Message: r.Message,
+ Error: r.Error,
+ SentAt: r.SentAt,
+ ProviderResponse: r.ProviderResponse,
+ }
+}
+
+// ListNotificationsResponse is the REST API response for listing notifications
+type ListNotificationsResponse struct {
+ Notifications []Notification `json:"notifications"`
+ Total int64 `json:"total"`
+}
+
+// RetryNotificationResponse is the REST API response for retrying a notification
+type RetryNotificationResponse struct {
+ Result NotificationResult `json:"result"`
+}
diff --git a/bin/restserver b/bin/restserver
new file mode 100755
index 0000000..23ac1eb
Binary files /dev/null and b/bin/restserver differ
diff --git a/bin/server b/bin/server
new file mode 100755
index 0000000..271ba85
Binary files /dev/null and b/bin/server differ
diff --git a/cmd/restserver/main.go b/cmd/restserver/main.go
index e69de29..0e5aeb6 100644
--- a/cmd/restserver/main.go
+++ b/cmd/restserver/main.go
@@ -0,0 +1,174 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "net/http"
+ "os"
+ "os/signal"
+ "syscall"
+ "time"
+
+ "github.com/igodwin/notifier/api/rest"
+ "github.com/igodwin/notifier/internal/config"
+ "github.com/igodwin/notifier/internal/domain"
+ "github.com/igodwin/notifier/internal/notifier"
+ "github.com/igodwin/notifier/internal/queue"
+ "github.com/igodwin/notifier/internal/service"
+)
+
+func main() {
+ // Load configuration
+ cfg, err := config.Load("")
+ if err != nil {
+ log.Printf("Warning: failed to load config, using defaults: %v", err)
+ cfg = getDefaultConfig()
+ }
+
+ log.Printf("Starting Notifier REST Server on port %d", cfg.Server.RESTPort)
+
+ // Create context
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ // Initialize queue
+ var q domain.Queue
+ if cfg.Queue.Type == "local" {
+ q, err = queue.NewLocalQueue(cfg.Queue.Local)
+ if err != nil {
+ log.Fatalf("Failed to create queue: %v", err)
+ }
+ log.Println("Using local queue")
+ } else {
+ log.Fatalf("Queue type %s not implemented yet", cfg.Queue.Type)
+ }
+
+ // Initialize notifier factory
+ factory := notifier.NewFactory()
+
+ // Register notifiers based on configuration
+ if cfg.Notifiers.Stdout {
+ stdoutNotifier := notifier.NewStdoutNotifier()
+ if err := factory.RegisterNotifier(domain.TypeStdout, stdoutNotifier); err != nil {
+ log.Fatalf("Failed to register stdout notifier: %v", err)
+ }
+ log.Println("Registered stdout notifier")
+ }
+
+ if cfg.Notifiers.SMTP != nil {
+ smtpNotifier, err := notifier.NewSMTPNotifier(cfg.Notifiers.SMTP)
+ if err != nil {
+ log.Printf("Warning: failed to create SMTP notifier: %v", err)
+ } else {
+ if err := factory.RegisterNotifier(domain.TypeEmail, smtpNotifier); err != nil {
+ log.Fatalf("Failed to register SMTP notifier: %v", err)
+ }
+ log.Println("Registered SMTP notifier")
+ }
+ }
+
+ if cfg.Notifiers.Slack != nil {
+ slackNotifier, err := notifier.NewSlackNotifier(cfg.Notifiers.Slack)
+ if err != nil {
+ log.Printf("Warning: failed to create Slack notifier: %v", err)
+ } else {
+ if err := factory.RegisterNotifier(domain.TypeSlack, slackNotifier); err != nil {
+ log.Fatalf("Failed to register Slack notifier: %v", err)
+ }
+ log.Println("Registered Slack notifier")
+ }
+ }
+
+ if cfg.Notifiers.Ntfy != nil {
+ ntfyNotifier, err := notifier.NewNtfyNotifier(cfg.Notifiers.Ntfy)
+ if err != nil {
+ log.Printf("Warning: failed to create Ntfy notifier: %v", err)
+ } else {
+ if err := factory.RegisterNotifier(domain.TypeNtfy, ntfyNotifier); err != nil {
+ log.Fatalf("Failed to register Ntfy notifier: %v", err)
+ }
+ log.Println("Registered Ntfy notifier")
+ }
+ }
+
+ // Check if any notifiers are registered
+ if len(factory.SupportedTypes()) == 0 {
+ log.Fatal("No notifiers configured. Please enable at least one notifier in config.yaml")
+ }
+
+ log.Printf("Supported notification types: %v", factory.SupportedTypes())
+
+ // Create notification service
+ svc := service.NewNotificationService(factory, q, cfg.Queue.WorkerCount)
+
+ // Start workers
+ if err := svc.Start(ctx); err != nil {
+ log.Fatalf("Failed to start service: %v", err)
+ }
+ log.Printf("Started %d worker(s)", cfg.Queue.WorkerCount)
+
+ // Create REST router
+ router := rest.NewRouter(svc)
+
+ // Create HTTP server
+ addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.RESTPort)
+ server := &http.Server{
+ Addr: addr,
+ Handler: router,
+ ReadTimeout: 15 * time.Second,
+ WriteTimeout: 15 * time.Second,
+ IdleTimeout: 60 * time.Second,
+ }
+
+ // Start server in goroutine
+ go func() {
+ log.Printf("REST server listening on %s", addr)
+ if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
+ log.Fatalf("Failed to start server: %v", err)
+ }
+ }()
+
+ // Wait for interrupt signal
+ sigChan := make(chan os.Signal, 1)
+ signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
+ <-sigChan
+
+ log.Println("Shutting down server...")
+
+ // Graceful shutdown
+ shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer shutdownCancel()
+
+ if err := server.Shutdown(shutdownCtx); err != nil {
+ log.Printf("Error during server shutdown: %v", err)
+ }
+
+ // Stop service
+ if err := svc.Stop(); err != nil {
+ log.Printf("Error stopping service: %v", err)
+ }
+
+ log.Println("Server stopped")
+}
+
+// getDefaultConfig returns a minimal default configuration
+func getDefaultConfig() *config.Config {
+ return &config.Config{
+ Server: config.ServerConfig{
+ RESTPort: 8080,
+ Host: "0.0.0.0",
+ },
+ Queue: domain.QueueConfig{
+ Type: "local",
+ WorkerCount: 5,
+ RetryAttempts: 3,
+ Local: &domain.LocalQueueConfig{
+ BufferSize: 1000,
+ },
+ },
+ Notifiers: config.NotifiersConfig{
+ Stdout: true,
+ },
+ }
+}
diff --git a/cmd/server/main.go b/cmd/server/main.go
new file mode 100644
index 0000000..1a825b9
--- /dev/null
+++ b/cmd/server/main.go
@@ -0,0 +1,238 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "log"
+ "net"
+ "net/http"
+ "os"
+ "os/signal"
+ "sync"
+ "syscall"
+ "time"
+
+ "github.com/igodwin/notifier/api/rest"
+ "github.com/igodwin/notifier/internal/config"
+ "github.com/igodwin/notifier/internal/domain"
+ "github.com/igodwin/notifier/internal/notifier"
+ "github.com/igodwin/notifier/internal/queue"
+ "github.com/igodwin/notifier/internal/service"
+ "google.golang.org/grpc"
+ "google.golang.org/grpc/reflection"
+)
+
+func main() {
+ // Load configuration
+ cfg, err := config.Load("")
+ if err != nil {
+ log.Printf("Warning: failed to load config, using defaults: %v", err)
+ cfg = getDefaultConfig()
+ }
+
+ log.Printf("Starting Notifier Service in mode: %s", cfg.Server.Mode)
+
+ // Create context
+ ctx, cancel := context.WithCancel(context.Background())
+ defer cancel()
+
+ // Initialize queue
+ var q domain.Queue
+ if cfg.Queue.Type == "local" {
+ q, err = queue.NewLocalQueue(cfg.Queue.Local)
+ if err != nil {
+ log.Fatalf("Failed to create queue: %v", err)
+ }
+ log.Println("Using local queue")
+ } else {
+ log.Fatalf("Queue type %s not implemented yet", cfg.Queue.Type)
+ }
+
+ // Initialize notifier factory and register notifiers
+ factory := notifier.NewFactory()
+ registerNotifiers(cfg, factory)
+
+ // Check if any notifiers are registered
+ if len(factory.SupportedTypes()) == 0 {
+ log.Fatal("No notifiers configured. Please enable at least one notifier in config.yaml")
+ }
+
+ log.Printf("Supported notification types: %v", factory.SupportedTypes())
+
+ // Create notification service
+ svc := service.NewNotificationService(factory, q, cfg.Queue.WorkerCount)
+
+ // Start workers
+ if err := svc.Start(ctx); err != nil {
+ log.Fatalf("Failed to start service: %v", err)
+ }
+ log.Printf("Started %d worker(s)", cfg.Queue.WorkerCount)
+
+ // Wait group for both servers
+ var wg sync.WaitGroup
+
+ // Start gRPC server if enabled
+ var grpcServer *grpc.Server
+ if cfg.Server.Mode == "both" || cfg.Server.Mode == "grpc" {
+ wg.Add(1)
+ grpcServer = startGRPCServer(ctx, &wg, cfg, svc)
+ }
+
+ // Start REST server if enabled
+ var restServer *http.Server
+ if cfg.Server.Mode == "both" || cfg.Server.Mode == "rest" {
+ wg.Add(1)
+ restServer = startRESTServer(ctx, &wg, cfg, svc)
+ }
+
+ // Wait for interrupt signal
+ sigChan := make(chan os.Signal, 1)
+ signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM)
+ <-sigChan
+
+ log.Println("Shutting down servers...")
+
+ // Graceful shutdown
+ shutdownCtx, shutdownCancel := context.WithTimeout(context.Background(), 30*time.Second)
+ defer shutdownCancel()
+
+ // Stop REST server
+ if restServer != nil {
+ if err := restServer.Shutdown(shutdownCtx); err != nil {
+ log.Printf("Error during REST server shutdown: %v", err)
+ }
+ }
+
+ // Stop gRPC server
+ if grpcServer != nil {
+ grpcServer.GracefulStop()
+ }
+
+ // Wait for servers to stop
+ wg.Wait()
+
+ // Stop service
+ if err := svc.Stop(); err != nil {
+ log.Printf("Error stopping service: %v", err)
+ }
+
+ log.Println("Servers stopped")
+}
+
+func registerNotifiers(cfg *config.Config, factory *notifier.Factory) {
+ if cfg.Notifiers.Stdout {
+ stdoutNotifier := notifier.NewStdoutNotifier()
+ if err := factory.RegisterNotifier(domain.TypeStdout, stdoutNotifier); err != nil {
+ log.Fatalf("Failed to register stdout notifier: %v", err)
+ }
+ log.Println("Registered stdout notifier")
+ }
+
+ if cfg.Notifiers.SMTP != nil {
+ smtpNotifier, err := notifier.NewSMTPNotifier(cfg.Notifiers.SMTP)
+ if err != nil {
+ log.Printf("Warning: failed to create SMTP notifier: %v", err)
+ } else {
+ if err := factory.RegisterNotifier(domain.TypeEmail, smtpNotifier); err != nil {
+ log.Fatalf("Failed to register SMTP notifier: %v", err)
+ }
+ log.Println("Registered SMTP notifier")
+ }
+ }
+
+ if cfg.Notifiers.Slack != nil {
+ slackNotifier, err := notifier.NewSlackNotifier(cfg.Notifiers.Slack)
+ if err != nil {
+ log.Printf("Warning: failed to create Slack notifier: %v", err)
+ } else {
+ if err := factory.RegisterNotifier(domain.TypeSlack, slackNotifier); err != nil {
+ log.Fatalf("Failed to register Slack notifier: %v", err)
+ }
+ log.Println("Registered Slack notifier")
+ }
+ }
+
+ if cfg.Notifiers.Ntfy != nil {
+ ntfyNotifier, err := notifier.NewNtfyNotifier(cfg.Notifiers.Ntfy)
+ if err != nil {
+ log.Printf("Warning: failed to create Ntfy notifier: %v", err)
+ } else {
+ if err := factory.RegisterNotifier(domain.TypeNtfy, ntfyNotifier); err != nil {
+ log.Fatalf("Failed to register Ntfy notifier: %v", err)
+ }
+ log.Println("Registered Ntfy notifier")
+ }
+ }
+}
+
+func startGRPCServer(ctx context.Context, wg *sync.WaitGroup, cfg *config.Config, svc domain.NotificationService) *grpc.Server {
+ addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.GRPCPort)
+
+ lis, err := net.Listen("tcp", addr)
+ if err != nil {
+ log.Fatalf("Failed to listen on %s: %v", addr, err)
+ }
+
+ grpcServer := grpc.NewServer()
+
+ // TODO: Register gRPC service implementation when protobuf is generated
+ // pb.RegisterNotifierServiceServer(grpcServer, grpcHandler)
+
+ // Enable reflection for tools like grpcurl
+ reflection.Register(grpcServer)
+
+ go func() {
+ defer wg.Done()
+ log.Printf("gRPC server listening on %s", addr)
+ if err := grpcServer.Serve(lis); err != nil {
+ log.Fatalf("Failed to serve gRPC: %v", err)
+ }
+ }()
+
+ return grpcServer
+}
+
+func startRESTServer(ctx context.Context, wg *sync.WaitGroup, cfg *config.Config, svc domain.NotificationService) *http.Server {
+ router := rest.NewRouter(svc)
+
+ addr := fmt.Sprintf("%s:%d", cfg.Server.Host, cfg.Server.RESTPort)
+ server := &http.Server{
+ Addr: addr,
+ Handler: router,
+ ReadTimeout: 15 * time.Second,
+ WriteTimeout: 15 * time.Second,
+ IdleTimeout: 60 * time.Second,
+ }
+
+ go func() {
+ defer wg.Done()
+ log.Printf("REST server listening on %s", addr)
+ if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
+ log.Fatalf("Failed to start REST server: %v", err)
+ }
+ }()
+
+ return server
+}
+
+func getDefaultConfig() *config.Config {
+ return &config.Config{
+ Server: config.ServerConfig{
+ GRPCPort: 50051,
+ RESTPort: 8080,
+ Host: "0.0.0.0",
+ Mode: "both",
+ },
+ Queue: domain.QueueConfig{
+ Type: "local",
+ WorkerCount: 5,
+ RetryAttempts: 3,
+ Local: &domain.LocalQueueConfig{
+ BufferSize: 1000,
+ },
+ },
+ Notifiers: config.NotifiersConfig{
+ Stdout: true,
+ },
+ }
+}
diff --git a/config.yaml b/config.yaml
new file mode 100644
index 0000000..b7e741a
--- /dev/null
+++ b/config.yaml
@@ -0,0 +1,91 @@
+# Notifier Service Configuration
+
+server:
+ grpc_port: 50051
+ rest_port: 8080
+ host: "0.0.0.0"
+ mode: "both" # Options: both, grpc, rest
+
+queue:
+ type: "local" # Options: local, kafka
+ max_size: 10000
+ worker_count: 10
+ retry_attempts: 3
+ retry_backoff: "exponential" # Options: exponential, linear, fixed
+
+ # Local queue configuration
+ local:
+ buffer_size: 1000
+ persist_to_disk: false
+ persist_path: "/var/lib/notifier/queue.json"
+
+ # Kafka queue configuration (when type: kafka)
+ # kafka:
+ # brokers:
+ # - "localhost:9092"
+ # topic: "notifications"
+ # consumer_group: "notifier-service"
+ # partition_count: 10
+ # replication_factor: 3
+ # enable_idempotence: true
+ # compression_type: "snappy"
+
+notifiers:
+ # Enable stdout notifier (useful for development/debugging)
+ stdout: true
+
+ # SMTP email configuration
+ smtp:
+ host: "smtp.gmail.com"
+ port: 587
+ username: "your-email@gmail.com"
+ password: "your-app-password"
+ from: "notifications@yourservice.com"
+ use_tls: true
+
+ # Slack configuration
+ slack:
+ webhook_url: "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
+ # token: "xoxb-your-bot-token" # Alternative to webhook
+ # channel: "#notifications" # Default channel
+ username: "Notifier Bot"
+ icon_emoji: ":bell:"
+ # Channel-specific webhooks
+ # webhooks:
+ # "#alerts": "https://hooks.slack.com/services/ALERTS/WEBHOOK"
+ # "#monitoring": "https://hooks.slack.com/services/MONITORING/WEBHOOK"
+
+ # Ntfy configuration
+ ntfy:
+ server_url: "https://ntfy.sh"
+
+ # Token-based authentication (recommended for ntfy.sh)
+ # Supports both access tokens (tk_...) and publish tokens
+ # token: "tk_your_access_token"
+
+ # Basic authentication (alternative to token)
+ # username: "your-username"
+ # password: "your-password"
+
+ # Default topic if recipient not specified
+ # default_topic: "default-notifications"
+
+ # Skip TLS verification (for self-hosted servers with self-signed certs)
+ # insecure_skip_verify: false
+
+logging:
+ level: "info" # Options: debug, info, warn, error
+ format: "json" # Options: json, text
+ output_path: "stdout" # Options: stdout, stderr, or file path
+
+metrics:
+ enabled: true
+ port: 9090
+ path: "/metrics"
+ prometheus_enabled: true
+
+health_check:
+ enabled: true
+ port: 8081
+ path: "/health"
+ interval: 30 # seconds
diff --git a/docker-compose.yaml b/docker-compose.yaml
new file mode 100644
index 0000000..6a50503
--- /dev/null
+++ b/docker-compose.yaml
@@ -0,0 +1,95 @@
+version: '3.8'
+
+services:
+ notifier:
+ build:
+ context: .
+ dockerfile: Dockerfile
+ container_name: notifier
+ ports:
+ - "8080:8080" # REST API
+ - "50051:50051" # gRPC
+ - "9090:9090" # Metrics
+ - "8081:8081" # Health check
+ volumes:
+ - ./config.yaml:/app/config.yaml:ro
+ - notifier-data:/var/lib/notifier
+ environment:
+ - NOTIFIER_SERVER_MODE=both
+ - NOTIFIER_LOGGING_LEVEL=info
+ - NOTIFIER_LOGGING_FORMAT=json
+ restart: unless-stopped
+ networks:
+ - notifier-net
+ healthcheck:
+ test: ["CMD", "wget", "--quiet", "--tries=1", "--spider", "http://localhost:8081/health"]
+ interval: 30s
+ timeout: 10s
+ retries: 3
+ start_period: 10s
+
+ # Optional: Kafka for distributed queue
+ # kafka:
+ # image: confluentinc/cp-kafka:latest
+ # container_name: kafka
+ # ports:
+ # - "9092:9092"
+ # environment:
+ # KAFKA_BROKER_ID: 1
+ # KAFKA_ZOOKEEPER_CONNECT: zookeeper:2181
+ # KAFKA_ADVERTISED_LISTENERS: PLAINTEXT://kafka:9092
+ # KAFKA_OFFSETS_TOPIC_REPLICATION_FACTOR: 1
+ # depends_on:
+ # - zookeeper
+ # networks:
+ # - notifier-net
+
+ # zookeeper:
+ # image: confluentinc/cp-zookeeper:latest
+ # container_name: zookeeper
+ # ports:
+ # - "2181:2181"
+ # environment:
+ # ZOOKEEPER_CLIENT_PORT: 2181
+ # ZOOKEEPER_TICK_TIME: 2000
+ # networks:
+ # - notifier-net
+
+ # Optional: Prometheus for metrics collection
+ # prometheus:
+ # image: prom/prometheus:latest
+ # container_name: prometheus
+ # ports:
+ # - "9091:9090"
+ # volumes:
+ # - ./prometheus.yml:/etc/prometheus/prometheus.yml:ro
+ # - prometheus-data:/prometheus
+ # command:
+ # - '--config.file=/etc/prometheus/prometheus.yml'
+ # - '--storage.tsdb.path=/prometheus'
+ # networks:
+ # - notifier-net
+
+ # Optional: Grafana for visualization
+ # grafana:
+ # image: grafana/grafana:latest
+ # container_name: grafana
+ # ports:
+ # - "3000:3000"
+ # environment:
+ # - GF_SECURITY_ADMIN_PASSWORD=admin
+ # volumes:
+ # - grafana-data:/var/lib/grafana
+ # depends_on:
+ # - prometheus
+ # networks:
+ # - notifier-net
+
+networks:
+ notifier-net:
+ driver: bridge
+
+volumes:
+ notifier-data:
+ # prometheus-data:
+ # grafana-data:
diff --git a/docs/NTFY_GUIDE.md b/docs/NTFY_GUIDE.md
new file mode 100644
index 0000000..8b69e53
--- /dev/null
+++ b/docs/NTFY_GUIDE.md
@@ -0,0 +1,479 @@
+# Ntfy Integration Guide
+
+This guide explains how to use ntfy.sh notifications with the Notifier service.
+
+## What is Ntfy?
+
+[ntfy](https://ntfy.sh) is a simple HTTP-based pub-sub notification service. You can send notifications to your phone, desktop, or any device that subscribes to your topics. It's perfect for:
+
+- Push notifications to mobile devices
+- Desktop notifications
+- Server alerts and monitoring
+- CI/CD pipeline notifications
+- IoT device notifications
+
+## Authentication Methods
+
+### 1. Token-Based Authentication (Recommended)
+
+Token authentication is the preferred method for ntfy.sh.
+
+#### Access Tokens (for authenticated topics)
+```yaml
+notifiers:
+ ntfy:
+ server_url: "https://ntfy.sh"
+ token: "tk_your_access_token"
+```
+
+Get an access token:
+1. Go to https://ntfy.sh/account
+2. Create an account or log in
+3. Go to "Access Tokens"
+4. Create a new token with appropriate permissions
+5. Copy the token (starts with `tk_`)
+
+#### Publish Tokens (for specific topics)
+```yaml
+notifiers:
+ ntfy:
+ server_url: "https://ntfy.sh"
+ token: "your_publish_token"
+```
+
+Create a publish token:
+1. Create a topic with reserved access
+2. Generate a publish token for that topic
+3. Use the token in your configuration
+
+### 2. Basic Authentication
+
+Alternative to token auth:
+
+```yaml
+notifiers:
+ ntfy:
+ server_url: "https://ntfy.sh"
+ username: "your-username"
+ password: "your-password"
+```
+
+### 3. No Authentication (Public Topics)
+
+For public topics on ntfy.sh:
+
+```yaml
+notifiers:
+ ntfy:
+ server_url: "https://ntfy.sh"
+ # No token, username, or password needed
+```
+
+## Configuration Options
+
+### Full Configuration Example
+
+```yaml
+notifiers:
+ ntfy:
+ # Server URL (default: https://ntfy.sh)
+ server_url: "https://ntfy.sh"
+
+ # Authentication (choose one method)
+ token: "tk_your_token" # Token auth (recommended)
+ # username: "user" # Or basic auth
+ # password: "pass"
+
+ # Optional: default topic if not specified in notification
+ default_topic: "my-default-topic"
+
+ # Optional: skip TLS verification (for self-hosted with self-signed certs)
+ insecure_skip_verify: false
+```
+
+### Self-Hosted Ntfy Server
+
+```yaml
+notifiers:
+ ntfy:
+ server_url: "https://ntfy.yourcompany.com"
+ token: "your_custom_token"
+ # For self-signed certificates
+ insecure_skip_verify: true
+```
+
+## Sending Notifications
+
+### Basic Notification
+
+```bash
+curl -X POST http://localhost:8080/api/v1/notifications \
+ -H "Content-Type: application/json" \
+ -d '{
+ "type": "ntfy",
+ "subject": "Hello from Notifier!",
+ "body": "This is a test notification",
+ "recipients": ["my-topic"]
+ }'
+```
+
+### With Priority
+
+```bash
+curl -X POST http://localhost:8080/api/v1/notifications \
+ -H "Content-Type: application/json" \
+ -d '{
+ "type": "ntfy",
+ "priority": 4,
+ "subject": "CRITICAL Alert",
+ "body": "Something important happened!",
+ "recipients": ["alerts"]
+ }'
+```
+
+Priority mapping:
+- `0` (Low) → ntfy priority 2
+- `1` (Normal) → ntfy priority 3 (default)
+- `2` (High) → ntfy priority 4
+- `3` (Critical) → ntfy priority 5 (max)
+
+### With Tags (Emojis)
+
+```bash
+curl -X POST http://localhost:8080/api/v1/notifications \
+ -H "Content-Type: application/json" \
+ -d '{
+ "type": "ntfy",
+ "subject": "Deployment Complete",
+ "body": "Application deployed successfully",
+ "recipients": ["deployments"],
+ "metadata": {
+ "tags": ["rocket", "tada", "white_check_mark"]
+ }
+ }'
+```
+
+Common tags:
+- `warning`, `rotating_light`, `skull` - Alerts
+- `tada`, `rocket`, `sparkles` - Success
+- `x`, `no_entry`, `stop_sign` - Errors
+- `information_source`, `eyes` - Info
+
+### With Click Action
+
+Make the notification clickable:
+
+```bash
+curl -X POST http://localhost:8080/api/v1/notifications \
+ -H "Content-Type: application/json" \
+ -d '{
+ "type": "ntfy",
+ "subject": "New Pull Request",
+ "body": "PR #123 needs review",
+ "recipients": ["github-notifications"],
+ "metadata": {
+ "click": "https://github.com/your-org/your-repo/pull/123",
+ "tags": ["github"]
+ }
+ }'
+```
+
+### With Attachment
+
+Attach an image or file:
+
+```bash
+curl -X POST http://localhost:8080/api/v1/notifications \
+ -H "Content-Type: application/json" \
+ -d '{
+ "type": "ntfy",
+ "subject": "Server Stats",
+ "body": "Current server metrics",
+ "recipients": ["monitoring"],
+ "metadata": {
+ "attach": "https://example.com/metrics.png",
+ "tags": ["chart_with_upwards_trend"]
+ }
+ }'
+```
+
+### With Custom Icon
+
+```bash
+curl -X POST http://localhost:8080/api/v1/notifications \
+ -H "Content-Type: application/json" \
+ -d '{
+ "type": "ntfy",
+ "subject": "Custom Notification",
+ "body": "With a custom icon",
+ "recipients": ["custom-alerts"],
+ "metadata": {
+ "icon": "https://example.com/logo.png"
+ }
+ }'
+```
+
+### With Delayed Delivery
+
+Schedule notification for later:
+
+```bash
+curl -X POST http://localhost:8080/api/v1/notifications \
+ -H "Content-Type: application/json" \
+ -d '{
+ "type": "ntfy",
+ "subject": "Reminder",
+ "body": "Meeting in 30 minutes",
+ "recipients": ["reminders"],
+ "metadata": {
+ "delay": "30m",
+ "tags": ["alarm_clock"]
+ }
+ }'
+```
+
+Delay formats:
+- `30s` - 30 seconds
+- `5m` - 5 minutes
+- `2h` - 2 hours
+- `tomorrow 10am` - Tomorrow at 10 AM
+
+### With Action Buttons
+
+Add interactive buttons:
+
+```bash
+curl -X POST http://localhost:8080/api/v1/notifications \
+ -H "Content-Type: application/json" \
+ -d '{
+ "type": "ntfy",
+ "subject": "Deploy to Production?",
+ "body": "Version 2.0 is ready",
+ "recipients": ["deployments"],
+ "metadata": {
+ "actions": [
+ {
+ "action": "view",
+ "label": "View Release",
+ "url": "https://github.com/your-org/your-repo/releases/v2.0",
+ "clear": true
+ },
+ {
+ "action": "http",
+ "label": "Deploy",
+ "url": "https://api.yourcompany.com/deploy",
+ "body": "{\"version\": \"2.0\"}",
+ "clear": true
+ }
+ ]
+ }
+ }'
+```
+
+Action types:
+- `view` - Open a URL
+- `http` - Send HTTP request
+- `broadcast` - Android broadcast intent
+
+### With Email Forwarding
+
+Forward to email (requires ntfy.sh tier 2+):
+
+```bash
+curl -X POST http://localhost:8080/api/v1/notifications \
+ -H "Content-Type: application/json" \
+ -d '{
+ "type": "ntfy",
+ "subject": "Important Alert",
+ "body": "This will also be sent via email",
+ "recipients": ["alerts"],
+ "metadata": {
+ "email": "admin@example.com",
+ "tags": ["email"]
+ }
+ }'
+```
+
+### Multiple Topics
+
+Send to multiple topics:
+
+```bash
+curl -X POST http://localhost:8080/api/v1/notifications \
+ -H "Content-Type: application/json" \
+ -d '{
+ "type": "ntfy",
+ "subject": "System Update",
+ "body": "System will restart in 5 minutes",
+ "recipients": ["admins", "monitoring", "alerts"],
+ "metadata": {
+ "tags": ["warning"]
+ }
+ }'
+```
+
+## Mobile App Setup
+
+### iOS
+1. Download ntfy from the App Store
+2. Add a topic subscription
+3. Use the same topic name in your notifications
+
+### Android
+1. Download ntfy from Google Play or F-Droid
+2. Add a topic subscription
+3. Configure notification settings
+4. Use the same topic name in your notifications
+
+### Desktop (Linux/macOS/Windows)
+```bash
+# Install ntfy CLI
+curl -sSL https://ntfy.sh/install.sh | sh
+
+# Subscribe to topics
+ntfy subscribe mytopic
+```
+
+## Topic Naming Best Practices
+
+### Public Topics
+- Use unique, hard-to-guess names
+- Consider including random strings: `myapp-alerts-x7k9p2`
+- Anyone who knows the name can subscribe
+
+### Private Topics (Recommended)
+- Requires authentication
+- Create reserved topics on ntfy.sh
+- Use access control lists (ACLs)
+
+### Topic Organization
+```yaml
+# Example topic structure
+- myapp-prod-alerts # Production alerts
+- myapp-prod-info # Production info
+- myapp-staging-alerts # Staging alerts
+- myapp-ci-cd # CI/CD notifications
+- myapp-monitoring # Monitoring metrics
+```
+
+## Environment Variables
+
+Override configuration with environment variables:
+
+```bash
+export NOTIFIER_NOTIFIERS_NTFY_SERVER_URL=https://ntfy.yourcompany.com
+export NOTIFIER_NOTIFIERS_NTFY_TOKEN=tk_your_token
+export NOTIFIER_NOTIFIERS_NTFY_DEFAULT_TOPIC=default-topic
+export NOTIFIER_NOTIFIERS_NTFY_INSECURE_SKIP_VERIFY=false
+```
+
+## Security Considerations
+
+### Token Security
+- **Never commit tokens to version control**
+- Store tokens in Kubernetes Secrets or environment variables
+- Rotate tokens regularly
+- Use publish tokens with limited scope when possible
+
+### Topic Security
+- Use reserved/private topics for sensitive data
+- Don't include secrets in notification bodies
+- Consider encryption for highly sensitive data
+- Use unique topic names to prevent enumeration
+
+### Self-Hosted Servers
+- Use TLS with valid certificates
+- Enable authentication
+- Configure rate limiting
+- Monitor access logs
+- Keep ntfy server updated
+
+## Kubernetes Deployment
+
+### Using Secrets
+
+```yaml
+apiVersion: v1
+kind: Secret
+metadata:
+ name: notifier-secrets
+type: Opaque
+stringData:
+ ntfy-token: "tk_your_access_token"
+```
+
+### Deployment Configuration
+
+```yaml
+env:
+- name: NOTIFIER_NOTIFIERS_NTFY_TOKEN
+ valueFrom:
+ secretKeyRef:
+ name: notifier-secrets
+ key: ntfy-token
+```
+
+## Rate Limits
+
+### ntfy.sh Free Tier
+- 250 messages/day per visitor
+- Unlimited topics
+- Message retention: 12 hours
+
+### ntfy.sh Tier 1 ($5/month)
+- 500 messages/day
+- Message retention: 1 day
+- Attachment & email support
+
+### ntfy.sh Tier 2 ($10/month)
+- 1000 messages/day
+- Message retention: 7 days
+- Higher attachment limits
+
+### Self-Hosted
+- Configure your own limits
+- Full control over retention
+- No external dependencies
+
+## Troubleshooting
+
+### Authentication Errors
+```
+Error: ntfy server returned status: 401
+```
+**Solution**: Verify token is correct and has necessary permissions
+
+### Topic Not Found
+```
+Error: ntfy server returned status: 404
+```
+**Solution**: Check topic name spelling, ensure topic exists if using reserved topics
+
+### TLS Errors (Self-Hosted)
+```
+Error: x509: certificate signed by unknown authority
+```
+**Solution**: Either fix certificate or set `insecure_skip_verify: true` (not recommended for production)
+
+### Rate Limit Exceeded
+```
+Error: ntfy server returned status: 429
+```
+**Solution**: Reduce message frequency or upgrade ntfy.sh tier
+
+### Connection Timeout
+```
+Error: context deadline exceeded
+```
+**Solution**: Check network connectivity, firewall rules, or server URL
+
+## Examples
+
+See [QUICKSTART.md](../QUICKSTART.md) for more examples of using ntfy with the Notifier service.
+
+## Resources
+
+- [Ntfy Documentation](https://docs.ntfy.sh)
+- [Ntfy.sh Public Instance](https://ntfy.sh)
+- [Self-Hosting Guide](https://docs.ntfy.sh/install/)
+- [API Reference](https://docs.ntfy.sh/publish/)
diff --git a/go.mod b/go.mod
index 95ffb12..3d2a2a6 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,36 @@
module github.com/igodwin/notifier
go 1.23.2
+
+require (
+ github.com/google/uuid v1.6.0
+ github.com/gorilla/mux v1.8.1
+ github.com/spf13/viper v1.19.0
+ google.golang.org/grpc v1.62.1
+ google.golang.org/protobuf v1.33.0
+)
+
+require (
+ github.com/fsnotify/fsnotify v1.7.0 // indirect
+ github.com/golang/protobuf v1.5.3 // indirect
+ github.com/hashicorp/hcl v1.0.0 // indirect
+ github.com/magiconair/properties v1.8.7 // indirect
+ github.com/mitchellh/mapstructure v1.5.0 // indirect
+ github.com/pelletier/go-toml/v2 v2.2.2 // indirect
+ github.com/sagikazarmark/locafero v0.4.0 // indirect
+ github.com/sagikazarmark/slog-shim v0.1.0 // indirect
+ github.com/sourcegraph/conc v0.3.0 // indirect
+ github.com/spf13/afero v1.11.0 // indirect
+ github.com/spf13/cast v1.6.0 // indirect
+ github.com/spf13/pflag v1.0.5 // indirect
+ github.com/subosito/gotenv v1.6.0 // indirect
+ go.uber.org/atomic v1.9.0 // indirect
+ go.uber.org/multierr v1.9.0 // indirect
+ golang.org/x/exp v0.0.0-20230905200255-921286631fa9 // indirect
+ golang.org/x/net v0.23.0 // indirect
+ golang.org/x/sys v0.24.0 // indirect
+ golang.org/x/text v0.17.0 // indirect
+ google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c // indirect
+ gopkg.in/ini.v1 v1.67.0 // indirect
+ gopkg.in/yaml.v3 v3.0.1 // indirect
+)
diff --git a/go.sum b/go.sum
new file mode 100644
index 0000000..8c0b93b
--- /dev/null
+++ b/go.sum
@@ -0,0 +1,90 @@
+github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
+github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
+github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
+github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
+github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
+github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
+github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
+github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
+github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
+github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
+github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
+github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
+github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
+github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
+github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
+github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
+github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
+github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
+github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
+github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
+github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
+github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
+github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
+github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
+github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
+github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
+github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
+github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
+github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
+github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
+github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
+github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
+github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
+github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
+github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
+github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
+github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
+github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
+github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
+github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
+github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
+github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
+github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
+github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
+github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
+github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
+github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
+github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
+github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
+github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
+go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
+go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
+go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
+go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
+golang.org/x/exp v0.0.0-20230905200255-921286631fa9 h1:GoHiUyI/Tp2nVkLI2mCxVkOjsbSXD66ic0XW0js0R9g=
+golang.org/x/exp v0.0.0-20230905200255-921286631fa9/go.mod h1:S2oDrQGGwySpoQPVqRShND87VCbxmc6bL1Yd2oYrm6k=
+golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
+golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
+golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg=
+golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/text v0.17.0 h1:XtiM5bkSOt+ewxlOE/aE/AKEHibwj/6gvWMl9Rsh0Qc=
+golang.org/x/text v0.17.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
+golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c h1:lfpJ/2rWPa/kJgxyyXM8PrNnfCzcmxJ265mADgwmvLI=
+google.golang.org/genproto/googleapis/rpc v0.0.0-20240314234333-6e1732d8331c/go.mod h1:WtryC6hu0hhx87FDGxWCDptyssuo68sk10vYjF+T9fY=
+google.golang.org/grpc v1.62.1 h1:B4n+nfKzOICUXMgyrNd19h/I9oH0L1pizfk1d4zSgTk=
+google.golang.org/grpc v1.62.1/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
+google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
+google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
+google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
+google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 h1:YR8cESwS4TdDjEe65xsg0ogRM/Nc3DYOhEAlW+xobZo=
+gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
+gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
+gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
+gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
+gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/internal/config/config.go b/internal/config/config.go
index e69de29..b7d38ae 100644
--- a/internal/config/config.go
+++ b/internal/config/config.go
@@ -0,0 +1,211 @@
+package config
+
+import (
+ "fmt"
+ "strings"
+
+ "github.com/igodwin/notifier/internal/domain"
+ "github.com/igodwin/notifier/internal/notifier"
+ "github.com/spf13/viper"
+)
+
+// Config represents the application configuration
+type Config struct {
+ Server ServerConfig `mapstructure:"server"`
+ Queue domain.QueueConfig `mapstructure:"queue"`
+ Notifiers NotifiersConfig `mapstructure:"notifiers"`
+ Logging LoggingConfig `mapstructure:"logging"`
+ Metrics MetricsConfig `mapstructure:"metrics"`
+ HealthCheck HealthCheckConfig `mapstructure:"health_check"`
+}
+
+// ServerConfig contains server configuration
+type ServerConfig struct {
+ GRPCPort int `mapstructure:"grpc_port"`
+ RESTPort int `mapstructure:"rest_port"`
+ Host string `mapstructure:"host"`
+ Mode string `mapstructure:"mode"` // "both", "grpc", "rest"
+}
+
+// NotifiersConfig contains configuration for all notifier types
+type NotifiersConfig struct {
+ SMTP *notifier.SMTPConfig `mapstructure:"smtp"`
+ Slack *notifier.SlackConfig `mapstructure:"slack"`
+ Ntfy *notifier.NtfyConfig `mapstructure:"ntfy"`
+ Stdout bool `mapstructure:"stdout"` // Enable stdout notifier
+}
+
+// LoggingConfig contains logging configuration
+type LoggingConfig struct {
+ Level string `mapstructure:"level"` // debug, info, warn, error
+ Format string `mapstructure:"format"` // json, text
+ OutputPath string `mapstructure:"output_path"` // stdout, stderr, or file path
+}
+
+// MetricsConfig contains metrics/observability configuration
+type MetricsConfig struct {
+ Enabled bool `mapstructure:"enabled"`
+ Port int `mapstructure:"port"`
+ Path string `mapstructure:"path"`
+ PrometheusEnabled bool `mapstructure:"prometheus_enabled"`
+}
+
+// HealthCheckConfig contains health check configuration
+type HealthCheckConfig struct {
+ Enabled bool `mapstructure:"enabled"`
+ Port int `mapstructure:"port"`
+ Path string `mapstructure:"path"`
+ Interval int `mapstructure:"interval"` // seconds
+}
+
+// Load loads configuration from file and environment variables
+func Load(configPath string) (*Config, error) {
+ v := viper.New()
+
+ // Set default values
+ setDefaults(v)
+
+ // Configure viper
+ v.SetConfigName("config")
+ v.SetConfigType("yaml")
+
+ if configPath != "" {
+ v.AddConfigPath(configPath)
+ }
+
+ // Also look in common locations
+ v.AddConfigPath(".")
+ v.AddConfigPath("./config")
+ v.AddConfigPath("/etc/notifier")
+ v.AddConfigPath("$HOME/.notifier")
+
+ // Environment variable support
+ v.SetEnvPrefix("NOTIFIER")
+ v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
+ v.AutomaticEnv()
+
+ // Read config file
+ if err := v.ReadInConfig(); err != nil {
+ // Config file is optional if environment variables are set
+ if _, ok := err.(viper.ConfigFileNotFoundError); !ok {
+ return nil, fmt.Errorf("failed to read config file: %w", err)
+ }
+ }
+
+ var config Config
+ if err := v.Unmarshal(&config); err != nil {
+ return nil, fmt.Errorf("failed to unmarshal config: %w", err)
+ }
+
+ // Validate configuration
+ if err := config.Validate(); err != nil {
+ return nil, fmt.Errorf("invalid configuration: %w", err)
+ }
+
+ return &config, nil
+}
+
+// setDefaults sets default configuration values
+func setDefaults(v *viper.Viper) {
+ // Server defaults
+ v.SetDefault("server.grpc_port", 50051)
+ v.SetDefault("server.rest_port", 8080)
+ v.SetDefault("server.host", "0.0.0.0")
+ v.SetDefault("server.mode", "both")
+
+ // Queue defaults
+ v.SetDefault("queue.type", "local")
+ v.SetDefault("queue.max_size", 10000)
+ v.SetDefault("queue.worker_count", 10)
+ v.SetDefault("queue.retry_attempts", 3)
+ v.SetDefault("queue.retry_backoff", "exponential")
+
+ // Local queue defaults
+ v.SetDefault("queue.local.buffer_size", 1000)
+ v.SetDefault("queue.local.persist_to_disk", false)
+
+ // Logging defaults
+ v.SetDefault("logging.level", "info")
+ v.SetDefault("logging.format", "json")
+ v.SetDefault("logging.output_path", "stdout")
+
+ // Metrics defaults
+ v.SetDefault("metrics.enabled", true)
+ v.SetDefault("metrics.port", 9090)
+ v.SetDefault("metrics.path", "/metrics")
+ v.SetDefault("metrics.prometheus_enabled", true)
+
+ // Health check defaults
+ v.SetDefault("health_check.enabled", true)
+ v.SetDefault("health_check.port", 8081)
+ v.SetDefault("health_check.path", "/health")
+ v.SetDefault("health_check.interval", 30)
+
+ // Notifier defaults
+ v.SetDefault("notifiers.stdout", true)
+ v.SetDefault("notifiers.smtp.port", 587)
+ v.SetDefault("notifiers.smtp.use_tls", true)
+ v.SetDefault("notifiers.ntfy.server_url", "https://ntfy.sh")
+}
+
+// Validate validates the configuration
+func (c *Config) Validate() error {
+ // Validate server config
+ if c.Server.GRPCPort < 1 || c.Server.GRPCPort > 65535 {
+ return fmt.Errorf("invalid gRPC port: %d", c.Server.GRPCPort)
+ }
+
+ if c.Server.RESTPort < 1 || c.Server.RESTPort > 65535 {
+ return fmt.Errorf("invalid REST port: %d", c.Server.RESTPort)
+ }
+
+ validModes := map[string]bool{"both": true, "grpc": true, "rest": true}
+ if !validModes[c.Server.Mode] {
+ return fmt.Errorf("invalid server mode: %s (must be both, grpc, or rest)", c.Server.Mode)
+ }
+
+ // Validate queue config
+ validQueueTypes := map[string]bool{"local": true, "kafka": true}
+ if !validQueueTypes[c.Queue.Type] {
+ return fmt.Errorf("invalid queue type: %s (must be local or kafka)", c.Queue.Type)
+ }
+
+ if c.Queue.Type == "kafka" && c.Queue.Kafka == nil {
+ return fmt.Errorf("Kafka queue type selected but no Kafka configuration provided")
+ }
+
+ // Validate at least one notifier is configured
+ if !c.HasAnyNotifier() {
+ return fmt.Errorf("at least one notifier must be configured")
+ }
+
+ return nil
+}
+
+// HasAnyNotifier checks if at least one notifier is configured
+func (c *Config) HasAnyNotifier() bool {
+ return c.Notifiers.Stdout ||
+ c.Notifiers.SMTP != nil ||
+ c.Notifiers.Slack != nil ||
+ c.Notifiers.Ntfy != nil
+}
+
+// GetEnabledNotifiers returns a list of enabled notifier types
+func (c *Config) GetEnabledNotifiers() []domain.NotificationType {
+ var enabled []domain.NotificationType
+
+ if c.Notifiers.Stdout {
+ enabled = append(enabled, domain.TypeStdout)
+ }
+ if c.Notifiers.SMTP != nil {
+ enabled = append(enabled, domain.TypeEmail)
+ }
+ if c.Notifiers.Slack != nil {
+ enabled = append(enabled, domain.TypeSlack)
+ }
+ if c.Notifiers.Ntfy != nil {
+ enabled = append(enabled, domain.TypeNtfy)
+ }
+
+ return enabled
+}
diff --git a/internal/domain/notification.go b/internal/domain/notification.go
new file mode 100644
index 0000000..71f94fb
--- /dev/null
+++ b/internal/domain/notification.go
@@ -0,0 +1,115 @@
+package domain
+
+import (
+ "time"
+)
+
+// Priority defines the urgency level of a notification
+type Priority int
+
+const (
+ PriorityLow Priority = iota
+ PriorityNormal
+ PriorityHigh
+ PriorityCritical
+)
+
+// NotificationType defines the channel through which to send the notification
+type NotificationType string
+
+const (
+ TypeEmail NotificationType = "email"
+ TypeSlack NotificationType = "slack"
+ TypeNtfy NotificationType = "ntfy"
+ TypeStdout NotificationType = "stdout"
+)
+
+// NotificationStatus represents the current state of a notification
+type NotificationStatus string
+
+const (
+ StatusPending NotificationStatus = "pending"
+ StatusQueued NotificationStatus = "queued"
+ StatusProcessing NotificationStatus = "processing"
+ StatusSent NotificationStatus = "sent"
+ StatusFailed NotificationStatus = "failed"
+ StatusRetrying NotificationStatus = "retrying"
+)
+
+// Notification represents a notification message with metadata
+type Notification struct {
+ // ID is a unique identifier for the notification
+ ID string `json:"id"`
+
+ // Type specifies which notifier should handle this notification
+ Type NotificationType `json:"type"`
+
+ // Priority determines urgency and retry behavior
+ Priority Priority `json:"priority"`
+
+ // Status tracks the current state of the notification
+ Status NotificationStatus `json:"status"`
+
+ // Subject is the notification subject/title (used for email, slack, ntfy)
+ Subject string `json:"subject"`
+
+ // Body is the main content of the notification
+ Body string `json:"body"`
+
+ // Recipients contains the target addresses (email, slack channel, ntfy topic, etc.)
+ Recipients []string `json:"recipients"`
+
+ // Metadata contains additional provider-specific data
+ Metadata map[string]interface{} `json:"metadata,omitempty"`
+
+ // CreatedAt is when the notification was created
+ CreatedAt time.Time `json:"created_at"`
+
+ // ScheduledFor allows delayed sending (optional)
+ ScheduledFor *time.Time `json:"scheduled_for,omitempty"`
+
+ // SentAt is when the notification was successfully sent
+ SentAt *time.Time `json:"sent_at,omitempty"`
+
+ // RetryCount tracks how many times sending has been attempted
+ RetryCount int `json:"retry_count"`
+
+ // MaxRetries defines the maximum retry attempts
+ MaxRetries int `json:"max_retries"`
+
+ // LastError stores the most recent error message if failed
+ LastError string `json:"last_error,omitempty"`
+}
+
+// NotificationResult represents the outcome of sending a notification
+type NotificationResult struct {
+ // NotificationID references the original notification
+ NotificationID string `json:"notification_id"`
+
+ // Success indicates if the notification was sent successfully
+ Success bool `json:"success"`
+
+ // Message provides additional context about the result
+ Message string `json:"message,omitempty"`
+
+ // Error contains error details if the notification failed
+ Error string `json:"error,omitempty"`
+
+ // SentAt is when the notification was sent
+ SentAt time.Time `json:"sent_at"`
+
+ // ProviderResponse contains raw response data from the notification provider
+ ProviderResponse map[string]interface{} `json:"provider_response,omitempty"`
+}
+
+// NotificationFilter is used for querying notifications
+type NotificationFilter struct {
+ IDs []string `json:"ids,omitempty"`
+ Types []NotificationType `json:"types,omitempty"`
+ Statuses []NotificationStatus `json:"statuses,omitempty"`
+ Recipients []string `json:"recipients,omitempty"`
+ CreatedAfter *time.Time `json:"created_after,omitempty"`
+ CreatedBefore *time.Time `json:"created_before,omitempty"`
+ Limit int `json:"limit,omitempty"`
+ Offset int `json:"offset,omitempty"`
+}
diff --git a/internal/domain/notifier.go b/internal/domain/notifier.go
new file mode 100644
index 0000000..8454d5e
--- /dev/null
+++ b/internal/domain/notifier.go
@@ -0,0 +1,67 @@
+package domain
+
+import (
+ "context"
+)
+
+// Notifier is the core interface that all notification implementations must satisfy
+type Notifier interface {
+ // Send sends a notification and returns the result
+ Send(ctx context.Context, notification *Notification) (*NotificationResult, error)
+
+ // Type returns the notification type this notifier handles
+ Type() NotificationType
+
+ // Validate checks if a notification can be sent with this notifier
+ Validate(notification *Notification) error
+
+ // Close performs cleanup when the notifier is no longer needed
+ Close() error
+}
+
+// NotifierFactory creates notifier instances based on configuration
+type NotifierFactory interface {
+ // Create creates a notifier for the given type
+ Create(notificationType NotificationType) (Notifier, error)
+
+ // RegisterNotifier registers a custom notifier implementation
+ RegisterNotifier(notificationType NotificationType, notifier Notifier) error
+
+ // SupportedTypes returns all supported notification types
+ SupportedTypes() []NotificationType
+}
+
+// NotificationService is the high-level service interface for managing notifications
+type NotificationService interface {
+ // Send queues a notification for delivery
+ Send(ctx context.Context, notification *Notification) (*NotificationResult, error)
+
+ // SendBatch queues multiple notifications for delivery
+ SendBatch(ctx context.Context, notifications []*Notification) ([]*NotificationResult, error)
+
+ // GetNotification retrieves a notification by ID
+ GetNotification(ctx context.Context, id string) (*Notification, error)
+
+ // ListNotifications retrieves notifications matching the filter
+ ListNotifications(ctx context.Context, filter *NotificationFilter) ([]*Notification, error)
+
+ // CancelNotification cancels a pending notification
+ CancelNotification(ctx context.Context, id string) error
+
+ // RetryNotification retries a failed notification
+ RetryNotification(ctx context.Context, id string) (*NotificationResult, error)
+
+ // GetStats returns notification statistics
+ GetStats(ctx context.Context) (*NotificationStats, error)
+}
+
+// NotificationStats contains statistics about notification processing
+type NotificationStats struct {
+ TotalSent int64 `json:"total_sent"`
+ TotalFailed int64 `json:"total_failed"`
+ TotalPending int64 `json:"total_pending"`
+ TotalQueued int64 `json:"total_queued"`
+ ByType map[string]int64 `json:"by_type"`
+ ByStatus map[string]int64 `json:"by_status"`
+ AverageLatency float64 `json:"average_latency_ms"`
+}
diff --git a/internal/domain/queue.go b/internal/domain/queue.go
new file mode 100644
index 0000000..c4f697d
--- /dev/null
+++ b/internal/domain/queue.go
@@ -0,0 +1,111 @@
+package domain
+
+import (
+ "context"
+)
+
+// QueueMessage wraps a notification with queue-specific metadata
+type QueueMessage struct {
+ // ID is a unique identifier for this queue message
+ ID string `json:"id"`
+
+ // Notification is the actual notification to be sent
+ Notification *Notification `json:"notification"`
+
+ // Attempt is the current delivery attempt number
+ Attempt int `json:"attempt"`
+
+ // EnqueuedAt is when the message was added to the queue
+ EnqueuedAt int64 `json:"enqueued_at"`
+}
+
+// Queue defines the interface for a notification queue
+type Queue interface {
+ // Enqueue adds a notification to the queue
+ Enqueue(ctx context.Context, notification *Notification) error
+
+ // EnqueueBatch adds multiple notifications to the queue
+ EnqueueBatch(ctx context.Context, notifications []*Notification) error
+
+ // Dequeue retrieves the next notification from the queue
+ // Returns nil if the queue is empty
+ Dequeue(ctx context.Context) (*QueueMessage, error)
+
+ // Ack acknowledges successful processing of a message
+ Ack(ctx context.Context, messageID string) error
+
+ // Nack indicates processing failure and may requeue the message
+ Nack(ctx context.Context, messageID string, requeue bool) error
+
+ // Size returns the current number of messages in the queue
+ Size(ctx context.Context) (int64, error)
+
+ // Purge removes all messages from the queue
+ Purge(ctx context.Context) error
+
+ // Close cleanly shuts down the queue
+ Close() error
+
+ // HealthCheck verifies the queue is operational
+ HealthCheck(ctx context.Context) error
+}
+
+// QueueConfig contains configuration for queue implementations
+type QueueConfig struct {
+ // Type specifies the queue implementation (local, kafka, etc.)
+ Type string `mapstructure:"type"`
+
+ // MaxSize is the maximum number of messages the queue can hold
+ MaxSize int64 `mapstructure:"max_size"`
+
+ // WorkerCount is the number of concurrent workers processing the queue
+ WorkerCount int `mapstructure:"worker_count"`
+
+ // RetryAttempts is the number of times to retry failed notifications
+ RetryAttempts int `mapstructure:"retry_attempts"`
+
+ // RetryBackoff is the backoff strategy for retries (exponential, linear, fixed)
+ RetryBackoff string `mapstructure:"retry_backoff"`
+
+ // Local queue specific config
+ Local *LocalQueueConfig `mapstructure:"local,omitempty"`
+
+ // Kafka specific config
+ Kafka *KafkaQueueConfig `mapstructure:"kafka,omitempty"`
+}
+
+// LocalQueueConfig contains configuration for the in-memory queue
+type LocalQueueConfig struct {
+ // BufferSize is the channel buffer size
+ BufferSize int `mapstructure:"buffer_size"`
+
+ // PersistToDisk enables writing queue state to disk for recovery
+ PersistToDisk bool `mapstructure:"persist_to_disk"`
+
+ // PersistPath is where to store the queue state
+ PersistPath string `mapstructure:"persist_path"`
+}
+
+// KafkaQueueConfig contains configuration for Kafka queue
+type KafkaQueueConfig struct {
+ // Brokers is the list of Kafka broker addresses
+ Brokers []string `mapstructure:"brokers"`
+
+ // Topic is the Kafka topic for notifications
+ Topic string `mapstructure:"topic"`
+
+ // ConsumerGroup is the Kafka consumer group ID
+ ConsumerGroup string `mapstructure:"consumer_group"`
+
+ // PartitionCount is the number of partitions for the topic
+ PartitionCount int `mapstructure:"partition_count"`
+
+ // ReplicationFactor is the replication factor for the topic
+ ReplicationFactor int `mapstructure:"replication_factor"`
+
+ // EnableIdempotence ensures exactly-once delivery semantics
+ EnableIdempotence bool `mapstructure:"enable_idempotence"`
+
+ // CompressionType defines compression (none, gzip, snappy, lz4, zstd)
+ CompressionType string `mapstructure:"compression_type"`
+}
diff --git a/internal/notifier/notifier.go b/internal/notifier/notifier.go
index e69de29..7339c95 100644
--- a/internal/notifier/notifier.go
+++ b/internal/notifier/notifier.go
@@ -0,0 +1,107 @@
+package notifier
+
+import (
+ "context"
+ "fmt"
+ "sync"
+
+ "github.com/igodwin/notifier/internal/domain"
+)
+
+// Factory creates and manages notifier instances
+type Factory struct {
+ notifiers map[domain.NotificationType]domain.Notifier
+ mu sync.RWMutex
+}
+
+// NewFactory creates a new notifier factory
+func NewFactory() *Factory {
+ return &Factory{
+ notifiers: make(map[domain.NotificationType]domain.Notifier),
+ }
+}
+
+// Create creates a notifier for the given type
+func (f *Factory) Create(notificationType domain.NotificationType) (domain.Notifier, error) {
+ f.mu.RLock()
+ defer f.mu.RUnlock()
+
+ notifier, exists := f.notifiers[notificationType]
+ if !exists {
+ return nil, fmt.Errorf("unsupported notification type: %s", notificationType)
+ }
+
+ return notifier, nil
+}
+
+// RegisterNotifier registers a custom notifier implementation
+func (f *Factory) RegisterNotifier(notificationType domain.NotificationType, notifier domain.Notifier) error {
+ f.mu.Lock()
+ defer f.mu.Unlock()
+
+ if _, exists := f.notifiers[notificationType]; exists {
+ return fmt.Errorf("notifier already registered for type: %s", notificationType)
+ }
+
+ f.notifiers[notificationType] = notifier
+ return nil
+}
+
+// SupportedTypes returns all supported notification types
+func (f *Factory) SupportedTypes() []domain.NotificationType {
+ f.mu.RLock()
+ defer f.mu.RUnlock()
+
+ types := make([]domain.NotificationType, 0, len(f.notifiers))
+ for t := range f.notifiers {
+ types = append(types, t)
+ }
+
+ return types
+}
+
+// BaseNotifier provides common functionality for all notifiers
+type BaseNotifier struct {
+ notificationType domain.NotificationType
+}
+
+// Type returns the notification type
+func (b *BaseNotifier) Type() domain.NotificationType {
+ return b.notificationType
+}
+
+// Validate performs basic validation common to all notifiers
+func (b *BaseNotifier) Validate(notification *domain.Notification) error {
+ if notification == nil {
+ return fmt.Errorf("notification is nil")
+ }
+
+ if len(notification.Recipients) == 0 {
+ return fmt.Errorf("notification has no recipients")
+ }
+
+ if notification.Type != b.notificationType {
+ return fmt.Errorf("notification type mismatch: expected %s, got %s", b.notificationType, notification.Type)
+ }
+
+ return nil
+}
+
+// Close performs cleanup (default implementation does nothing)
+func (b *BaseNotifier) Close() error {
+ return nil
+}
+
+// ValidateContext checks if the context is valid
+func ValidateContext(ctx context.Context) error {
+ if ctx == nil {
+ return fmt.Errorf("context is nil")
+ }
+
+ select {
+ case <-ctx.Done():
+ return ctx.Err()
+ default:
+ return nil
+ }
+}
diff --git a/internal/notifier/ntfy.go b/internal/notifier/ntfy.go
index e69de29..24f5465 100644
--- a/internal/notifier/ntfy.go
+++ b/internal/notifier/ntfy.go
@@ -0,0 +1,261 @@
+package notifier
+
+import (
+ "bytes"
+ "context"
+ "crypto/tls"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/igodwin/notifier/internal/domain"
+)
+
+// NtfyConfig contains ntfy.sh configuration
+type NtfyConfig struct {
+ // ServerURL is the ntfy server URL (default: https://ntfy.sh)
+ ServerURL string `mapstructure:"server_url"`
+
+ // Token is the access token for authentication (preferred method)
+ // Supports both regular tokens (tk_...) and publish tokens
+ Token string `mapstructure:"token"`
+
+ // Username for basic authentication (alternative to token)
+ Username string `mapstructure:"username"`
+
+ // Password for basic authentication (alternative to token)
+ Password string `mapstructure:"password"`
+
+ // DefaultTopic is the default topic if not specified in notification
+ DefaultTopic string `mapstructure:"default_topic"`
+
+ // InsecureSkipVerify skips TLS verification (for self-hosted servers with self-signed certs)
+ InsecureSkipVerify bool `mapstructure:"insecure_skip_verify"`
+}
+
+// NtfyNotifier sends notifications via ntfy.sh
+type NtfyNotifier struct {
+ BaseNotifier
+ config *NtfyConfig
+ httpClient *http.Client
+}
+
+// ntfyRequest represents the ntfy API request format
+type ntfyRequest struct {
+ Topic string `json:"topic"`
+ Message string `json:"message"`
+ Title string `json:"title,omitempty"`
+ Priority int `json:"priority,omitempty"`
+ Tags []string `json:"tags,omitempty"`
+ Click string `json:"click,omitempty"`
+ Attach string `json:"attach,omitempty"`
+ Actions []ntfyAction `json:"actions,omitempty"`
+ Icon string `json:"icon,omitempty"`
+ Delay string `json:"delay,omitempty"`
+ Email string `json:"email,omitempty"`
+}
+
+// ntfyAction represents an action button in ntfy
+type ntfyAction struct {
+ Action string `json:"action"`
+ Label string `json:"label"`
+ URL string `json:"url,omitempty"`
+ Body string `json:"body,omitempty"`
+ Clear bool `json:"clear,omitempty"`
+}
+
+// NewNtfyNotifier creates a new ntfy notifier
+func NewNtfyNotifier(config *NtfyConfig) (*NtfyNotifier, error) {
+ if config == nil {
+ return nil, fmt.Errorf("ntfy config is required")
+ }
+
+ if config.ServerURL == "" {
+ config.ServerURL = "https://ntfy.sh" // Default public ntfy server
+ }
+
+ // Create HTTP client with optional TLS skip verify
+ httpClient := &http.Client{
+ Timeout: 30 * time.Second,
+ }
+
+ if config.InsecureSkipVerify {
+ // For self-hosted servers with self-signed certificates
+ transport := &http.Transport{
+ TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
+ }
+ httpClient.Transport = transport
+ }
+
+ return &NtfyNotifier{
+ BaseNotifier: BaseNotifier{
+ notificationType: domain.TypeNtfy,
+ },
+ config: config,
+ httpClient: httpClient,
+ }, nil
+}
+
+// Send sends a notification via ntfy
+func (n *NtfyNotifier) Send(ctx context.Context, notification *domain.Notification) (*domain.NotificationResult, error) {
+ if err := ValidateContext(ctx); err != nil {
+ return nil, err
+ }
+
+ if err := n.Validate(notification); err != nil {
+ return nil, err
+ }
+
+ // For ntfy, recipients are topics
+ recipients := notification.Recipients
+ if len(recipients) == 0 && n.config.DefaultTopic != "" {
+ recipients = []string{n.config.DefaultTopic}
+ }
+
+ for _, topic := range recipients {
+ req := ntfyRequest{
+ Topic: topic,
+ Message: notification.Body,
+ Title: notification.Subject,
+ Priority: n.mapPriority(notification.Priority),
+ }
+
+ // Add custom tags from metadata
+ if tags, ok := notification.Metadata["tags"].([]interface{}); ok {
+ for _, tag := range tags {
+ if tagStr, ok := tag.(string); ok {
+ req.Tags = append(req.Tags, tagStr)
+ }
+ }
+ }
+
+ // Add click action from metadata
+ if click, ok := notification.Metadata["click"].(string); ok {
+ req.Click = click
+ }
+
+ // Add attachment from metadata
+ if attach, ok := notification.Metadata["attach"].(string); ok {
+ req.Attach = attach
+ }
+
+ // Add icon from metadata
+ if icon, ok := notification.Metadata["icon"].(string); ok {
+ req.Icon = icon
+ }
+
+ // Add delay from metadata (e.g., "30s", "1m", "1h")
+ if delay, ok := notification.Metadata["delay"].(string); ok {
+ req.Delay = delay
+ }
+
+ // Add email from metadata (for email notifications)
+ if email, ok := notification.Metadata["email"].(string); ok {
+ req.Email = email
+ }
+
+ // Add actions from metadata
+ if actions, ok := notification.Metadata["actions"].([]interface{}); ok {
+ for _, action := range actions {
+ if actionMap, ok := action.(map[string]interface{}); ok {
+ ntfyAct := ntfyAction{}
+ if actionType, ok := actionMap["action"].(string); ok {
+ ntfyAct.Action = actionType
+ }
+ if label, ok := actionMap["label"].(string); ok {
+ ntfyAct.Label = label
+ }
+ if url, ok := actionMap["url"].(string); ok {
+ ntfyAct.URL = url
+ }
+ if body, ok := actionMap["body"].(string); ok {
+ ntfyAct.Body = body
+ }
+ if clear, ok := actionMap["clear"].(bool); ok {
+ ntfyAct.Clear = clear
+ }
+ req.Actions = append(req.Actions, ntfyAct)
+ }
+ }
+ }
+
+ if err := n.sendToTopic(ctx, &req); err != nil {
+ return &domain.NotificationResult{
+ NotificationID: notification.ID,
+ Success: false,
+ Error: err.Error(),
+ SentAt: time.Now(),
+ }, err
+ }
+ }
+
+ return &domain.NotificationResult{
+ NotificationID: notification.ID,
+ Success: true,
+ Message: fmt.Sprintf("Notification sent to %d topics", len(notification.Recipients)),
+ SentAt: time.Now(),
+ ProviderResponse: map[string]interface{}{
+ "server": n.config.ServerURL,
+ "topics": notification.Recipients,
+ },
+ }, nil
+}
+
+// sendToTopic sends a notification to a specific ntfy topic
+func (n *NtfyNotifier) sendToTopic(ctx context.Context, req *ntfyRequest) error {
+ url := fmt.Sprintf("%s", n.config.ServerURL)
+
+ jsonData, err := json.Marshal(req)
+ if err != nil {
+ return fmt.Errorf("failed to marshal ntfy request: %w", err)
+ }
+
+ httpReq, err := http.NewRequestWithContext(ctx, "POST", url, bytes.NewBuffer(jsonData))
+ if err != nil {
+ return fmt.Errorf("failed to create request: %w", err)
+ }
+
+ httpReq.Header.Set("Content-Type", "application/json")
+
+ // Add authentication if configured
+ if n.config.Token != "" {
+ httpReq.Header.Set("Authorization", fmt.Sprintf("Bearer %s", n.config.Token))
+ } else if n.config.Username != "" && n.config.Password != "" {
+ httpReq.SetBasicAuth(n.config.Username, n.config.Password)
+ }
+
+ resp, err := n.httpClient.Do(httpReq)
+ if err != nil {
+ return fmt.Errorf("failed to send ntfy notification: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+ return fmt.Errorf("ntfy server returned status: %d", resp.StatusCode)
+ }
+
+ return nil
+}
+
+// mapPriority maps domain priority to ntfy priority (1-5)
+func (n *NtfyNotifier) mapPriority(priority domain.Priority) int {
+ switch priority {
+ case domain.PriorityLow:
+ return 2
+ case domain.PriorityNormal:
+ return 3
+ case domain.PriorityHigh:
+ return 4
+ case domain.PriorityCritical:
+ return 5
+ default:
+ return 3
+ }
+}
+
+// Close closes the HTTP client
+func (n *NtfyNotifier) Close() error {
+ n.httpClient.CloseIdleConnections()
+ return nil
+}
diff --git a/internal/notifier/slack.go b/internal/notifier/slack.go
new file mode 100644
index 0000000..5b48b9a
--- /dev/null
+++ b/internal/notifier/slack.go
@@ -0,0 +1,215 @@
+package notifier
+
+import (
+ "bytes"
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "time"
+
+ "github.com/igodwin/notifier/internal/domain"
+)
+
+// SlackConfig contains Slack webhook configuration
+type SlackConfig struct {
+ WebhookURL string `mapstructure:"webhook_url"`
+ Token string `mapstructure:"token"`
+ Channel string `mapstructure:"channel"`
+ Username string `mapstructure:"username"`
+ IconEmoji string `mapstructure:"icon_emoji"`
+ Webhooks map[string]string `mapstructure:"webhooks"` // Channel-specific webhooks
+}
+
+// SlackNotifier sends notifications to Slack
+type SlackNotifier struct {
+ BaseNotifier
+ config *SlackConfig
+ httpClient *http.Client
+}
+
+// slackMessage represents the Slack API request format
+type slackMessage struct {
+ Channel string `json:"channel,omitempty"`
+ Username string `json:"username,omitempty"`
+ IconEmoji string `json:"icon_emoji,omitempty"`
+ Text string `json:"text,omitempty"`
+ Blocks []slackBlock `json:"blocks,omitempty"`
+ Markdown bool `json:"mrkdwn,omitempty"`
+}
+
+// slackBlock represents a Slack block element
+type slackBlock struct {
+ Type string `json:"type"`
+ Text *slackTextBlock `json:"text,omitempty"`
+}
+
+// slackTextBlock represents a text element in a Slack block
+type slackTextBlock struct {
+ Type string `json:"type"`
+ Text string `json:"text"`
+}
+
+// NewSlackNotifier creates a new Slack notifier
+func NewSlackNotifier(config *SlackConfig) (*SlackNotifier, error) {
+ if config == nil {
+ return nil, fmt.Errorf("Slack config is required")
+ }
+
+ // Either webhook URL or token is required
+ if config.WebhookURL == "" && config.Token == "" && len(config.Webhooks) == 0 {
+ return nil, fmt.Errorf("Slack webhook URL, token, or channel webhooks are required")
+ }
+
+ return &SlackNotifier{
+ BaseNotifier: BaseNotifier{
+ notificationType: domain.TypeSlack,
+ },
+ config: config,
+ httpClient: &http.Client{
+ Timeout: 30 * time.Second,
+ },
+ }, nil
+}
+
+// Send sends a notification to Slack
+func (s *SlackNotifier) Send(ctx context.Context, notification *domain.Notification) (*domain.NotificationResult, error) {
+ if err := ValidateContext(ctx); err != nil {
+ return nil, err
+ }
+
+ if err := s.Validate(notification); err != nil {
+ return nil, err
+ }
+
+ // For Slack, recipients are channel names or webhook URLs
+ for _, recipient := range notification.Recipients {
+ msg := s.buildMessage(notification, recipient)
+ webhookURL := s.getWebhookURL(recipient)
+
+ if err := s.sendToSlack(ctx, webhookURL, msg); err != nil {
+ return &domain.NotificationResult{
+ NotificationID: notification.ID,
+ Success: false,
+ Error: err.Error(),
+ SentAt: time.Now(),
+ }, err
+ }
+ }
+
+ return &domain.NotificationResult{
+ NotificationID: notification.ID,
+ Success: true,
+ Message: fmt.Sprintf("Slack notification sent to %d channels", len(notification.Recipients)),
+ SentAt: time.Now(),
+ ProviderResponse: map[string]interface{}{
+ "channels": notification.Recipients,
+ },
+ }, nil
+}
+
+// buildMessage constructs a Slack message with rich formatting
+func (s *SlackNotifier) buildMessage(notification *domain.Notification, channel string) *slackMessage {
+ msg := &slackMessage{
+ Channel: channel,
+ Username: s.config.Username,
+ IconEmoji: s.config.IconEmoji,
+ Markdown: true,
+ }
+
+ // Use blocks for rich formatting if both subject and body exist
+ if notification.Subject != "" && notification.Body != "" {
+ msg.Blocks = []slackBlock{
+ {
+ Type: "header",
+ Text: &slackTextBlock{
+ Type: "plain_text",
+ Text: notification.Subject,
+ },
+ },
+ {
+ Type: "section",
+ Text: &slackTextBlock{
+ Type: "mrkdwn",
+ Text: notification.Body,
+ },
+ },
+ }
+ } else {
+ // Fallback to simple text
+ if notification.Subject != "" {
+ msg.Text = fmt.Sprintf("*%s*\n%s", notification.Subject, notification.Body)
+ } else {
+ msg.Text = notification.Body
+ }
+ }
+
+ // Add priority indicator for high priority notifications
+ if notification.Priority >= domain.PriorityHigh {
+ priorityEmoji := ":warning:"
+ if notification.Priority == domain.PriorityCritical {
+ priorityEmoji = ":rotating_light:"
+ }
+
+ msg.Blocks = append([]slackBlock{
+ {
+ Type: "context",
+ Text: &slackTextBlock{
+ Type: "mrkdwn",
+ Text: fmt.Sprintf("%s *Priority: %d*", priorityEmoji, notification.Priority),
+ },
+ },
+ }, msg.Blocks...)
+ }
+
+ return msg
+}
+
+// getWebhookURL returns the webhook URL for a specific channel
+func (s *SlackNotifier) getWebhookURL(channel string) string {
+ // Check for channel-specific webhook
+ if webhook, ok := s.config.Webhooks[channel]; ok {
+ return webhook
+ }
+
+ // Fall back to default webhook URL
+ return s.config.WebhookURL
+}
+
+// sendToSlack sends the message to Slack via webhook
+func (s *SlackNotifier) sendToSlack(ctx context.Context, webhookURL string, msg *slackMessage) error {
+ jsonData, err := json.Marshal(msg)
+ if err != nil {
+ return fmt.Errorf("failed to marshal Slack message: %w", err)
+ }
+
+ req, err := http.NewRequestWithContext(ctx, "POST", webhookURL, bytes.NewBuffer(jsonData))
+ if err != nil {
+ return fmt.Errorf("failed to create request: %w", err)
+ }
+
+ req.Header.Set("Content-Type", "application/json")
+
+ // Add token authentication if configured
+ if s.config.Token != "" {
+ req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", s.config.Token))
+ }
+
+ resp, err := s.httpClient.Do(req)
+ if err != nil {
+ return fmt.Errorf("failed to send Slack notification: %w", err)
+ }
+ defer resp.Body.Close()
+
+ if resp.StatusCode < 200 || resp.StatusCode >= 300 {
+ return fmt.Errorf("Slack API returned status: %d", resp.StatusCode)
+ }
+
+ return nil
+}
+
+// Close closes the HTTP client
+func (s *SlackNotifier) Close() error {
+ s.httpClient.CloseIdleConnections()
+ return nil
+}
diff --git a/internal/notifier/smtp.go b/internal/notifier/smtp.go
index e69de29..319494f 100644
--- a/internal/notifier/smtp.go
+++ b/internal/notifier/smtp.go
@@ -0,0 +1,137 @@
+package notifier
+
+import (
+ "context"
+ "fmt"
+ "net/smtp"
+ "strings"
+ "time"
+
+ "github.com/igodwin/notifier/internal/domain"
+)
+
+// SMTPConfig contains SMTP server configuration
+type SMTPConfig struct {
+ Host string `mapstructure:"host"`
+ Port int `mapstructure:"port"`
+ Username string `mapstructure:"username"`
+ Password string `mapstructure:"password"`
+ From string `mapstructure:"from"`
+ UseTLS bool `mapstructure:"use_tls"`
+}
+
+// SMTPNotifier sends notifications via email using SMTP
+type SMTPNotifier struct {
+ BaseNotifier
+ config *SMTPConfig
+}
+
+// NewSMTPNotifier creates a new SMTP notifier
+func NewSMTPNotifier(config *SMTPConfig) (*SMTPNotifier, error) {
+ if config == nil {
+ return nil, fmt.Errorf("SMTP config is required")
+ }
+
+ if config.Host == "" {
+ return nil, fmt.Errorf("SMTP host is required")
+ }
+
+ if config.Port == 0 {
+ config.Port = 587 // Default SMTP submission port
+ }
+
+ if config.From == "" {
+ return nil, fmt.Errorf("SMTP from address is required")
+ }
+
+ return &SMTPNotifier{
+ BaseNotifier: BaseNotifier{
+ notificationType: domain.TypeEmail,
+ },
+ config: config,
+ }, nil
+}
+
+// Send sends a notification via email
+func (s *SMTPNotifier) Send(ctx context.Context, notification *domain.Notification) (*domain.NotificationResult, error) {
+ if err := ValidateContext(ctx); err != nil {
+ return nil, err
+ }
+
+ if err := s.Validate(notification); err != nil {
+ return nil, err
+ }
+
+ // Validate email recipients
+ for _, recipient := range notification.Recipients {
+ if !strings.Contains(recipient, "@") {
+ return &domain.NotificationResult{
+ NotificationID: notification.ID,
+ Success: false,
+ Error: fmt.Sprintf("invalid email address: %s", recipient),
+ SentAt: time.Now(),
+ }, fmt.Errorf("invalid email address: %s", recipient)
+ }
+ }
+
+ // Build email message
+ message := s.buildMessage(notification)
+
+ // Send email
+ addr := fmt.Sprintf("%s:%d", s.config.Host, s.config.Port)
+ auth := smtp.PlainAuth("", s.config.Username, s.config.Password, s.config.Host)
+
+ err := smtp.SendMail(addr, auth, s.config.From, notification.Recipients, []byte(message))
+ if err != nil {
+ return &domain.NotificationResult{
+ NotificationID: notification.ID,
+ Success: false,
+ Error: err.Error(),
+ SentAt: time.Now(),
+ }, fmt.Errorf("failed to send email: %w", err)
+ }
+
+ return &domain.NotificationResult{
+ NotificationID: notification.ID,
+ Success: true,
+ Message: fmt.Sprintf("Email sent to %d recipients", len(notification.Recipients)),
+ SentAt: time.Now(),
+ ProviderResponse: map[string]interface{}{
+ "smtp_server": addr,
+ "from": s.config.From,
+ "to": notification.Recipients,
+ },
+ }, nil
+}
+
+// buildMessage constructs the email message with headers
+func (s *SMTPNotifier) buildMessage(notification *domain.Notification) string {
+ var builder strings.Builder
+
+ builder.WriteString(fmt.Sprintf("From: %s\r\n", s.config.From))
+ builder.WriteString(fmt.Sprintf("To: %s\r\n", strings.Join(notification.Recipients, ", ")))
+ builder.WriteString(fmt.Sprintf("Subject: %s\r\n", notification.Subject))
+ builder.WriteString("MIME-Version: 1.0\r\n")
+ builder.WriteString("Content-Type: text/plain; charset=UTF-8\r\n")
+ builder.WriteString("\r\n")
+ builder.WriteString(notification.Body)
+
+ return builder.String()
+}
+
+// Validate checks if the notification is valid for SMTP
+func (s *SMTPNotifier) Validate(notification *domain.Notification) error {
+ if err := s.BaseNotifier.Validate(notification); err != nil {
+ return err
+ }
+
+ if notification.Subject == "" {
+ return fmt.Errorf("email subject is required")
+ }
+
+ if notification.Body == "" {
+ return fmt.Errorf("email body is required")
+ }
+
+ return nil
+}
diff --git a/internal/notifier/stdout.go b/internal/notifier/stdout.go
index e69de29..ed861da 100644
--- a/internal/notifier/stdout.go
+++ b/internal/notifier/stdout.go
@@ -0,0 +1,50 @@
+package notifier
+
+import (
+ "context"
+ "fmt"
+ "time"
+
+ "github.com/igodwin/notifier/internal/domain"
+)
+
+// StdoutNotifier sends notifications to stdout (useful for debugging)
+type StdoutNotifier struct {
+ BaseNotifier
+}
+
+// NewStdoutNotifier creates a new stdout notifier
+func NewStdoutNotifier() *StdoutNotifier {
+ return &StdoutNotifier{
+ BaseNotifier: BaseNotifier{
+ notificationType: domain.TypeStdout,
+ },
+ }
+}
+
+// Send sends a notification to stdout
+func (s *StdoutNotifier) Send(ctx context.Context, notification *domain.Notification) (*domain.NotificationResult, error) {
+ if err := ValidateContext(ctx); err != nil {
+ return nil, err
+ }
+
+ if err := s.Validate(notification); err != nil {
+ return nil, err
+ }
+
+ fmt.Println("========================================")
+ fmt.Printf("Notification ID: %s\n", notification.ID)
+ fmt.Printf("Type: %s\n", notification.Type)
+ fmt.Printf("Priority: %d\n", notification.Priority)
+ fmt.Printf("Recipients: %v\n", notification.Recipients)
+ fmt.Printf("Subject: %s\n", notification.Subject)
+ fmt.Printf("Body:\n%s\n", notification.Body)
+ fmt.Println("========================================")
+
+ return &domain.NotificationResult{
+ NotificationID: notification.ID,
+ Success: true,
+ Message: "Notification printed to stdout",
+ SentAt: time.Now(),
+ }, nil
+}
diff --git a/internal/queue/local.go b/internal/queue/local.go
new file mode 100644
index 0000000..cf4f7bd
--- /dev/null
+++ b/internal/queue/local.go
@@ -0,0 +1,297 @@
+package queue
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "os"
+ "sync"
+ "time"
+
+ "github.com/igodwin/notifier/internal/domain"
+ "github.com/google/uuid"
+)
+
+// LocalQueue is an in-memory queue implementation
+type LocalQueue struct {
+ queue chan *domain.QueueMessage
+ messages map[string]*domain.QueueMessage
+ mu sync.RWMutex
+ config *domain.LocalQueueConfig
+ persistToDisk bool
+ persistPath string
+ closed bool
+ closeChan chan struct{}
+}
+
+// NewLocalQueue creates a new local queue instance
+func NewLocalQueue(config *domain.LocalQueueConfig) (*LocalQueue, error) {
+ if config == nil {
+ config = &domain.LocalQueueConfig{
+ BufferSize: 1000,
+ PersistToDisk: false,
+ }
+ }
+
+ lq := &LocalQueue{
+ queue: make(chan *domain.QueueMessage, config.BufferSize),
+ messages: make(map[string]*domain.QueueMessage),
+ config: config,
+ persistToDisk: config.PersistToDisk,
+ persistPath: config.PersistPath,
+ closeChan: make(chan struct{}),
+ }
+
+ // Load persisted messages if enabled
+ if lq.persistToDisk && lq.persistPath != "" {
+ if err := lq.loadFromDisk(); err != nil {
+ return nil, fmt.Errorf("failed to load persisted queue: %w", err)
+ }
+ }
+
+ return lq, nil
+}
+
+// Enqueue adds a notification to the queue
+func (lq *LocalQueue) Enqueue(ctx context.Context, notification *domain.Notification) error {
+ lq.mu.Lock()
+ defer lq.mu.Unlock()
+
+ if lq.closed {
+ return fmt.Errorf("queue is closed")
+ }
+
+ msg := &domain.QueueMessage{
+ ID: uuid.New().String(),
+ Notification: notification,
+ Attempt: 0,
+ EnqueuedAt: time.Now().Unix(),
+ }
+
+ select {
+ case lq.queue <- msg:
+ lq.messages[msg.ID] = msg
+ notification.Status = domain.StatusQueued
+
+ if lq.persistToDisk {
+ return lq.persistToDiskSync()
+ }
+ return nil
+ case <-ctx.Done():
+ return ctx.Err()
+ case <-lq.closeChan:
+ return fmt.Errorf("queue is closed")
+ }
+}
+
+// EnqueueBatch adds multiple notifications to the queue
+func (lq *LocalQueue) EnqueueBatch(ctx context.Context, notifications []*domain.Notification) error {
+ lq.mu.Lock()
+ defer lq.mu.Unlock()
+
+ if lq.closed {
+ return fmt.Errorf("queue is closed")
+ }
+
+ for _, notification := range notifications {
+ msg := &domain.QueueMessage{
+ ID: uuid.New().String(),
+ Notification: notification,
+ Attempt: 0,
+ EnqueuedAt: time.Now().Unix(),
+ }
+
+ select {
+ case lq.queue <- msg:
+ lq.messages[msg.ID] = msg
+ notification.Status = domain.StatusQueued
+ case <-ctx.Done():
+ return ctx.Err()
+ case <-lq.closeChan:
+ return fmt.Errorf("queue is closed")
+ }
+ }
+
+ if lq.persistToDisk {
+ return lq.persistToDiskSync()
+ }
+ return nil
+}
+
+// Dequeue retrieves the next notification from the queue
+func (lq *LocalQueue) Dequeue(ctx context.Context) (*domain.QueueMessage, error) {
+ if lq.closed {
+ return nil, fmt.Errorf("queue is closed")
+ }
+
+ select {
+ case msg := <-lq.queue:
+ lq.mu.Lock()
+ msg.Attempt++
+ msg.Notification.Status = domain.StatusProcessing
+ lq.mu.Unlock()
+ return msg, nil
+ case <-ctx.Done():
+ return nil, ctx.Err()
+ case <-lq.closeChan:
+ return nil, fmt.Errorf("queue is closed")
+ }
+}
+
+// Ack acknowledges successful processing of a message
+func (lq *LocalQueue) Ack(ctx context.Context, messageID string) error {
+ lq.mu.Lock()
+ defer lq.mu.Unlock()
+
+ if msg, exists := lq.messages[messageID]; exists {
+ msg.Notification.Status = domain.StatusSent
+ delete(lq.messages, messageID)
+
+ if lq.persistToDisk {
+ return lq.persistToDiskSync()
+ }
+ }
+
+ return nil
+}
+
+// Nack indicates processing failure and may requeue the message
+func (lq *LocalQueue) Nack(ctx context.Context, messageID string, requeue bool) error {
+ lq.mu.Lock()
+ defer lq.mu.Unlock()
+
+ msg, exists := lq.messages[messageID]
+ if !exists {
+ return fmt.Errorf("message not found: %s", messageID)
+ }
+
+ if requeue {
+ msg.Notification.Status = domain.StatusRetrying
+ select {
+ case lq.queue <- msg:
+ if lq.persistToDisk {
+ return lq.persistToDiskSync()
+ }
+ return nil
+ case <-ctx.Done():
+ return ctx.Err()
+ case <-lq.closeChan:
+ return fmt.Errorf("queue is closed")
+ }
+ } else {
+ msg.Notification.Status = domain.StatusFailed
+ delete(lq.messages, messageID)
+
+ if lq.persistToDisk {
+ return lq.persistToDiskSync()
+ }
+ }
+
+ return nil
+}
+
+// Size returns the current number of messages in the queue
+func (lq *LocalQueue) Size(ctx context.Context) (int64, error) {
+ lq.mu.RLock()
+ defer lq.mu.RUnlock()
+ return int64(len(lq.queue)), nil
+}
+
+// Purge removes all messages from the queue
+func (lq *LocalQueue) Purge(ctx context.Context) error {
+ lq.mu.Lock()
+ defer lq.mu.Unlock()
+
+ // Drain the channel
+ for len(lq.queue) > 0 {
+ <-lq.queue
+ }
+
+ lq.messages = make(map[string]*domain.QueueMessage)
+
+ if lq.persistToDisk {
+ return lq.persistToDiskSync()
+ }
+
+ return nil
+}
+
+// Close cleanly shuts down the queue
+func (lq *LocalQueue) Close() error {
+ lq.mu.Lock()
+ defer lq.mu.Unlock()
+
+ if lq.closed {
+ return nil
+ }
+
+ lq.closed = true
+ close(lq.closeChan)
+
+ if lq.persistToDisk {
+ if err := lq.persistToDiskSync(); err != nil {
+ return err
+ }
+ }
+
+ close(lq.queue)
+ return nil
+}
+
+// HealthCheck verifies the queue is operational
+func (lq *LocalQueue) HealthCheck(ctx context.Context) error {
+ lq.mu.RLock()
+ defer lq.mu.RUnlock()
+
+ if lq.closed {
+ return fmt.Errorf("queue is closed")
+ }
+
+ return nil
+}
+
+// persistToDiskSync persists the queue state to disk (must be called with lock held)
+func (lq *LocalQueue) persistToDiskSync() error {
+ if !lq.persistToDisk || lq.persistPath == "" {
+ return nil
+ }
+
+ data, err := json.Marshal(lq.messages)
+ if err != nil {
+ return fmt.Errorf("failed to marshal queue state: %w", err)
+ }
+
+ if err := os.WriteFile(lq.persistPath, data, 0644); err != nil {
+ return fmt.Errorf("failed to write queue state: %w", err)
+ }
+
+ return nil
+}
+
+// loadFromDisk loads the queue state from disk
+func (lq *LocalQueue) loadFromDisk() error {
+ if lq.persistPath == "" {
+ return nil
+ }
+
+ data, err := os.ReadFile(lq.persistPath)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return nil // No persisted state yet
+ }
+ return fmt.Errorf("failed to read queue state: %w", err)
+ }
+
+ var messages map[string]*domain.QueueMessage
+ if err := json.Unmarshal(data, &messages); err != nil {
+ return fmt.Errorf("failed to unmarshal queue state: %w", err)
+ }
+
+ // Re-enqueue persisted messages
+ for _, msg := range messages {
+ lq.queue <- msg
+ lq.messages[msg.ID] = msg
+ }
+
+ return nil
+}
diff --git a/internal/service/service.go b/internal/service/service.go
new file mode 100644
index 0000000..c1fab8e
--- /dev/null
+++ b/internal/service/service.go
@@ -0,0 +1,383 @@
+package service
+
+import (
+ "context"
+ "fmt"
+ "sync"
+ "time"
+
+ "github.com/igodwin/notifier/internal/domain"
+)
+
+// NotificationService implements the domain.NotificationService interface
+type NotificationService struct {
+ factory domain.NotifierFactory
+ queue domain.Queue
+ notifications map[string]*domain.Notification
+ mu sync.RWMutex
+ workerCount int
+ stopChan chan struct{}
+ wg sync.WaitGroup
+}
+
+// NewNotificationService creates a new notification service
+func NewNotificationService(factory domain.NotifierFactory, queue domain.Queue, workerCount int) *NotificationService {
+ if workerCount <= 0 {
+ workerCount = 10
+ }
+
+ return &NotificationService{
+ factory: factory,
+ queue: queue,
+ notifications: make(map[string]*domain.Notification),
+ workerCount: workerCount,
+ stopChan: make(chan struct{}),
+ }
+}
+
+// Start starts the worker pool
+func (s *NotificationService) Start(ctx context.Context) error {
+ for i := 0; i < s.workerCount; i++ {
+ s.wg.Add(1)
+ go s.worker(ctx, i)
+ }
+ return nil
+}
+
+// Stop stops the service gracefully
+func (s *NotificationService) Stop() error {
+ close(s.stopChan)
+ s.wg.Wait()
+ return s.queue.Close()
+}
+
+// worker processes notifications from the queue
+func (s *NotificationService) worker(ctx context.Context, id int) {
+ defer s.wg.Done()
+
+ for {
+ select {
+ case <-s.stopChan:
+ return
+ case <-ctx.Done():
+ return
+ default:
+ // Try to dequeue with timeout
+ workerCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
+ msg, err := s.queue.Dequeue(workerCtx)
+ cancel()
+
+ if err != nil {
+ if err == context.DeadlineExceeded {
+ continue
+ }
+ time.Sleep(100 * time.Millisecond)
+ continue
+ }
+
+ if msg == nil {
+ time.Sleep(100 * time.Millisecond)
+ continue
+ }
+
+ // Process the notification
+ s.processNotification(ctx, msg)
+ }
+ }
+}
+
+// processNotification sends a notification and handles the result
+func (s *NotificationService) processNotification(ctx context.Context, msg *domain.QueueMessage) {
+ notification := msg.Notification
+
+ // Get the appropriate notifier
+ notifier, err := s.factory.Create(notification.Type)
+ if err != nil {
+ notification.Status = domain.StatusFailed
+ notification.LastError = fmt.Sprintf("failed to create notifier: %v", err)
+ s.queue.Nack(ctx, msg.ID, false)
+ s.updateNotification(notification)
+ return
+ }
+
+ // Send the notification
+ result, err := notifier.Send(ctx, notification)
+ if err != nil || !result.Success {
+ notification.RetryCount++
+ notification.LastError = result.Error
+ if err != nil {
+ notification.LastError = err.Error()
+ }
+
+ // Check if we should retry
+ if notification.RetryCount < notification.MaxRetries {
+ notification.Status = domain.StatusRetrying
+ s.queue.Nack(ctx, msg.ID, true) // Requeue
+ } else {
+ notification.Status = domain.StatusFailed
+ s.queue.Nack(ctx, msg.ID, false) // Don't requeue
+ }
+ } else {
+ notification.Status = domain.StatusSent
+ now := time.Now()
+ notification.SentAt = &now
+ s.queue.Ack(ctx, msg.ID)
+ }
+
+ s.updateNotification(notification)
+}
+
+// Send queues a notification for delivery
+func (s *NotificationService) Send(ctx context.Context, notification *domain.Notification) (*domain.NotificationResult, error) {
+ // Store the notification
+ s.storeNotification(notification)
+
+ // Enqueue for processing
+ if err := s.queue.Enqueue(ctx, notification); err != nil {
+ return &domain.NotificationResult{
+ NotificationID: notification.ID,
+ Success: false,
+ Error: fmt.Sprintf("failed to enqueue: %v", err),
+ SentAt: time.Now(),
+ }, err
+ }
+
+ return &domain.NotificationResult{
+ NotificationID: notification.ID,
+ Success: true,
+ Message: "notification queued successfully",
+ SentAt: time.Now(),
+ }, nil
+}
+
+// SendBatch queues multiple notifications for delivery
+func (s *NotificationService) SendBatch(ctx context.Context, notifications []*domain.Notification) ([]*domain.NotificationResult, error) {
+ results := make([]*domain.NotificationResult, 0, len(notifications))
+
+ // Store all notifications
+ for _, notification := range notifications {
+ s.storeNotification(notification)
+ }
+
+ // Enqueue batch
+ if err := s.queue.EnqueueBatch(ctx, notifications); err != nil {
+ return nil, fmt.Errorf("failed to enqueue batch: %w", err)
+ }
+
+ // Create results
+ for _, notification := range notifications {
+ results = append(results, &domain.NotificationResult{
+ NotificationID: notification.ID,
+ Success: true,
+ Message: "notification queued successfully",
+ SentAt: time.Now(),
+ })
+ }
+
+ return results, nil
+}
+
+// GetNotification retrieves a notification by ID
+func (s *NotificationService) GetNotification(ctx context.Context, id string) (*domain.Notification, error) {
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+
+ notification, exists := s.notifications[id]
+ if !exists {
+ return nil, fmt.Errorf("notification not found: %s", id)
+ }
+
+ return notification, nil
+}
+
+// ListNotifications retrieves notifications matching the filter
+func (s *NotificationService) ListNotifications(ctx context.Context, filter *domain.NotificationFilter) ([]*domain.Notification, error) {
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+
+ // Simple in-memory filtering
+ var results []*domain.Notification
+
+ for _, notification := range s.notifications {
+ if s.matchesFilter(notification, filter) {
+ results = append(results, notification)
+ }
+ }
+
+ // Apply limit and offset
+ if filter.Offset > 0 && filter.Offset < len(results) {
+ results = results[filter.Offset:]
+ }
+
+ if filter.Limit > 0 && filter.Limit < len(results) {
+ results = results[:filter.Limit]
+ }
+
+ return results, nil
+}
+
+// CancelNotification cancels a pending notification
+func (s *NotificationService) CancelNotification(ctx context.Context, id string) error {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+
+ notification, exists := s.notifications[id]
+ if !exists {
+ return fmt.Errorf("notification not found: %s", id)
+ }
+
+ if notification.Status == domain.StatusSent {
+ return fmt.Errorf("notification already sent")
+ }
+
+ notification.Status = domain.StatusFailed
+ notification.LastError = "cancelled by user"
+
+ return nil
+}
+
+// RetryNotification retries a failed notification
+func (s *NotificationService) RetryNotification(ctx context.Context, id string) (*domain.NotificationResult, error) {
+ notification, err := s.GetNotification(ctx, id)
+ if err != nil {
+ return nil, err
+ }
+
+ if notification.Status == domain.StatusSent {
+ return &domain.NotificationResult{
+ NotificationID: id,
+ Success: false,
+ Error: "notification already sent",
+ SentAt: time.Now(),
+ }, fmt.Errorf("notification already sent")
+ }
+
+ // Reset retry count and status
+ notification.RetryCount = 0
+ notification.Status = domain.StatusPending
+
+ // Re-enqueue
+ return s.Send(ctx, notification)
+}
+
+// GetStats returns notification statistics
+func (s *NotificationService) GetStats(ctx context.Context) (*domain.NotificationStats, error) {
+ s.mu.RLock()
+ defer s.mu.RUnlock()
+
+ stats := &domain.NotificationStats{
+ ByType: make(map[string]int64),
+ ByStatus: make(map[string]int64),
+ }
+
+ for _, notification := range s.notifications {
+ switch notification.Status {
+ case domain.StatusSent:
+ stats.TotalSent++
+ case domain.StatusFailed:
+ stats.TotalFailed++
+ case domain.StatusPending:
+ stats.TotalPending++
+ case domain.StatusQueued:
+ stats.TotalQueued++
+ }
+
+ stats.ByType[string(notification.Type)]++
+ stats.ByStatus[string(notification.Status)]++
+ }
+
+ return stats, nil
+}
+
+// storeNotification stores a notification in memory
+func (s *NotificationService) storeNotification(notification *domain.Notification) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ s.notifications[notification.ID] = notification
+}
+
+// updateNotification updates a notification in memory
+func (s *NotificationService) updateNotification(notification *domain.Notification) {
+ s.mu.Lock()
+ defer s.mu.Unlock()
+ s.notifications[notification.ID] = notification
+}
+
+// matchesFilter checks if a notification matches the filter
+func (s *NotificationService) matchesFilter(notification *domain.Notification, filter *domain.NotificationFilter) bool {
+ if filter == nil {
+ return true
+ }
+
+ // Check IDs
+ if len(filter.IDs) > 0 {
+ found := false
+ for _, id := range filter.IDs {
+ if notification.ID == id {
+ found = true
+ break
+ }
+ }
+ if !found {
+ return false
+ }
+ }
+
+ // Check types
+ if len(filter.Types) > 0 {
+ found := false
+ for _, t := range filter.Types {
+ if notification.Type == t {
+ found = true
+ break
+ }
+ }
+ if !found {
+ return false
+ }
+ }
+
+ // Check statuses
+ if len(filter.Statuses) > 0 {
+ found := false
+ for _, s := range filter.Statuses {
+ if notification.Status == s {
+ found = true
+ break
+ }
+ }
+ if !found {
+ return false
+ }
+ }
+
+ // Check recipients
+ if len(filter.Recipients) > 0 {
+ found := false
+ for _, fr := range filter.Recipients {
+ for _, nr := range notification.Recipients {
+ if fr == nr {
+ found = true
+ break
+ }
+ }
+ if found {
+ break
+ }
+ }
+ if !found {
+ return false
+ }
+ }
+
+ // Check time ranges
+ if filter.CreatedAfter != nil && notification.CreatedAt.Before(*filter.CreatedAfter) {
+ return false
+ }
+
+ if filter.CreatedBefore != nil && notification.CreatedAt.After(*filter.CreatedBefore) {
+ return false
+ }
+
+ return true
+}
diff --git a/k8s/configmap.yaml b/k8s/configmap.yaml
new file mode 100644
index 0000000..8a837cf
--- /dev/null
+++ b/k8s/configmap.yaml
@@ -0,0 +1,43 @@
+apiVersion: v1
+kind: ConfigMap
+metadata:
+ name: notifier-config
+ labels:
+ app: notifier
+data:
+ config.yaml: |
+ server:
+ grpc_port: 50051
+ rest_port: 8080
+ host: "0.0.0.0"
+ mode: "both"
+
+ queue:
+ type: "local"
+ max_size: 10000
+ worker_count: 10
+ retry_attempts: 3
+ retry_backoff: "exponential"
+ local:
+ buffer_size: 1000
+ persist_to_disk: false
+
+ notifiers:
+ stdout: true
+
+ logging:
+ level: "info"
+ format: "json"
+ output_path: "stdout"
+
+ metrics:
+ enabled: true
+ port: 9090
+ path: "/metrics"
+ prometheus_enabled: true
+
+ health_check:
+ enabled: true
+ port: 8081
+ path: "/health"
+ interval: 30
diff --git a/k8s/deployment.yaml b/k8s/deployment.yaml
new file mode 100644
index 0000000..3dc534e
--- /dev/null
+++ b/k8s/deployment.yaml
@@ -0,0 +1,97 @@
+apiVersion: apps/v1
+kind: Deployment
+metadata:
+ name: notifier
+ labels:
+ app: notifier
+ version: v1
+spec:
+ replicas: 3
+ selector:
+ matchLabels:
+ app: notifier
+ template:
+ metadata:
+ labels:
+ app: notifier
+ version: v1
+ spec:
+ serviceAccountName: notifier
+ containers:
+ - name: notifier
+ image: notifier:latest
+ imagePullPolicy: Always
+ ports:
+ - name: rest
+ containerPort: 8080
+ protocol: TCP
+ - name: grpc
+ containerPort: 50051
+ protocol: TCP
+ - name: metrics
+ containerPort: 9090
+ protocol: TCP
+ - name: health
+ containerPort: 8081
+ protocol: TCP
+ env:
+ - name: NOTIFIER_SERVER_MODE
+ value: "both"
+ - name: NOTIFIER_LOGGING_LEVEL
+ value: "info"
+ - name: NOTIFIER_LOGGING_FORMAT
+ value: "json"
+ - name: NOTIFIER_QUEUE_TYPE
+ value: "local"
+ volumeMounts:
+ - name: config
+ mountPath: /app/config.yaml
+ subPath: config.yaml
+ readOnly: true
+ - name: queue-storage
+ mountPath: /var/lib/notifier
+ resources:
+ requests:
+ cpu: 100m
+ memory: 128Mi
+ limits:
+ cpu: 500m
+ memory: 512Mi
+ livenessProbe:
+ httpGet:
+ path: /health
+ port: health
+ initialDelaySeconds: 30
+ periodSeconds: 10
+ timeoutSeconds: 5
+ failureThreshold: 3
+ readinessProbe:
+ httpGet:
+ path: /health
+ port: health
+ initialDelaySeconds: 10
+ periodSeconds: 5
+ timeoutSeconds: 3
+ failureThreshold: 3
+ securityContext:
+ runAsNonRoot: true
+ runAsUser: 1000
+ allowPrivilegeEscalation: false
+ readOnlyRootFilesystem: false
+ capabilities:
+ drop:
+ - ALL
+ volumes:
+ - name: config
+ configMap:
+ name: notifier-config
+ - name: queue-storage
+ emptyDir: {}
+ restartPolicy: Always
+---
+apiVersion: v1
+kind: ServiceAccount
+metadata:
+ name: notifier
+ labels:
+ app: notifier
diff --git a/k8s/hpa.yaml b/k8s/hpa.yaml
new file mode 100644
index 0000000..4ff488e
--- /dev/null
+++ b/k8s/hpa.yaml
@@ -0,0 +1,43 @@
+apiVersion: autoscaling/v2
+kind: HorizontalPodAutoscaler
+metadata:
+ name: notifier-hpa
+ labels:
+ app: notifier
+spec:
+ scaleTargetRef:
+ apiVersion: apps/v1
+ kind: Deployment
+ name: notifier
+ minReplicas: 3
+ maxReplicas: 10
+ metrics:
+ - type: Resource
+ resource:
+ name: cpu
+ target:
+ type: Utilization
+ averageUtilization: 70
+ - type: Resource
+ resource:
+ name: memory
+ target:
+ type: Utilization
+ averageUtilization: 80
+ behavior:
+ scaleDown:
+ stabilizationWindowSeconds: 300
+ policies:
+ - type: Percent
+ value: 50
+ periodSeconds: 60
+ scaleUp:
+ stabilizationWindowSeconds: 60
+ policies:
+ - type: Percent
+ value: 100
+ periodSeconds: 60
+ - type: Pods
+ value: 2
+ periodSeconds: 60
+ selectPolicy: Max
diff --git a/k8s/ingress.yaml b/k8s/ingress.yaml
new file mode 100644
index 0000000..078f5c4
--- /dev/null
+++ b/k8s/ingress.yaml
@@ -0,0 +1,26 @@
+apiVersion: networking.k8s.io/v1
+kind: Ingress
+metadata:
+ name: notifier-ingress
+ labels:
+ app: notifier
+ annotations:
+ kubernetes.io/ingress.class: nginx
+ cert-manager.io/cluster-issuer: letsencrypt-prod
+ nginx.ingress.kubernetes.io/ssl-redirect: "true"
+spec:
+ tls:
+ - hosts:
+ - notifier.example.com
+ secretName: notifier-tls
+ rules:
+ - host: notifier.example.com
+ http:
+ paths:
+ - path: /
+ pathType: Prefix
+ backend:
+ service:
+ name: notifier-rest
+ port:
+ number: 8080
diff --git a/k8s/kustomization.yaml b/k8s/kustomization.yaml
new file mode 100644
index 0000000..476f0d5
--- /dev/null
+++ b/k8s/kustomization.yaml
@@ -0,0 +1,20 @@
+apiVersion: kustomize.config.k8s.io/v1beta1
+kind: Kustomization
+
+namespace: notifier
+
+resources:
+ - deployment.yaml
+ - service.yaml
+ - configmap.yaml
+ - ingress.yaml
+ - hpa.yaml
+
+commonLabels:
+ app: notifier
+ managed-by: kustomize
+
+images:
+ - name: notifier
+ newName: your-registry/notifier
+ newTag: latest
diff --git a/k8s/secret-example.yaml b/k8s/secret-example.yaml
new file mode 100644
index 0000000..93b0114
--- /dev/null
+++ b/k8s/secret-example.yaml
@@ -0,0 +1,45 @@
+# Example secret for notifier credentials
+# DO NOT commit actual secrets to version control
+# Use sealed-secrets, external-secrets, or vault in production
+
+apiVersion: v1
+kind: Secret
+metadata:
+ name: notifier-secrets
+ labels:
+ app: notifier
+type: Opaque
+stringData:
+ # SMTP credentials
+ smtp-username: "your-email@gmail.com"
+ smtp-password: "your-app-password"
+
+ # Slack webhook URL
+ slack-webhook-url: "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"
+
+ # Ntfy token
+ ntfy-token: "tk_your_token"
+
+ # Kafka credentials (if using Kafka)
+ kafka-username: "kafka-user"
+ kafka-password: "kafka-password"
+
+---
+# Example of using secrets in deployment
+# Add this to deployment.yaml under spec.template.spec.containers[0].env
+
+# - name: NOTIFIER_NOTIFIERS_SMTP_USERNAME
+# valueFrom:
+# secretKeyRef:
+# name: notifier-secrets
+# key: smtp-username
+# - name: NOTIFIER_NOTIFIERS_SMTP_PASSWORD
+# valueFrom:
+# secretKeyRef:
+# name: notifier-secrets
+# key: smtp-password
+# - name: NOTIFIER_NOTIFIERS_SLACK_WEBHOOK_URL
+# valueFrom:
+# secretKeyRef:
+# name: notifier-secrets
+# key: slack-webhook-url
diff --git a/k8s/service.yaml b/k8s/service.yaml
new file mode 100644
index 0000000..d9f584a
--- /dev/null
+++ b/k8s/service.yaml
@@ -0,0 +1,50 @@
+apiVersion: v1
+kind: Service
+metadata:
+ name: notifier-rest
+ labels:
+ app: notifier
+ service: rest
+spec:
+ type: ClusterIP
+ ports:
+ - port: 8080
+ targetPort: rest
+ protocol: TCP
+ name: http
+ selector:
+ app: notifier
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: notifier-grpc
+ labels:
+ app: notifier
+ service: grpc
+spec:
+ type: ClusterIP
+ ports:
+ - port: 50051
+ targetPort: grpc
+ protocol: TCP
+ name: grpc
+ selector:
+ app: notifier
+---
+apiVersion: v1
+kind: Service
+metadata:
+ name: notifier-metrics
+ labels:
+ app: notifier
+ service: metrics
+spec:
+ type: ClusterIP
+ ports:
+ - port: 9090
+ targetPort: metrics
+ protocol: TCP
+ name: metrics
+ selector:
+ app: notifier