package bridge import ( "fmt" "sync" "github.com/yourname/cyrene-ai/platform-bridge/internal/permissions" ) // PlatformRouter manages all platform adapters and routes messages. type PlatformRouter struct { mu sync.RWMutex adapters map[string]PlatformAdapter mapper *IdentityMapper checker *permissions.Checker handler MessageHandler // Conversational context per channel. contexts map[string]*ChannelContext // channelKey -> context } // ChannelContext stores the active conversation state for a channel. type ChannelContext struct { Platform string ChannelID string ChannelType string LastUserMsg string MessageCount int } func NewPlatformRouter(mapper *IdentityMapper, checker *permissions.Checker) *PlatformRouter { return &PlatformRouter{ adapters: make(map[string]PlatformAdapter), mapper: mapper, checker: checker, contexts: make(map[string]*ChannelContext), } } // RegisterAdapter adds a platform adapter. func (r *PlatformRouter) RegisterAdapter(a PlatformAdapter) { r.mu.Lock() defer r.mu.Unlock() r.adapters[a.PlatformName()] = a } // GetAdapter returns the adapter for a platform. func (r *PlatformRouter) GetAdapter(platform string) (PlatformAdapter, error) { r.mu.RLock() defer r.mu.RUnlock() a, ok := r.adapters[platform] if !ok { return nil, fmt.Errorf("no adapter for platform: %s", platform) } return a, nil } // ListAdapters returns all registered adapter names. func (r *PlatformRouter) ListAdapters() []string { r.mu.RLock() defer r.mu.RUnlock() names := make([]string, 0, len(r.adapters)) for name := range r.adapters { names = append(names, name) } return names } // SetMessageHandler sets the callback for processing unified messages. func (r *PlatformRouter) SetMessageHandler(h MessageHandler) { r.handler = h } // RouteMessage converts a platform message to unified, checks permissions, and dispatches. func (r *PlatformRouter) RouteMessage(platform string, rawMsg interface{}) (*UnifiedResponse, error) { a, err := r.GetAdapter(platform) if err != nil { return nil, err } unified, err := a.ToUnified(rawMsg) if err != nil { return nil, fmt.Errorf("convert to unified: %w", err) } // Resolve identity. identity, err := r.mapper.Resolve(platform, unified.SenderID) if err != nil { return nil, fmt.Errorf("identity not found: %w", err) } // Merge identity info into the unified message. unified.SenderID = identity.CyreneUser unified.SenderName = identity.Nickname // Apply permission-based filtering. _ = identity // used by permission checks on tools // Update channel context. r.updateContext(unified) if r.handler == nil { return nil, fmt.Errorf("no message handler configured") } response, err := r.handler(unified) if err != nil { return nil, err } response.Platform = platform response.PlatformHints = r.platformHints(platform) return response, nil } // SendResponse converts and sends a unified response through the platform adapter. func (r *PlatformRouter) SendResponse(response *UnifiedResponse) ([]PlatformMessage, error) { a, err := r.GetAdapter(response.Platform) if err != nil { return nil, err } return a.FromUnified(response) } func (r *PlatformRouter) platformHints(platform string) PlatformHints { cap := PlatformCapabilities{} if a, err := r.GetAdapter(platform); err == nil { cap = a.Capabilities() } return PlatformHints{ TypingIndicator: cap.SupportsTypingHint, BurstMode: cap.RecommendBurstMax > 1, } } func (r *PlatformRouter) channelKey(platform, channelID string) string { return platform + ":" + channelID } func (r *PlatformRouter) updateContext(msg *UnifiedMessage) { key := r.channelKey(msg.Platform, msg.ChannelID) r.mu.Lock() defer r.mu.Unlock() ctx, ok := r.contexts[key] if !ok { ctx = &ChannelContext{ Platform: msg.Platform, ChannelID: msg.ChannelID, ChannelType: msg.ChannelType, } r.contexts[key] = ctx } ctx.LastUserMsg = msg.Content ctx.MessageCount++ } // ListAllIdentities returns all registered identity mappings. func (r *PlatformRouter) ListAllIdentities() map[string][]permissions.PlatformIdentity { return r.mapper.ListAll() } // GetContext returns the channel context. func (r *PlatformRouter) GetContext(platform, channelID string) *ChannelContext { r.mu.RLock() defer r.mu.RUnlock() return r.contexts[platform+":"+channelID] }