170 lines
7.0 KiB
Go
170 lines
7.0 KiB
Go
package channel
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/gochat/gochat/internal/model"
|
|
"github.com/gochat/gochat/internal/pubsub"
|
|
applogger "github.com/gochat/gochat/pkg/logger"
|
|
)
|
|
|
|
// Broker is the central message routing interface that coordinates incoming
|
|
// and outgoing message flow through registered ChannelProviders.
|
|
// Reference: Chatwoot Dispatcher — singleton that delegates to SyncDispatcher
|
|
// + AsyncDispatcher, which uses Wisper pub/sub to broadcast events to Listener
|
|
// instances (WebhookListener, NotificationListener, etc).
|
|
type Broker interface {
|
|
// HandleIncoming processes an inbound message from a channel provider.
|
|
// The message is routed through the IncomingMessageProcessor pipeline
|
|
// (validate → resolve contact → resolve conversation → persist → fire events).
|
|
HandleIncoming(ctx context.Context, inbox *model.Inbox, msg *IncomingMessage) error
|
|
|
|
// HandleOutgoing processes an outbound message request.
|
|
// The message is routed through the OutgoingMessageProcessor pipeline
|
|
// (validate → resolve config → send via provider → update source ID → fire events).
|
|
HandleOutgoing(ctx context.Context, inbox *model.Inbox, message *model.Message, contact *model.Contact) error
|
|
}
|
|
|
|
// MessageBroker is the concrete implementation of Broker.
|
|
// It holds references to the channel registry, the incoming/outgoing
|
|
// processors, and the event bus for cross-instance event broadcasting.
|
|
//
|
|
// Reference: Chatwoot Dispatcher pattern:
|
|
// - Incoming: per-channel IncomingMessageService parses webhook payload →
|
|
// create/update Contact → find/create Conversation → create Message → fire events
|
|
// - Outgoing: per-channel SendOn*Service (parent Base::SendOnChannelService) sends
|
|
// message via external API → update source_id → fire events
|
|
type MessageBroker struct {
|
|
registry *ChannelRegistry
|
|
incoming *IncomingMessageProcessor
|
|
outgoing *OutgoingMessageProcessor
|
|
eventBus *pubsub.EventBus
|
|
}
|
|
|
|
// NewMessageBroker creates a new MessageBroker with the given dependencies.
|
|
func NewMessageBroker(
|
|
registry *ChannelRegistry,
|
|
incoming *IncomingMessageProcessor,
|
|
outgoing *OutgoingMessageProcessor,
|
|
eventBus *pubsub.EventBus,
|
|
) *MessageBroker {
|
|
return &MessageBroker{
|
|
registry: registry,
|
|
incoming: incoming,
|
|
outgoing: outgoing,
|
|
eventBus: eventBus,
|
|
}
|
|
}
|
|
|
|
// HandleIncoming processes an inbound message from a channel provider.
|
|
// It delegates to the IncomingMessageProcessor pipeline and then publishes
|
|
// the resulting events to the EventBus for cross-instance propagation.
|
|
func (b *MessageBroker) HandleIncoming(ctx context.Context, inbox *model.Inbox, msg *IncomingMessage) error {
|
|
applogger.L().Infof("Broker: handling incoming message from %s inbox=%d source_id=%s",
|
|
inbox.ChannelType, inbox.ID, msg.SourceID)
|
|
|
|
// Build the pipeline context with inbox + incoming message
|
|
pipelineCtx := &PipelineContext{
|
|
Inbox: inbox,
|
|
IncomingMessage: msg,
|
|
}
|
|
|
|
// Run through the incoming pipeline
|
|
result, err := b.incoming.Process(ctx, pipelineCtx)
|
|
if err != nil {
|
|
applogger.L().Errorf("Broker: incoming pipeline failed: inbox=%d err=%v", inbox.ID, err)
|
|
return fmt.Errorf("incoming pipeline failed for inbox %d: %w", inbox.ID, err)
|
|
}
|
|
|
|
// Publish events to the event bus for cross-module propagation.
|
|
// Reference: Chatwoot Dispatcher fires Wisper events after message creation,
|
|
// which are consumed by NotificationListener, WebhookListener, etc.
|
|
if result.Message != nil {
|
|
topic := pubsub.TopicMessageCreated
|
|
payload, encodeErr := encodeEventPayload(EventMessageCreated, result.Message)
|
|
if encodeErr != nil {
|
|
applogger.L().Errorf("Broker: encode event payload failed: %v", encodeErr)
|
|
} else if publishErr := b.eventBus.Publish(topic, payload); publishErr != nil {
|
|
applogger.L().Errorf("Broker: publish event failed: topic=%s err=%v", topic, publishErr)
|
|
}
|
|
}
|
|
|
|
if result.Conversation != nil {
|
|
topic := pubsub.TopicConversationUpdated
|
|
payload, encodeErr := encodeEventPayload(EventConversationUpdated, result.Conversation)
|
|
if encodeErr != nil {
|
|
applogger.L().Errorf("Broker: encode conversation event payload failed: %v", encodeErr)
|
|
} else if publishErr := b.eventBus.Publish(topic, payload); publishErr != nil {
|
|
applogger.L().Errorf("Broker: publish conversation event failed: topic=%s err=%v", topic, publishErr)
|
|
}
|
|
}
|
|
|
|
if result.Contact != nil {
|
|
channelTopic := pubsub.FormatTopic(pubsub.TopicChannelEvent, inbox.ChannelType)
|
|
payload, encodeErr := encodeEventPayload(EventContactCreated, result.Contact)
|
|
if encodeErr != nil {
|
|
applogger.L().Errorf("Broker: encode contact event payload failed: %v", encodeErr)
|
|
} else if publishErr := b.eventBus.Publish(channelTopic, payload); publishErr != nil {
|
|
applogger.L().Errorf("Broker: publish contact event failed: topic=%s err=%v", channelTopic, publishErr)
|
|
}
|
|
}
|
|
|
|
applogger.L().Infof("Broker: incoming message processed successfully: message_id=%d conversation_id=%d",
|
|
result.Message.ID, result.Conversation.ID)
|
|
return nil
|
|
}
|
|
|
|
// HandleOutgoing processes an outbound message request.
|
|
// It delegates to the OutgoingMessageProcessor pipeline and then publishes
|
|
// the resulting events to the EventBus.
|
|
func (b *MessageBroker) HandleOutgoing(ctx context.Context, inbox *model.Inbox, message *model.Message, contact *model.Contact) error {
|
|
applogger.L().Infof("Broker: handling outgoing message to %s inbox=%d message_id=%d",
|
|
inbox.ChannelType, inbox.ID, message.ID)
|
|
|
|
// Build the outgoing pipeline context
|
|
outCtx := &OutgoingPipelineContext{
|
|
Inbox: inbox,
|
|
Message: message,
|
|
Contact: contact,
|
|
}
|
|
|
|
// Run through the outgoing pipeline
|
|
result, err := b.outgoing.Process(ctx, outCtx)
|
|
if err != nil {
|
|
applogger.L().Errorf("Broker: outgoing pipeline failed: inbox=%d message_id=%d err=%v",
|
|
inbox.ID, message.ID, err)
|
|
return fmt.Errorf("outgoing pipeline failed for inbox %d message %d: %w", inbox.ID, message.ID, err)
|
|
}
|
|
|
|
// Publish outgoing message event
|
|
if result.Message != nil {
|
|
topic := pubsub.TopicMessageUpdated
|
|
payload, encodeErr := encodeEventPayload(EventMessageOutgoing, result.Message)
|
|
if encodeErr != nil {
|
|
applogger.L().Errorf("Broker: encode outgoing event payload failed: %v", encodeErr)
|
|
} else if publishErr := b.eventBus.Publish(topic, payload); publishErr != nil {
|
|
applogger.L().Errorf("Broker: publish outgoing event failed: topic=%s err=%v", topic, publishErr)
|
|
}
|
|
}
|
|
|
|
applogger.L().Infof("Broker: outgoing message processed successfully: message_id=%d source_id=%s",
|
|
result.Message.ID, result.Message.SourceID)
|
|
return nil
|
|
}
|
|
|
|
// encodeEventPayload marshals an event type + data into JSON bytes for pub/sub.
|
|
func encodeEventPayload(eventType EventType, data interface{}) ([]byte, error) {
|
|
// We import encoding/json inline in this function to keep the event
|
|
// encoding simple. In production, this could be replaced with a more
|
|
// efficient serialization (protobuf, msgpack, etc.)
|
|
payload := struct {
|
|
EventType EventType `json:"event_type"`
|
|
Data interface{} `json:"data"`
|
|
}{
|
|
EventType: eventType,
|
|
Data: data,
|
|
}
|
|
// Use stdlib json encoding
|
|
return marshalJSON(payload)
|
|
} |