package socialconnect import ( "context" "fmt" "net/http" "net/url" "time" ) const ( instagramDefaultAuthorizeURL = "https://api.instagram.com/oauth/authorize" instagramDefaultTokenURL = "https://api.instagram.com/oauth/access_token" instagramDefaultLongLivedTokenURL = "https://graph.instagram.com/access_token" // instagram_business_basic ist die einzige Berechtigung, die wir // brauchen (Profil + Medien lesen) — mehr zu verlangen verzögert nur // den App-Review, siehe Paket-Kommentar. instagramScope = "instagram_business_basic" ) // InstagramConnector implementiert Connector für Instagram (Instagram // API with Instagram Login). Der Flow läuft zweistufig: der // Autorisierungscode wird zuerst gegen ein 1 Stunde gültiges Token // getauscht, das anschließend gegen ein 60 Tage gültiges langlebiges // Token getauscht wird — ein einzelner API-Aufruf reicht dafür nicht. type InstagramConnector struct { ClientID string ClientSecret string RedirectURL string // Überschreibbar für Tests (Default: die echten Instagram-Endpunkte). AuthorizeURL string TokenURL string LongLivedTokenURL string HTTPClient *http.Client } // NewInstagramConnector erstellt einen InstagramConnector mit den // echten Instagram-Endpunkten. func NewInstagramConnector(clientID, clientSecret, redirectURL string) *InstagramConnector { return &InstagramConnector{ ClientID: clientID, ClientSecret: clientSecret, RedirectURL: redirectURL, AuthorizeURL: instagramDefaultAuthorizeURL, TokenURL: instagramDefaultTokenURL, LongLivedTokenURL: instagramDefaultLongLivedTokenURL, HTTPClient: http.DefaultClient, } } func (c *InstagramConnector) Platform() string { return "instagram" } func (c *InstagramConnector) AuthorizationURL(state string) string { v := url.Values{ "client_id": {c.ClientID}, "redirect_uri": {c.RedirectURL}, "scope": {instagramScope}, "response_type": {"code"}, "state": {state}, } return c.AuthorizeURL + "?" + v.Encode() } func (c *InstagramConnector) Exchange(ctx context.Context, code string) (Token, error) { form := url.Values{ "client_id": {c.ClientID}, "client_secret": {c.ClientSecret}, "grant_type": {"authorization_code"}, "redirect_uri": {c.RedirectURL}, "code": {code}, } var short struct { AccessToken string `json:"access_token"` UserID any `json:"user_id"` // liefert Instagram mal als Zahl, mal als String } if err := postForm(ctx, c.HTTPClient, c.TokenURL, form, &short); err != nil { return Token{}, fmt.Errorf("socialconnect: instagram code exchange: %w", err) } if short.AccessToken == "" { return Token{}, fmt.Errorf("socialconnect: instagram code exchange: kein access_token in der Antwort") } long := url.Values{ "grant_type": {"ig_exchange_token"}, "client_secret": {c.ClientSecret}, "access_token": {short.AccessToken}, } var longResp struct { AccessToken string `json:"access_token"` ExpiresIn int `json:"expires_in"` } if err := getJSON(ctx, c.HTTPClient, c.LongLivedTokenURL, long, &longResp); err != nil { return Token{}, fmt.Errorf("socialconnect: instagram long-lived token exchange: %w", err) } return Token{ AccessToken: longResp.AccessToken, ExpiresAt: time.Now().Add(time.Duration(longResp.ExpiresIn) * time.Second), PlatformUserID: fmt.Sprint(short.UserID), }, nil }