Files
notifier/api/grpc/handler.go
T

352 lines
10 KiB
Go

package grpc
import (
"context"
"fmt"
pb "github.com/igodwin/notifier/api/grpc/pb"
"github.com/igodwin/notifier/internal/domain"
"google.golang.org/protobuf/types/known/timestamppb"
)
// NotifierHandler implements the gRPC NotifierService
type NotifierHandler struct {
pb.UnimplementedNotifierServiceServer
service domain.NotificationService
}
// NewNotifierHandler creates a new gRPC handler
func NewNotifierHandler(svc domain.NotificationService) *NotifierHandler {
return &NotifierHandler{
service: svc,
}
}
// HealthCheck verifies the service is operational
func (h *NotifierHandler) HealthCheck(ctx context.Context, req *pb.HealthCheckRequest) (*pb.HealthCheckResponse, error) {
// TODO: Implement proper health check logic
return &pb.HealthCheckResponse{
Healthy: true,
Status: "ok",
Components: map[string]string{
"service": "running",
},
}, nil
}
// SendNotification sends a single notification
func (h *NotifierHandler) SendNotification(ctx context.Context, req *pb.SendNotificationRequest) (*pb.SendNotificationResponse, error) {
// Convert proto notification type to domain type
notifType := convertProtoTypeToDomain(req.Type)
// Build notification
notification := &domain.Notification{
Type: notifType,
Account: req.Account,
Priority: domain.Priority(req.Priority),
Subject: req.Subject,
Body: req.Body,
Recipients: req.Recipients,
Metadata: convertStringMapToInterface(req.Metadata),
MaxRetries: int(req.MaxRetries),
}
if req.ScheduledFor != nil {
scheduledTime := req.ScheduledFor.AsTime()
notification.ScheduledFor = &scheduledTime
}
// Send notification
result, err := h.service.Send(ctx, notification)
if err != nil {
return &pb.SendNotificationResponse{
Result: &pb.NotificationResult{
Success: false,
Error: err.Error(),
},
}, nil
}
// Convert result to proto
return &pb.SendNotificationResponse{
Result: &pb.NotificationResult{
NotificationId: result.NotificationID,
Success: result.Success,
Message: result.Message,
SentAt: timestamppb.New(result.SentAt),
},
}, nil
}
// SendBatchNotifications sends multiple notifications
func (h *NotifierHandler) SendBatchNotifications(ctx context.Context, req *pb.SendBatchNotificationsRequest) (*pb.SendBatchNotificationsResponse, error) {
var results []*pb.NotificationResult
for _, notifReq := range req.Notifications {
resp, err := h.SendNotification(ctx, notifReq)
if err != nil {
results = append(results, &pb.NotificationResult{
Success: false,
Error: err.Error(),
})
} else {
results = append(results, resp.Result)
}
}
return &pb.SendBatchNotificationsResponse{
Results: results,
}, nil
}
// GetNotification retrieves a notification by ID
func (h *NotifierHandler) GetNotification(ctx context.Context, req *pb.GetNotificationRequest) (*pb.GetNotificationResponse, error) {
notification, err := h.service.GetNotification(ctx, req.Id)
if err != nil {
return nil, err
}
return &pb.GetNotificationResponse{
Notification: convertDomainToProtoNotification(notification),
}, nil
}
// ListNotifications retrieves notifications matching a filter
func (h *NotifierHandler) ListNotifications(ctx context.Context, req *pb.ListNotificationsRequest) (*pb.ListNotificationsResponse, error) {
// Convert proto filter to domain filter
filter := convertProtoFilterToDomain(req.Filter)
notifications, err := h.service.ListNotifications(ctx, filter)
if err != nil {
return nil, err
}
protoNotifications := make([]*pb.Notification, len(notifications))
for i, notif := range notifications {
protoNotifications[i] = convertDomainToProtoNotification(notif)
}
return &pb.ListNotificationsResponse{
Notifications: protoNotifications,
Total: int64(len(notifications)),
}, nil
}
// CancelNotification cancels a pending notification
func (h *NotifierHandler) CancelNotification(ctx context.Context, req *pb.CancelNotificationRequest) (*pb.CancelNotificationResponse, error) {
err := h.service.CancelNotification(ctx, req.Id)
if err != nil {
return &pb.CancelNotificationResponse{
Success: false,
Message: err.Error(),
}, nil
}
return &pb.CancelNotificationResponse{
Success: true,
Message: "notification cancelled successfully",
}, nil
}
// RetryNotification retries a failed notification
func (h *NotifierHandler) RetryNotification(ctx context.Context, req *pb.RetryNotificationRequest) (*pb.RetryNotificationResponse, error) {
result, err := h.service.RetryNotification(ctx, req.Id)
if err != nil {
return &pb.RetryNotificationResponse{
Result: &pb.NotificationResult{
Success: false,
Error: err.Error(),
},
}, nil
}
return &pb.RetryNotificationResponse{
Result: &pb.NotificationResult{
NotificationId: result.NotificationID,
Success: result.Success,
Message: result.Message,
SentAt: timestamppb.New(result.SentAt),
},
}, nil
}
// GetStats returns notification statistics
func (h *NotifierHandler) GetStats(ctx context.Context, req *pb.GetStatsRequest) (*pb.GetStatsResponse, error) {
stats, err := h.service.GetStats(ctx)
if err != nil {
return nil, err
}
return &pb.GetStatsResponse{
TotalSent: stats.TotalSent,
TotalFailed: stats.TotalFailed,
TotalPending: stats.TotalPending,
TotalQueued: stats.TotalQueued,
ByType: stats.ByType,
ByStatus: stats.ByStatus,
}, nil
}
// Helper functions to convert between proto and domain types
// convertStringMapToInterface converts proto's map[string]string to domain's map[string]interface{}
func convertStringMapToInterface(m map[string]string) map[string]interface{} {
if m == nil {
return nil
}
result := make(map[string]interface{}, len(m))
for k, v := range m {
result[k] = v
}
return result
}
// convertInterfaceMapToString converts domain's map[string]interface{} to proto's map[string]string
func convertInterfaceMapToString(m map[string]interface{}) map[string]string {
if m == nil {
return nil
}
result := make(map[string]string, len(m))
for k, v := range m {
if v != nil {
result[k] = fmt.Sprint(v)
}
}
return result
}
func convertProtoTypeToDomain(protoType pb.NotificationType) domain.NotificationType {
switch protoType {
case pb.NotificationType_NOTIFICATION_TYPE_EMAIL:
return domain.TypeEmail
case pb.NotificationType_NOTIFICATION_TYPE_SLACK:
return domain.TypeSlack
case pb.NotificationType_NOTIFICATION_TYPE_NTFY:
return domain.TypeNtfy
case pb.NotificationType_NOTIFICATION_TYPE_STDOUT:
return domain.TypeStdout
default:
return domain.TypeStdout
}
}
func convertDomainToProtoType(domainType domain.NotificationType) pb.NotificationType {
switch domainType {
case domain.TypeEmail:
return pb.NotificationType_NOTIFICATION_TYPE_EMAIL
case domain.TypeSlack:
return pb.NotificationType_NOTIFICATION_TYPE_SLACK
case domain.TypeNtfy:
return pb.NotificationType_NOTIFICATION_TYPE_NTFY
case domain.TypeStdout:
return pb.NotificationType_NOTIFICATION_TYPE_STDOUT
default:
return pb.NotificationType_NOTIFICATION_TYPE_UNSPECIFIED
}
}
func convertDomainToProtoStatus(status domain.NotificationStatus) pb.NotificationStatus {
switch status {
case domain.StatusPending:
return pb.NotificationStatus_NOTIFICATION_STATUS_PENDING
case domain.StatusQueued:
return pb.NotificationStatus_NOTIFICATION_STATUS_QUEUED
case domain.StatusProcessing:
return pb.NotificationStatus_NOTIFICATION_STATUS_PROCESSING
case domain.StatusSent:
return pb.NotificationStatus_NOTIFICATION_STATUS_SENT
case domain.StatusFailed:
return pb.NotificationStatus_NOTIFICATION_STATUS_FAILED
case domain.StatusRetrying:
return pb.NotificationStatus_NOTIFICATION_STATUS_RETRYING
default:
return pb.NotificationStatus_NOTIFICATION_STATUS_UNSPECIFIED
}
}
func convertDomainToProtoNotification(notif *domain.Notification) *pb.Notification {
protoNotif := &pb.Notification{
Id: notif.ID,
Type: convertDomainToProtoType(notif.Type),
Account: notif.Account,
Priority: pb.Priority(notif.Priority),
Status: convertDomainToProtoStatus(notif.Status),
Subject: notif.Subject,
Body: notif.Body,
Recipients: notif.Recipients,
Metadata: convertInterfaceMapToString(notif.Metadata),
CreatedAt: timestamppb.New(notif.CreatedAt),
RetryCount: int32(notif.RetryCount),
MaxRetries: int32(notif.MaxRetries),
LastError: notif.LastError,
}
// Handle optional timestamp fields
if notif.ScheduledFor != nil {
protoNotif.ScheduledFor = timestamppb.New(*notif.ScheduledFor)
}
if notif.SentAt != nil {
protoNotif.SentAt = timestamppb.New(*notif.SentAt)
}
return protoNotif
}
func convertProtoFilterToDomain(filter *pb.NotificationFilter) *domain.NotificationFilter {
if filter == nil {
return &domain.NotificationFilter{}
}
// Convert proto types to domain types
var types []domain.NotificationType
for _, protoType := range filter.Types {
types = append(types, convertProtoTypeToDomain(protoType))
}
// Convert proto statuses to domain statuses
var statuses []domain.NotificationStatus
for _, protoStatus := range filter.Statuses {
statuses = append(statuses, convertProtoStatusToDomain(protoStatus))
}
domainFilter := &domain.NotificationFilter{
IDs: filter.Ids,
Types: types,
Statuses: statuses,
Recipients: filter.Recipients,
Limit: int(filter.Limit),
Offset: int(filter.Offset),
}
if filter.CreatedAfter != nil {
createdAfter := filter.CreatedAfter.AsTime()
domainFilter.CreatedAfter = &createdAfter
}
if filter.CreatedBefore != nil {
createdBefore := filter.CreatedBefore.AsTime()
domainFilter.CreatedBefore = &createdBefore
}
return domainFilter
}
func convertProtoStatusToDomain(protoStatus pb.NotificationStatus) domain.NotificationStatus {
switch protoStatus {
case pb.NotificationStatus_NOTIFICATION_STATUS_PENDING:
return domain.StatusPending
case pb.NotificationStatus_NOTIFICATION_STATUS_QUEUED:
return domain.StatusQueued
case pb.NotificationStatus_NOTIFICATION_STATUS_PROCESSING:
return domain.StatusProcessing
case pb.NotificationStatus_NOTIFICATION_STATUS_SENT:
return domain.StatusSent
case pb.NotificationStatus_NOTIFICATION_STATUS_FAILED:
return domain.StatusFailed
case pb.NotificationStatus_NOTIFICATION_STATUS_RETRYING:
return domain.StatusRetrying
default:
return domain.StatusPending
}
}