Files
EveryPublish/server/internal/platform/platform.go
T

147 lines
3.3 KiB
Go

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
}