mirror of
https://github.com/warmbly/warmbly.git
synced 2026-08-19 08:01:16 +00:00
d227038ca0
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.
261 lines
6.5 KiB
Go
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))
|
|
}
|
|
}
|