package platform import ( "context" "errors" "fmt" "sort" "sync" "time" ) // LoginState is deliberately small: the web UI maps it to pending/scanned/ // solved without knowing platform-specific numeric QR codes. type LoginState string const ( LoginPending LoginState = "pending" LoginScanned LoginState = "scanned" LoginConfirmed LoginState = "confirmed" ) type LoginSession struct { Token string URL string } type LoginPoll struct { State LoginState Cookies string Prompt string } type PublishInput struct { WorkspaceID uint64 AccountID uint64 TaskID uint64 Title string Content string Tags []string MaterialURLs []string CategoryID int ScheduleAt *time.Time } type PublishResult struct { URL string Receipt string } type ErrorCategory string const ( AuthRequired ErrorCategory = "auth_required" ChallengeRequired ErrorCategory = "challenge_required" Validation ErrorCategory = "validation" RateLimited ErrorCategory = "rate_limited" Network ErrorCategory = "network" PlatformChanged ErrorCategory = "platform_changed" Unknown ErrorCategory = "unknown" ) // Error is safe to show in task history: it carries a category and excludes // cookies, tokens and raw response bodies by construction. type Error struct { Category ErrorCategory Message string Err error } func (e *Error) Error() string { if e == nil { return "" } if e.Message != "" { return fmt.Sprintf("%s: %s", e.Category, e.Message) } return string(e.Category) } func (e *Error) Unwrap() error { return e.Err } func CategoryOf(err error) ErrorCategory { var pe *Error if errors.As(err, &pe) && pe.Category != "" { return pe.Category } return Unknown } // Adapter is the server-side boundary for a platform. Implementations own // cookies/profile details; API handlers only persist challenge state. type Adapter interface { Platform() string BeginLogin(context.Context) (LoginSession, error) PollLogin(context.Context, string) (LoginPoll, error) SaveCredentials(workspaceID, accountID uint64, cookies string) error Publish(context.Context, PublishInput) (PublishResult, error) } // CredentialChecker is optional so a platform can expose a cheap login health // probe without forcing every adapter to implement a platform-specific API. type CredentialChecker interface { CheckCredentials(context.Context, uint64, uint64) error } type Registry struct { mu sync.RWMutex adapters map[string]Adapter } func NewRegistry() *Registry { return &Registry{adapters: make(map[string]Adapter)} } func (r *Registry) Register(adapter Adapter) error { if r == nil || adapter == nil || adapter.Platform() == "" { return errors.New("invalid platform adapter") } r.mu.Lock() defer r.mu.Unlock() r.adapters[adapter.Platform()] = adapter return nil } func (r *Registry) Get(name string) Adapter { if r == nil { return nil } r.mu.RLock() defer r.mu.RUnlock() return r.adapters[name] } func (r *Registry) Has(name string) bool { return r.Get(name) != nil } func (r *Registry) Names() []string { if r == nil { return nil } r.mu.RLock() defer r.mu.RUnlock() names := make([]string, 0, len(r.adapters)) for name := range r.adapters { names = append(names, name) } sort.Strings(names) return names }