mirror of
https://github.com/warmbly/warmbly.git
synced 2026-08-19 16: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.
347 lines
9.8 KiB
Go
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:*",
|
|
},
|
|
}
|
|
}
|