Files
warmbly/internal/infrastructure/pubsub/events.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

347 lines
9.8 KiB
Go

package pubsub
import (
"context"
"time"
"github.com/google/uuid"
)
// Additional Topics for realtime updates
const (
TopicUserEvents = "user-events"
TopicEmailInbox = "email-inbox"
TopicBulkOps = "bulk-operations"
TopicContactsSync = "contacts-sync"
)
// Additional Event Types
const (
// Email inbox events
EventEmailReceived EventType = "EMAIL_RECEIVED"
EventEmailUpdated EventType = "EMAIL_UPDATED"
EventEmailDeleted EventType = "EMAIL_DELETED"
// Contact events
EventContactCreated EventType = "CONTACT_CREATED"
EventContactUpdated EventType = "CONTACT_UPDATED"
EventContactDeleted EventType = "CONTACT_DELETED"
EventContactsReload EventType = "CONTACTS_RELOAD"
// Campaign events
EventCampaignCreated EventType = "CAMPAIGN_CREATED"
EventCampaignUpdated EventType = "CAMPAIGN_UPDATED"
EventCampaignDeleted EventType = "CAMPAIGN_DELETED"
EventCampaignStarted EventType = "CAMPAIGN_STARTED"
EventCampaignPaused EventType = "CAMPAIGN_PAUSED"
EventCampaignCompleted EventType = "CAMPAIGN_COMPLETED"
// Email account events
EventAccountConnected EventType = "ACCOUNT_CONNECTED"
EventAccountDisconnected EventType = "ACCOUNT_DISCONNECTED"
EventAccountError EventType = "ACCOUNT_ERROR"
EventAccountSynced EventType = "ACCOUNT_SYNCED"
// Bulk operation events
EventBulkStarted EventType = "BULK_STARTED"
EventBulkProgress EventType = "BULK_PROGRESS"
EventBulkCompleted EventType = "BULK_COMPLETED"
EventBulkFailed EventType = "BULK_FAILED"
// Tracking events (from Rust tracking service)
EventEmailOpened EventType = "EMAIL_OPENED"
EventEmailClicked EventType = "EMAIL_CLICKED"
// Task progress events
EventTaskProgress EventType = "TASK_PROGRESS"
)
// BaseEvent contains common fields for all events
type BaseEvent struct {
EventType EventType `json:"event_type"`
UserID string `json:"user_id"`
Timestamp time.Time `json:"timestamp"`
}
// EmailInboxEvent for new/updated emails
type EmailInboxEvent struct {
BaseEvent
EmailAccountID string `json:"email_account_id"`
MessageID string `json:"message_id"`
ThreadID string `json:"thread_id,omitempty"`
Subject string `json:"subject,omitempty"`
From string `json:"from,omitempty"`
Preview string `json:"preview,omitempty"`
}
// ContactEvent for contact changes
type ContactEvent struct {
BaseEvent
ContactID string `json:"contact_id"`
Email string `json:"email,omitempty"`
ListID string `json:"list_id,omitempty"`
}
// BulkOperationEvent for bulk operation signals
type BulkOperationEvent struct {
BaseEvent
OperationID string `json:"operation_id"`
OperationType string `json:"operation_type"`
EntityType string `json:"entity_type"`
TotalItems int `json:"total_items,omitempty"`
ProcessedItems int `json:"processed_items,omitempty"`
FailedItems int `json:"failed_items,omitempty"`
Progress float64 `json:"progress,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
}
// CampaignEvent for campaign changes
type CampaignEvent struct {
BaseEvent
CampaignID string `json:"campaign_id"`
Name string `json:"name,omitempty"`
Status string `json:"status,omitempty"`
Progress *CampaignProgressData `json:"progress,omitempty"`
}
// CampaignProgressData for campaign progress updates
type CampaignProgressData struct {
TotalContacts int `json:"total_contacts"`
EmailsSent int `json:"emails_sent"`
EmailsOpened int `json:"emails_opened"`
EmailsClicked int `json:"emails_clicked"`
EmailsReplied int `json:"emails_replied"`
EmailsBounced int `json:"emails_bounced"`
}
// AccountEvent for email account status changes
type AccountEvent struct {
BaseEvent
EmailAccountID string `json:"email_account_id"`
Email string `json:"email"`
Provider string `json:"provider,omitempty"`
Status string `json:"status,omitempty"`
ErrorCode string `json:"error_code,omitempty"`
ErrorMessage string `json:"error_message,omitempty"`
}
// WarmupStatsEvent for warmup statistics updates
type WarmupStatsEvent struct {
BaseEvent
EmailAccountID string `json:"email_account_id"`
Date string `json:"date"`
EmailsSent int `json:"emails_sent"`
EmailsReplied int `json:"emails_replied"`
TargetVolume int `json:"target_volume"`
}
// TrackingEventPayload for email open/click tracking events
type TrackingEventPayload struct {
BaseEvent
CampaignID string `json:"campaign_id"`
ContactID string `json:"contact_id,omitempty"`
ContactEmail string `json:"contact_email,omitempty"`
SequenceID string `json:"sequence_id,omitempty"`
OriginalURL string `json:"original_url,omitempty"` // For click events
}
// TaskProgressEvent for detailed campaign task progress
type TaskProgressEvent struct {
BaseEvent
CampaignID string `json:"campaign_id"`
TaskID string `json:"task_id"`
Status string `json:"status"` // pending, active, completed, failed
ContactID string `json:"contact_id"`
ContactEmail string `json:"contact_email"`
ContactName string `json:"contact_name"`
SequenceID string `json:"sequence_id"`
SequenceName string `json:"sequence_name"`
SequenceIndex int `json:"sequence_index"`
Progress int `json:"progress"` // Percentage 0-100
TotalContacts int `json:"total_contacts"`
ProcessedCount int `json:"processed_count"`
}
// New publish methods
// PublishEmailReceived notifies user of new email
func (p *StreamingPublisher) PublishEmailReceived(ctx context.Context, event *EmailInboxEvent) {
if p.client == nil {
return
}
event.EventType = EventEmailReceived
event.Timestamp = time.Now()
attrs := map[string]string{
"user_id": event.UserID,
"email_id": event.EmailAccountID,
"event_type": string(EventEmailReceived),
}
if err := p.client.Publish(ctx, TopicEmailInbox, event, attrs); err != nil {
// Log error but don't fail
}
}
// PublishContactsReload signals frontend to reload contacts
func (p *StreamingPublisher) PublishContactsReload(ctx context.Context, userID, operationID string) {
if p.client == nil {
return
}
event := &BulkOperationEvent{
BaseEvent: BaseEvent{
EventType: EventContactsReload,
UserID: userID,
Timestamp: time.Now(),
},
OperationID: operationID,
EntityType: "contacts",
}
attrs := map[string]string{
"user_id": userID,
"event_type": string(EventContactsReload),
}
if err := p.client.Publish(ctx, TopicBulkOps, event, attrs); err != nil {
// Log error but don't fail
}
}
// PublishBulkProgress sends bulk operation progress update
func (p *StreamingPublisher) PublishBulkProgress(ctx context.Context, event *BulkOperationEvent) {
if p.client == nil {
return
}
event.Timestamp = time.Now()
attrs := map[string]string{
"user_id": event.UserID,
"operation_id": event.OperationID,
"event_type": string(event.EventType),
}
if err := p.client.Publish(ctx, TopicBulkOps, event, attrs); err != nil {
// Log error but don't fail
}
}
// PublishCampaignEvent sends campaign event
func (p *StreamingPublisher) PublishCampaignEvent(ctx context.Context, event *CampaignEvent) {
if p.client == nil {
return
}
event.Timestamp = time.Now()
attrs := map[string]string{
"user_id": event.UserID,
"campaign_id": event.CampaignID,
"event_type": string(event.EventType),
}
if err := p.client.Publish(ctx, TopicCampaignUpdate, event, attrs); err != nil {
// Log error but don't fail
}
}
// PublishAccountEvent sends email account event
func (p *StreamingPublisher) PublishAccountEvent(ctx context.Context, event *AccountEvent) {
if p.client == nil {
return
}
event.Timestamp = time.Now()
attrs := map[string]string{
"user_id": event.UserID,
"account_id": event.EmailAccountID,
"event_type": string(event.EventType),
}
topicID := TopicUserEvents
if event.EventType == EventAccountError {
topicID = TopicEmailError
}
if err := p.client.Publish(ctx, topicID, event, attrs); err != nil {
// Log error but don't fail
}
}
// PublishToUser publishes a generic event to a user
func (p *StreamingPublisher) PublishToUser(ctx context.Context, userID string, event interface{}) {
if p.client == nil {
return
}
attrs := map[string]string{
"user_id": userID,
}
if err := p.client.Publish(ctx, TopicUserEvents, event, attrs); err != nil {
// Log error but don't fail
}
}
// PublishTrackingEvent publishes an email open/click tracking event
func (p *StreamingPublisher) PublishTrackingEvent(ctx context.Context, event *TrackingEventPayload) {
if p.client == nil {
return
}
event.Timestamp = time.Now()
attrs := map[string]string{
"user_id": event.UserID,
"campaign_id": event.CampaignID,
"event_type": string(event.EventType),
}
if err := p.client.Publish(ctx, TopicCampaignUpdate, event, attrs); err != nil {
// Log error but don't fail
}
}
// PublishTaskProgress publishes a detailed task progress event
func (p *StreamingPublisher) PublishTaskProgress(ctx context.Context, event *TaskProgressEvent) {
if p.client == nil {
return
}
event.EventType = EventTaskProgress
event.Timestamp = time.Now()
attrs := map[string]string{
"user_id": event.UserID,
"campaign_id": event.CampaignID,
"task_id": event.TaskID,
"event_type": string(EventTaskProgress),
}
if err := p.client.Publish(ctx, TopicCampaignUpdate, event, attrs); err != nil {
// Log error but don't fail
}
}
// Subscription info for clients
type RealtimeSubscriptionInfo struct {
WebsocketURL string `json:"websocket_url"`
Topics []string `json:"topics"`
}
// GetSubscriptionInfo returns the subscription info for a user
func GetSubscriptionInfo(userID uuid.UUID, wsHost string) *RealtimeSubscriptionInfo {
return &RealtimeSubscriptionInfo{
WebsocketURL: wsHost + "/socket",
Topics: []string{
"user:" + userID.String(),
"campaign:*",
"account:*",
},
}
}