Files
gochat/internal/channel/broker.go
T
2026-06-04 15:44:48 +08:00

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)
}