// Package socialconnect implementiert den OAuth-Authorization-Code-Flow, // mit dem ein Kunde seinen eigenen Instagram- oder TikTok-Account mit // Deklarix verbindet — Ziel ist, dass die Beweissicherung einen // veröffentlichten Beitrag später direkt abrufen kann, statt ihn manuell // hochladen zu müssen. Reine HTTP-Logik gegen die jeweilige Plattform- // API, keine Datenbankzugriffe — Persistenz der Verbindung liegt in // internal/store (platform_connection). // // WICHTIG: Instagram- und TikTok-Endpunkte, Scopes und Token-Formate // ändern sich häufiger als andere APIs. Vor dem ersten echten // Verbindungsversuch mit realen Client-Credentials die Konstanten hier // gegen die aktuelle Meta-/TikTok-Entwicklerdokumentation prüfen — // dieser Code wurde ohne echte Zugangsdaten gebaut und gegen die // Dokumentation, nicht gegen die echte API, verifiziert. package socialconnect import ( "context" "encoding/json" "fmt" "io" "net/http" "net/url" "strings" "time" ) // Token ist das Ergebnis eines erfolgreichen Code-Tauschs. type Token struct { AccessToken string RefreshToken string ExpiresAt time.Time // Nullwert, wenn die Plattform keine Ablaufzeit liefert PlatformUserID string } // Connector kapselt den OAuth-Flow einer einzelnen Plattform. *InstagramConnector // und *TikTokConnector erfüllen dieses Interface; internal/web hält eine // Menge konfigurierter Connectors (nur die, für die echte Client- // Credentials gesetzt sind — siehe cmd/deklarix/main.go). type Connector interface { // Platform ist der interne Bezeichner ("instagram" | "tiktok"), wie // er auch in platform_connection.platform gespeichert wird. Platform() string AuthorizationURL(state string) string Exchange(ctx context.Context, code string) (Token, error) } func postForm(ctx context.Context, client *http.Client, endpoint string, form url.Values, out any) error { req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, strings.NewReader(form.Encode())) if err != nil { return err } req.Header.Set("Content-Type", "application/x-www-form-urlencoded") return doJSON(client, req, out) } func getJSON(ctx context.Context, client *http.Client, endpoint string, query url.Values, out any) error { req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint+"?"+query.Encode(), nil) if err != nil { return err } return doJSON(client, req, out) } func doJSON(client *http.Client, req *http.Request, out any) error { resp, err := client.Do(req) if err != nil { return err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body) if err != nil { return fmt.Errorf("antwort lesen: %w", err) } if resp.StatusCode >= 300 { return fmt.Errorf("unerwarteter Status %d: %s", resp.StatusCode, string(body)) } if err := json.Unmarshal(body, out); err != nil { return fmt.Errorf("antwort parsen: %w", err) } return nil }