Files
gochat/backend/internal/channel/registry.go
T
rogee aeddedf2a3 Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
Restructure the monorepo into clear top-level directories:
- backend/: Go module root (cmd, internal, pkg, configs, migrations,
  docs/swagger, scripts, tests, go.mod, Makefile, .air.toml)
- deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd
- docs/: project documentation + reports/ (moved from repo root)
- AGENTS.md: new AI coding-agent guide at repo root

Update all references to the new layout:
- Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root)
- docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile,
  env_file ../../.env, volume mounts ../../backend:/app
- deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile
- CI: working-directory: backend for go commands, file deploy/docker/Dockerfile,
  coverage path backend/coverage.out, health_check backend/scripts/
- backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../
- README: architecture tree, quickstart, config paths updated

Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh,
gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to
preserve history. Build, vet, SQLite tests, and docker compose config verified.
2026-07-07 14:44:12 +08:00

161 lines
5.1 KiB
Go

package channel
import (
"context"
"fmt"
"sync"
"github.com/gochat/gochat/internal/model"
)
// ChannelRegistry is the plugin registration center for channel providers.
// Reference: P2D §4 — replaces Chatwoot's channel_type enum with runtime registration
// New channels just implement ChannelProvider + call Register(), no core code changes needed.
type ChannelRegistry struct {
mu sync.RWMutex
providers map[ChannelType]ChannelProvider
}
// Global registry instance — all providers register here via init() or explicit call
var globalRegistry = &ChannelRegistry{
providers: make(map[ChannelType]ChannelProvider),
}
// Register adds a channel provider to the global registry.
// Returns error if a provider of the same type is already registered.
// Pattern: each channel package calls channel.Register() in its init() function.
func Register(provider ChannelProvider) error {
globalRegistry.mu.Lock()
defer globalRegistry.mu.Unlock()
ct := provider.Type()
if _, exists := globalRegistry.providers[ct]; exists {
return fmt.Errorf("channel type %s already registered", ct)
}
globalRegistry.providers[ct] = provider
return nil
}
// MustRegister is like Register but panics on duplicate registration.
// Used in init() functions where failure is a programming error.
func MustRegister(provider ChannelProvider) {
if err := Register(provider); err != nil {
panic(err)
}
}
// Get retrieves a channel provider by type from the global registry.
func Get(ct ChannelType) (ChannelProvider, error) {
globalRegistry.mu.RLock()
defer globalRegistry.mu.RUnlock()
provider, ok := globalRegistry.providers[ct]
if !ok {
return nil, fmt.Errorf("channel type %s not registered", ct)
}
return provider, nil
}
// MustGet is like Get but panics if provider not found.
func MustGet(ct ChannelType) ChannelProvider {
provider, err := Get(ct)
if err != nil {
panic(err)
}
return provider
}
// GetRegistry returns the global channel registry instance.
// Used by MessageBroker and other components that need access to the registry struct.
func GetRegistry() *ChannelRegistry {
return globalRegistry
}
// RegisterOnInstance adds a channel provider to this registry instance.
// Returns error if a provider of the same type is already registered.
// This is the instance-level counterpart of the global Register() function.
func (r *ChannelRegistry) RegisterOnInstance(provider ChannelProvider) error {
r.mu.Lock()
defer r.mu.Unlock()
ct := provider.Type()
if _, exists := r.providers[ct]; exists {
return fmt.Errorf("channel type %s already registered", ct)
}
r.providers[ct] = provider
return nil
}
// GetOnRegistry retrieves a channel provider by type from a specific registry instance.
func (r *ChannelRegistry) Get(ct ChannelType) (ChannelProvider, error) {
r.mu.RLock()
defer r.mu.RUnlock()
provider, ok := r.providers[ct]
if !ok {
return nil, fmt.Errorf("channel type %s not registered", ct)
}
return provider, nil
}
// List returns all registered channel types.
func List() []ChannelType {
globalRegistry.mu.RLock()
defer globalRegistry.mu.RUnlock()
types := make([]ChannelType, 0, len(globalRegistry.providers))
for ct := range globalRegistry.providers {
types = append(types, ct)
}
return types
}
// ListProviders returns all registered provider instances.
func ListProviders() []ChannelProvider {
globalRegistry.mu.RLock()
defer globalRegistry.mu.RUnlock()
providers := make([]ChannelProvider, 0, len(globalRegistry.providers))
for _, p := range globalRegistry.providers {
providers = append(providers, p)
}
return providers
}
// IsRegistered checks if a channel type is registered.
func IsRegistered(ct ChannelType) bool {
globalRegistry.mu.RLock()
defer globalRegistry.mu.RUnlock()
_, exists := globalRegistry.providers[ct]
return exists
}
// ProcessIncomingMessage is the unified inbound message processing pipeline.
// Reference: P2D §8 — all inbound messages flow through this pipeline
// This is the primary entry point for webhook handlers and polling workers.
func ProcessIncomingMessage(ctx context.Context, ct ChannelType, inbox *model.Inbox, rawPayload []byte) (*IncomingMessage, error) {
provider, err := Get(ct)
if err != nil {
return nil, fmt.Errorf("no provider for channel type %s: %w", ct, err)
}
return provider.ProcessIncoming(ctx, inbox, rawPayload)
}
// SendMessageToChannel is the unified outbound message sending pipeline.
// Reference: P2D §8 — all outbound messages flow through this pipeline
func SendMessageToChannel(ctx context.Context, ct ChannelType, inbox *model.Inbox, message *model.Message, contact *model.Contact) (*SendResult, error) {
provider, err := Get(ct)
if err != nil {
return nil, fmt.Errorf("no provider for channel type %s: %w", ct, err)
}
return provider.SendMessage(ctx, inbox, message, contact)
}
// ValidateWebhookRequest delegates webhook validation to the appropriate provider.
func ValidateWebhookRequest(ctx context.Context, ct ChannelType, inbox *model.Inbox, request *WebhookRequest) error {
provider, err := Get(ct)
if err != nil {
return fmt.Errorf("no provider for channel type %s: %w", ct, err)
}
return provider.ValidateWebhookRequest(ctx, inbox, request)
}