Lark adapter was already implemented in Go (lark.go — outbound Custom Bot webhook + inbound Event Subscriptions with constant-time token verify), but the Canvas connect-form hardcoded a Telegram-shaped pair of inputs (bot_token + chat_id). Selecting "Lark / Feishu" from the dropdown silently sent the wrong field names — there was no way to enter a webhook URL. Fix: move form shape to the server. - Add `ConfigField` struct + `ConfigSchema()` method to the `ChannelAdapter` interface. Each adapter declares its own fields with label/type/required/sensitive/placeholder/help. - Implement per-adapter schemas: - Lark: webhook_url (required+sensitive) + verify_token (optional+sensitive) - Slack: bot_token/channel_id/webhook_url/username/icon_emoji - Discord: webhook_url + optional public_key - Telegram: bot_token + chat_id (unchanged UX, keeps Detect Chats) - Change `ListAdapters()` to return `[]AdapterInfo` with config_schema inline. Sorted deterministically by display name so UI ordering is stable across Go's random map iteration. - Update the 3 existing `ListAdapters` test sites to struct access. Canvas (`ChannelsTab.tsx`): - Replace the two hardcoded bot_token/chat_id inputs with a single schema-driven `SchemaField` component. Renders one input per field in the order the adapter returns them. - Form state becomes `formValues: Record<string,string>` keyed by `ConfigField.key`. Values reset on platform-switch so stale Telegram credentials can't leak into a new Lark channel. - "Detect Chats" stays but only renders for platforms in `SUPPORTS_DETECT_CHATS` (Telegram only — the only provider with getUpdates). - Only schema-known keys are posted in `config`, scrubbing any stale values from previous platform selections. Regression tests: - `TestLark_ConfigSchema` locks in the 2-field Lark contract with the required/sensitive flags correctly set. - `TestListAdapters_IncludesLark` confirms registry wiring + schema survives round-trip through ListAdapters. Known pre-existing `TestStripPluginMarkers_AwkScript` failure in internal/handlers is unrelated to this change (verified via stash+test on clean staging). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
91 lines
3.9 KiB
Go
91 lines
3.9 KiB
Go
// Package channels provides a pluggable adapter system for social channel
|
|
// integrations (Telegram, Slack, Discord, etc.). Each platform implements
|
|
// the ChannelAdapter interface and registers itself in the adapter registry.
|
|
package channels
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// ChannelAdapter is the interface every social channel must implement.
|
|
type ChannelAdapter interface {
|
|
// Type returns the channel type identifier (e.g. "telegram", "slack").
|
|
Type() string
|
|
|
|
// DisplayName returns the human-readable name (e.g. "Telegram").
|
|
DisplayName() string
|
|
|
|
// ConfigSchema describes the config fields each adapter needs. The UI
|
|
// renders the connect-channel form from this list, so each platform's
|
|
// field set (Telegram bot_token+chat_id, Lark webhook_url+verify_token,
|
|
// Slack bot_token+channel_id, Discord webhook_url) can be captured
|
|
// correctly without per-platform UI branching. Adapters must return the
|
|
// same schema on every call — the order is the rendering order.
|
|
ConfigSchema() []ConfigField
|
|
|
|
// ValidateConfig checks that channel_config JSONB has required fields.
|
|
ValidateConfig(config map[string]interface{}) error
|
|
|
|
// SendMessage sends a text message to the social platform.
|
|
SendMessage(ctx context.Context, config map[string]interface{}, chatID string, text string) error
|
|
|
|
// ParseWebhook extracts message info from an incoming webhook request.
|
|
ParseWebhook(c *gin.Context, config map[string]interface{}) (*InboundMessage, error)
|
|
|
|
// StartPolling begins long-polling for platforms that support it.
|
|
// Returns nil immediately if the platform only supports webhooks.
|
|
StartPolling(ctx context.Context, config map[string]interface{}, onMessage MessageHandler) error
|
|
}
|
|
|
|
// ConfigField describes a single config field for the channels connect-form UI.
|
|
// Canvas renders one input per field in order. Values are strings in
|
|
// channel_config JSONB — this struct carries only presentation + validation
|
|
// hints; ValidateConfig on the adapter is still the source of truth for
|
|
// acceptance.
|
|
type ConfigField struct {
|
|
// Key is the channel_config map key (e.g. "webhook_url").
|
|
Key string `json:"key"`
|
|
// Label is the human-readable field name (e.g. "Webhook URL").
|
|
Label string `json:"label"`
|
|
// Type controls the HTML input type: "text" | "password" | "textarea".
|
|
Type string `json:"type"`
|
|
// Required marks the field as non-optional in the UI. Still enforced
|
|
// server-side via ValidateConfig regardless of this flag.
|
|
Required bool `json:"required"`
|
|
// Sensitive means the value must not be logged or shown unmasked in
|
|
// read APIs after creation. Canvas uses this to redact the value in
|
|
// list responses; server-side encryption is governed by sensitiveFields
|
|
// in secret.go (today: bot_token + webhook_secret only — this flag is
|
|
// forward-looking until that list is widened).
|
|
Sensitive bool `json:"sensitive"`
|
|
// Placeholder is rendered as the input's placeholder attribute.
|
|
Placeholder string `json:"placeholder,omitempty"`
|
|
// Help is a short one-liner shown below the input.
|
|
Help string `json:"help,omitempty"`
|
|
}
|
|
|
|
// InboundMessage is the standardized message from any social platform.
|
|
type InboundMessage struct {
|
|
ChatID string // Platform-specific chat/channel ID
|
|
UserID string // Platform-specific user ID
|
|
Username string // Human-readable username
|
|
Text string // Message text
|
|
MessageID string // Platform-specific message ID (for threading)
|
|
Metadata map[string]string // Extra platform-specific data
|
|
}
|
|
|
|
// MessageHandler is called by polling adapters when a message arrives.
|
|
type MessageHandler func(ctx context.Context, channelID string, msg *InboundMessage) error
|
|
|
|
// ChannelRow represents a row from the workspace_channels table.
|
|
type ChannelRow struct {
|
|
ID string
|
|
WorkspaceID string
|
|
ChannelType string
|
|
Config map[string]interface{}
|
|
Enabled bool
|
|
AllowedUsers []string
|
|
}
|