Files
warmbly/internal/infrastructure/pubsub/client.go
T
Matthew Meszaros d227038ca0 ci: drop unused/unconvert/gosimple + shadow/nilness, run gofmt
Disable the linters that fire on legacy code without flagging real
bugs: `unused` (orphan repos kept for future feature flags),
`unconvert` (defensive type conversions), `gosimple` (style
suggestions in code we don't want to touch).

govet: disable `shadow` (idiomatic `err :=` re-decls in transaction
patterns) and `nilness` (legitimate defensive nil checks that look
tautological to the analyzer).

Ran `gofmt -w internal/ cmd/` — every Go file now passes
gofmt -l with no output.

Kept: govet, staticcheck, ineffassign, typecheck, bodyclose, noctx,
sqlclosecheck, gofmt, goimports, misspell — the real-bug checks.
2026-05-23 16:54:12 +00:00

261 lines
6.5 KiB
Go

package pubsub
import (
"context"
"encoding/json"
"fmt"
"time"
"cloud.google.com/go/pubsub"
"github.com/getsentry/sentry-go"
"github.com/google/uuid"
)
// Client wraps Google Pub/Sub for real-time streaming
type Client struct {
client *pubsub.Client
projectID string
}
// NewClient creates a new Pub/Sub client
func NewClient(ctx context.Context, projectID string) (*Client, error) {
client, err := pubsub.NewClient(ctx, projectID)
if err != nil {
return nil, fmt.Errorf("failed to create pubsub client: %w", err)
}
return &Client{
client: client,
projectID: projectID,
}, nil
}
// Close closes the Pub/Sub client
func (c *Client) Close() error {
return c.client.Close()
}
// getTopic gets or creates a topic
func (c *Client) getTopic(ctx context.Context, topicID string) (*pubsub.Topic, error) {
topic := c.client.Topic(topicID)
exists, err := topic.Exists(ctx)
if err != nil {
return nil, err
}
if !exists {
topic, err = c.client.CreateTopic(ctx, topicID)
if err != nil {
return nil, err
}
}
return topic, nil
}
// Publish publishes a message to a topic
func (c *Client) Publish(ctx context.Context, topicID string, data interface{}, attributes map[string]string) error {
topic, err := c.getTopic(ctx, topicID)
if err != nil {
return fmt.Errorf("failed to get topic: %w", err)
}
jsonData, err := json.Marshal(data)
if err != nil {
return fmt.Errorf("failed to marshal data: %w", err)
}
result := topic.Publish(ctx, &pubsub.Message{
Data: jsonData,
Attributes: attributes,
})
_, err = result.Get(ctx)
if err != nil {
return fmt.Errorf("failed to publish message: %w", err)
}
return nil
}
// StreamingPublisher handles real-time streaming to users
type StreamingPublisher struct {
client *Client
}
// NewStreamingPublisher creates a new streaming publisher
func NewStreamingPublisher(client *Client) *StreamingPublisher {
return &StreamingPublisher{
client: client,
}
}
// Topic names for real-time updates
const (
TopicTaskStatus = "task-status"
TopicCampaignUpdate = "campaign-update"
TopicWarmupUpdate = "warmup-update"
TopicEmailError = "email-error"
TopicEmailWarning = "email-warning"
)
// Event types
type EventType string
const (
EventTaskCreated EventType = "TASK_CREATED"
EventTaskStarted EventType = "TASK_STARTED"
EventTaskCompleted EventType = "TASK_COMPLETED"
EventTaskFailed EventType = "TASK_FAILED"
EventEmailSent EventType = "EMAIL_SENT"
EventEmailFailed EventType = "EMAIL_FAILED"
EventError EventType = "ERROR"
EventWarning EventType = "WARNING"
)
// StreamEvent represents a real-time event for users
type StreamEvent struct {
EventType EventType `json:"event_type"`
UserID string `json:"user_id"`
TaskID string `json:"task_id,omitempty"`
EmailID string `json:"email_id,omitempty"`
Message string `json:"message,omitempty"`
Data any `json:"data,omitempty"`
Timestamp time.Time `json:"timestamp"`
}
// PublishTaskStatus publishes a task status update
func (p *StreamingPublisher) PublishTaskStatus(ctx context.Context, userID string, taskID uuid.UUID, eventType EventType, message string, data any) {
if p.client == nil {
return
}
event := StreamEvent{
EventType: eventType,
UserID: userID,
TaskID: taskID.String(),
Message: message,
Data: data,
Timestamp: time.Now(),
}
attrs := map[string]string{
"user_id": userID,
"event_type": string(eventType),
}
if err := p.client.Publish(ctx, TopicTaskStatus, event, attrs); err != nil {
sentry.CaptureException(fmt.Errorf("failed to publish task status: %w", err))
}
}
// PublishEmailError publishes an email error for user visibility
func (p *StreamingPublisher) PublishEmailError(ctx context.Context, userID string, emailID uuid.UUID, taskID uuid.UUID, errorTitle, errorMessage string) {
if p.client == nil {
return
}
event := StreamEvent{
EventType: EventError,
UserID: userID,
TaskID: taskID.String(),
EmailID: emailID.String(),
Message: errorTitle,
Data: map[string]string{
"title": errorTitle,
"message": errorMessage,
},
Timestamp: time.Now(),
}
attrs := map[string]string{
"user_id": userID,
"email_id": emailID.String(),
"event_type": string(EventError),
}
if err := p.client.Publish(ctx, TopicEmailError, event, attrs); err != nil {
sentry.CaptureException(fmt.Errorf("failed to publish email error: %w", err))
}
}
// PublishEmailWarning publishes an email warning for user visibility
func (p *StreamingPublisher) PublishEmailWarning(ctx context.Context, userID string, emailID uuid.UUID, warningTitle, warningMessage string) {
if p.client == nil {
return
}
event := StreamEvent{
EventType: EventWarning,
UserID: userID,
EmailID: emailID.String(),
Message: warningTitle,
Data: map[string]string{
"title": warningTitle,
"message": warningMessage,
},
Timestamp: time.Now(),
}
attrs := map[string]string{
"user_id": userID,
"email_id": emailID.String(),
"event_type": string(EventWarning),
}
if err := p.client.Publish(ctx, TopicEmailWarning, event, attrs); err != nil {
sentry.CaptureException(fmt.Errorf("failed to publish email warning: %w", err))
}
}
// PublishCampaignProgress publishes campaign progress update
func (p *StreamingPublisher) PublishCampaignProgress(ctx context.Context, userID string, campaignID uuid.UUID, progress any) {
if p.client == nil {
return
}
event := StreamEvent{
EventType: EventTaskCompleted,
UserID: userID,
Message: "Campaign progress updated",
Data: progress,
Timestamp: time.Now(),
}
attrs := map[string]string{
"user_id": userID,
"campaign_id": campaignID.String(),
"event_type": "CAMPAIGN_PROGRESS",
}
if err := p.client.Publish(ctx, TopicCampaignUpdate, event, attrs); err != nil {
sentry.CaptureException(fmt.Errorf("failed to publish campaign progress: %w", err))
}
}
// PublishWarmupStats publishes warmup statistics update
func (p *StreamingPublisher) PublishWarmupStats(ctx context.Context, userID string, emailID uuid.UUID, stats any) {
if p.client == nil {
return
}
event := StreamEvent{
EventType: EventTaskCompleted,
UserID: userID,
EmailID: emailID.String(),
Message: "Warmup statistics updated",
Data: stats,
Timestamp: time.Now(),
}
attrs := map[string]string{
"user_id": userID,
"email_id": emailID.String(),
"event_type": "WARMUP_STATS",
}
if err := p.client.Publish(ctx, TopicWarmupUpdate, event, attrs); err != nil {
sentry.CaptureException(fmt.Errorf("failed to publish warmup stats: %w", err))
}
}