[WIP] music platform user data scraper
teal-fm atproto
32
fork

Configure Feed

Select the types of activity you want to include in your feed.

at 5eb3ece929aab55a16cf7c19d12a86a2c05b177c 158 lines 4.7 kB view raw
1package oauth 2 3import ( 4 "context" 5 "crypto/rand" 6 "crypto/sha256" 7 "encoding/base64" 8 "errors" 9 "fmt" 10 "log" 11 "net/http" 12 "strings" 13 14 "github.com/teal-fm/piper/session" 15 "golang.org/x/oauth2" 16 "golang.org/x/oauth2/spotify" 17) 18 19type OAuth2Service struct { 20 config oauth2.Config 21 state string 22 codeVerifier string 23 codeChallenge string 24 tokenReceiver TokenReceiver 25} 26 27func GenerateRandomState() string { 28 b := make([]byte, 16) 29 rand.Read(b) 30 return base64.URLEncoding.EncodeToString(b) 31} 32 33func NewOAuth2Service(clientID, clientSecret, redirectURI string, scopes []string, provider string, tokenReceiver TokenReceiver) *OAuth2Service { 34 var endpoint oauth2.Endpoint 35 36 switch strings.ToLower(provider) { 37 case "spotify": 38 endpoint = spotify.Endpoint 39 default: 40 // placeholder 41 log.Printf("Warning: OAuth2 provider '%s' not explicitly configured. Using placeholder endpoints.", provider) 42 endpoint = oauth2.Endpoint{ 43 AuthURL: "https://example.com/auth", 44 TokenURL: "https://example.com/token", 45 } 46 } 47 48 codeVerifier := GenerateCodeVerifier() 49 codeChallenge := GenerateCodeChallenge(codeVerifier) 50 51 return &OAuth2Service{ 52 config: oauth2.Config{ 53 ClientID: clientID, 54 ClientSecret: clientSecret, 55 RedirectURL: redirectURI, 56 Scopes: scopes, 57 Endpoint: endpoint, 58 }, 59 state: GenerateRandomState(), 60 codeVerifier: codeVerifier, 61 codeChallenge: codeChallenge, 62 tokenReceiver: tokenReceiver, 63 } 64} 65 66// generate a random code verifier, for PKCE 67func GenerateCodeVerifier() string { 68 b := make([]byte, 64) 69 rand.Read(b) 70 return base64.RawURLEncoding.EncodeToString(b) 71} 72 73// generate a code challenge for verification later 74func GenerateCodeChallenge(verifier string) string { 75 h := sha256.New() 76 h.Write([]byte(verifier)) 77 return base64.RawURLEncoding.EncodeToString(h.Sum(nil)) 78} 79 80func (o *OAuth2Service) HandleLogin(w http.ResponseWriter, r *http.Request) { 81 opts := []oauth2.AuthCodeOption{ 82 oauth2.SetAuthURLParam("code_challenge", o.codeChallenge), 83 oauth2.SetAuthURLParam("code_challenge_method", "S256"), 84 } 85 authURL := o.config.AuthCodeURL(o.state, opts...) 86 http.Redirect(w, r, authURL, http.StatusSeeOther) 87} 88 89func (o *OAuth2Service) HandleCallback(w http.ResponseWriter, r *http.Request) (int64, error) { 90 state := r.URL.Query().Get("state") 91 if state != o.state { 92 log.Printf("OAuth2 Callback Error: State mismatch. Expected '%s', got '%s'", o.state, state) 93 http.Error(w, "State mismatch", http.StatusBadRequest) 94 return 0, errors.New("state mismatch") 95 } 96 97 code := r.URL.Query().Get("code") 98 if code == "" { 99 errMsg := r.URL.Query().Get("error") 100 errDesc := r.URL.Query().Get("error_description") 101 log.Printf("OAuth2 Callback Error: No code provided. Error: '%s', Description: '%s'", errMsg, errDesc) 102 http.Error(w, fmt.Sprintf("Authorization failed: %s (%s)", errMsg, errDesc), http.StatusBadRequest) 103 return 0, errors.New("no code provided") 104 } 105 106 if o.tokenReceiver == nil { 107 log.Printf("OAuth2 Callback Error: TokenReceiver is not configured for this service.") 108 http.Error(w, "Internal server configuration error", http.StatusInternalServerError) 109 return 0, errors.New("token receiver not configured") 110 } 111 112 opts := []oauth2.AuthCodeOption{ 113 oauth2.SetAuthURLParam("code_verifier", o.codeVerifier), 114 } 115 116 log.Println(code) 117 118 token, err := o.config.Exchange(context.Background(), code, opts...) 119 if err != nil { 120 log.Printf("OAuth2 Callback Error: Failed to exchange code for token: %v", err) 121 http.Error(w, fmt.Sprintf("Error exchanging code for token: %v", err), http.StatusInternalServerError) 122 return 0, errors.New("failed to exchange code for token") 123 } 124 125 userId, hasSession := session.GetUserID(r.Context()) 126 127 // store token and get uid 128 userID, err := o.tokenReceiver.SetAccessToken(token.AccessToken, userId, hasSession) 129 if err != nil { 130 log.Printf("OAuth2 Callback Info: TokenReceiver did not return a valid user ID for token: %s...", token.AccessToken[:min(10, len(token.AccessToken))]) 131 } 132 133 log.Printf("OAuth2 Callback Success: Exchanged code for token, UserID: %d", userID) 134 return userID, nil 135} 136 137func (o *OAuth2Service) GetToken(code string) (*oauth2.Token, error) { 138 opts := []oauth2.AuthCodeOption{ 139 oauth2.SetAuthURLParam("code_verifier", o.codeVerifier), 140 } 141 return o.config.Exchange(context.Background(), code, opts...) 142} 143 144func (o *OAuth2Service) GetClient(token *oauth2.Token) *http.Client { 145 return o.config.Client(context.Background(), token) 146} 147 148func (o *OAuth2Service) RefreshToken(token *oauth2.Token) (*oauth2.Token, error) { 149 source := o.config.TokenSource(context.Background(), token) 150 return oauth2.ReuseTokenSource(token, source).Token() 151} 152 153func min(a, b int) int { 154 if a < b { 155 return a 156 } 157 return b 158}