11package requests
22
33import (
4+ "context"
45 "encoding/json"
56 "errors"
67 "fmt"
78 "io"
89 "net/http"
910 "net/url"
1011 "os"
11- "strings"
1212
1313 "github.com/cli/browser"
1414
@@ -34,13 +34,68 @@ func NewRequestWithAuth(method, url string, body io.Reader) (*http.Request, erro
3434 if cfg .Env == "development" {
3535 token = "dev-token"
3636 } else {
37+ // Production OAuth2 Flow
3738 clientID := os .Getenv ("DISCORD_CLIENT_ID" )
3839 if clientID == "" {
3940 return nil , errors .New ("DISCORD_CLIENT_ID is unset" )
4041 }
42+ clientSecret := os .Getenv ("DISCORD_CLIENT_SECRET" )
43+ if clientSecret == "" {
44+ return nil , errors .New ("DISCORD_CLIENT_SECRET is unset" )
45+ }
4146 // TODO: check that this port isn't being used first
4247 const redirectURI = "http://localhost:8888"
4348 const scope = "identify"
49+
50+ tokenChan := make (chan string )
51+ errChan := make (chan error )
52+ mux := http .NewServeMux ()
53+ server := & http.Server {Addr : ":8888" , Handler : mux }
54+ mux .HandleFunc ("/" , func (w http.ResponseWriter , r * http.Request ) {
55+ code := r .URL .Query ().Get ("code" )
56+ if code == "" {
57+ fmt .Fprintln (w , "No code found" )
58+ return
59+ }
60+ fmt .Fprintf (w , "Got code! You can close this window." )
61+
62+ data := url.Values {}
63+ data .set ("client_id" , clientID )
64+ data .set ("client_secret" , clientSecret )
65+ data .set ("grant_type" , "authorization_code" )
66+ data .set ("code" , code )
67+ data .set ("redirect_uri" , redirectURI )
68+
69+ resp , err := http .PostForm ("https://discord.com/api/oauth2/token" , data )
70+ if err != nil {
71+ errChan <- fmt .Errorf ("failed to exchange token: %w" , err )
72+ return
73+ }
74+ defer resp .Body .Close ()
75+
76+ respBody , _ := io .ReadAll (resp .Body )
77+
78+ if resp .StatusCode != http .StatusOK {
79+ errChan <- fmt .Errorf ("Discord API error: %s" , string (respBody ))
80+ }
81+
82+ var tokenResp TokenResponse
83+ if err := json .Unmarshal (respBody , & tokenResp ); err != nil {
84+ errChan <- fmt .Errorf ("JSON parse error: %w" , err )
85+ }
86+
87+ tokenChan <- tokenResp .AccessToken
88+ })
89+
90+ // So server doesn't block
91+ go func () {
92+ if err := server .ListenAndServe (); err != nil && err != http .ErrServerClosed {
93+ errChan <- err
94+ }
95+ }()
96+
97+ // Human needs to open browser to get the token
98+ // A web client can do this more gracefully, but we have to do this since we're on a CLI
4499 params := url.Values {}
45100 params .Add ("client_id" , clientID )
46101 params .Add ("redirect_uri" , redirectURI )
@@ -49,62 +104,19 @@ func NewRequestWithAuth(method, url string, body io.Reader) (*http.Request, erro
49104
50105 authURL := "https://discord.com/oauth2/authorize" + params .Encode ()
51106 fmt .Println ("Opening browser to:" , authURL )
52- browser .OpenURL (authURL )
53107 // TODO: "Press enter to open the following link in your browser"
54- http .HandleFunc ("/" , func (w http.ResponseWriter , r * http.Request ) {
55- code := r .URL .Query ().Get ("code" )
56- if code != "" {
57- fmt .Fprintf (w , "Got code! You can close this window.\n \n Code: %s" , code )
58- fmt .Printf ("Success! Auth code: %s\n " , code )
59-
60- fmt .Println ("Exchanging code for access token..." )
61- data := url.Values {}
62- data .set ("client_id" , clientID )
63- clientSecret := os .Getenv ("DISCORD_CLIENT_SECRET" )
64- if clientSecret == "" {
65- fmt .Fprintf (os .Stderr , "DISCORD_CLIENT_SECRET is unset" )
66- return
67- }
68- data .set ("client_secret" , clientSecret )
69- data .set ("grant_type" , "authorization_code" )
70- data .set ("code" , code )
71- data .set ("redirectURI" , redirectURI )
72-
73- req , _ := http .NewRequest ("POST" , "https://discord.com/api/oauth2/token" ,
74- strings .NewReader (data .Encode ()))
75- req .Header .Set ("Content-Type" , "application/x-www-form-urlencoded" )
76-
77- client := & http.Client {}
78- resp , err := client .Do (req )
79- if err != nil {
80- fmt .Printf ("Error exchanging token: %v\n " , err )
81- return
82- }
83-
84- defer resp .Body .Close ()
85- body , _ := io .ReadAll (resp .Body )
86-
87- var tokenResp TokenResponse
88- if err := json .Unmarshal (body , & tokenResp ); err != nil {
89- fmt .Printf ("Error parsing JSON: %v\n Response Body: %s\n " , err , string (body ))
90- return
91- }
92-
93- if tokenResp .AccessToken != "" {
94- token = tokenResp .AccessToken
95- } else {
96- fmt .Fprintf (os .Stderr , "Error: Failed to get token. Discord said:\n %s\n " , string (body ))
97- }
98-
99- go func () {
100- return
101- }()
102- } else {
103- fmt .Fprintln (os .Stderr , "Error: no code in URL" )
104- }
105- })
108+ browser .OpenURL (authURL )
109+
110+ // Block until we get a token or err
111+ select {
112+ case t := <- tokenChan :
113+ token = t
114+ case e := <- errChan :
115+ server .Shutdown (context .Background ())
116+ return nil , e
117+ }
106118
107- http . ListenAndServe ( ":8888" , nil )
119+ server . Shutdown ( context . Background () )
108120 }
109121
110122 req .Header .Set ("Authorization" , fmt .Sprintf ("Bearer %s" , token ))
0 commit comments