package adapter import ( "context" "encoding/json" "errors" "fmt" ) // MessageHandler receives a normalized inbound message. It is the push-style // counterpart to ReadMessages: adapters call it when the platform delivers a // message asynchronously, so consumers do not have to run a polling loop. type MessageHandler func(ctx context.Context, msg UnifiedMessage) // ChannelInfo describes a channel the adapter exposes, used by ListChannels. type ChannelInfo struct { ChannelID string ChannelType ChannelType ChannelName string GroupID string GroupName string } // PlatformAdapter is the contract every cross-platform IM adapter implements. // Implementations translate between a native platform SDK/protocol and the // normalized UnifiedMessage/UnifiedResponse model. The API is deliberately // narrow so it is trivial to mock and so a new platform plugs in without // touching consumers. // // All I/O operations accept a context.Context per the project constraint and // should return promptly when the context is canceled. type PlatformAdapter interface { // Name returns the platform identifier (for example "telegram"). Name() string // Config returns a snapshot of the adapter's current configuration as a // plain map, so callers can inspect or persist it without importing the // concrete type. Use DecodeConfig to read it into a typed struct. Config() map[string]any // Connect establishes the platform session. It is idempotent. Connect(ctx context.Context) error // Disconnect tears down the platform session. It is idempotent. Disconnect(ctx context.Context) error // IsConnected reports whether the session is currently live. IsConnected() bool // HealthCheck verifies the session is usable, returning an error when not. HealthCheck(ctx context.Context) error // ReadMessages runs a blocking read loop, emitting each normalized inbound // message to emit until ctx is canceled. Adapters that receive push // notifications should instead deliver through SetMessageHandler. ReadMessages(ctx context.Context, emit func(UnifiedMessage)) // SendMessage delivers a reply (potentially several messages) to the channel. SendMessage(ctx context.Context, resp UnifiedResponse) error // SendProactive pushes a message that was not triggered by a recent inbound // message. Whether that is permitted is governed by Capabilities.Proactive // and platform-specific limits (for example the weixin-ilink 24h window). SendProactive(ctx context.Context, msg UnifiedMessage) error // SetMessageHandler installs the callbacks receiving push-style inbound // messages. A nil fn clears the handler. SetMessageHandler(fn MessageHandler) // Capabilities declares what the adapter supports. Capabilities() Capabilities // ListChannels returns the channels currently known to the adapter. ListChannels(ctx context.Context) ([]ChannelInfo, error) } // Sentinel errors returned by adapters. Use errors.Is to test for them. var ( // ErrNotConnected is returned when an operation requires a live session. ErrNotConnected = errors.New("adapter: not connected") // ErrUnsupported is returned when a requested capability is not available // (for example proactive push on a channel that does not allow it). ErrUnsupported = errors.New("adapter: operation not supported") // ErrClosed is returned when an operation is attempted on a closed adapter. ErrClosed = errors.New("adapter: closed") ) // DecodeConfig decodes a raw config map produced by Config into the struct // pointed to by v. It is a convenience for adapters and tests that model their // configuration as strongly typed structs. Unknown keys are ignored; missing // keys leave the corresponding zero value. func DecodeConfig(raw map[string]any, v any) error { // A nil config map is treated as empty (decodes to the zero value), so a // pending account with no keys yet (e.g. a weixin-ilink scan-login account the // panel created before binding) builds with defaults rather than erroring. if raw == nil { raw = map[string]any{} } b, err := json.Marshal(raw) if err != nil { return fmt.Errorf("adapter: encode config: %w", err) } if err := json.Unmarshal(b, v); err != nil { return fmt.Errorf("adapter: decode config into %T: %w", v, err) } return nil }