github.com/olivere/camlistore@v0.0.0-20140121221811-1b7ac2da0199/pkg/importer/foursquare/foursquare.go (about) 1 /* 2 Copyright 2014 The Camlistore Authors 3 4 Licensed under the Apache License, Version 2.0 (the "License"); 5 you may not use this file except in compliance with the License. 6 You may obtain a copy of the License at 7 8 http://www.apache.org/licenses/LICENSE-2.0 9 10 Unless required by applicable law or agreed to in writing, software 11 distributed under the License is distributed on an "AS IS" BASIS, 12 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 See the License for the specific language governing permissions and 14 limitations under the License. 15 */ 16 17 // Package foursquare implements an importer for foursquare.com accounts. 18 package foursquare 19 20 import ( 21 "errors" 22 "fmt" 23 "io/ioutil" 24 "log" 25 "net/http" 26 "strings" 27 "sync" 28 29 "camlistore.org/pkg/httputil" 30 "camlistore.org/pkg/importer" 31 "camlistore.org/pkg/jsonconfig" 32 "camlistore.org/third_party/code.google.com/p/goauth2/oauth" 33 ) 34 35 func init() { 36 importer.Register("foursquare", newFromConfig) 37 } 38 39 type imp struct { 40 host *importer.Host 41 42 oauthConfig *oauth.Config 43 tokenCache oauth.Cache 44 45 mu sync.Mutex 46 user string 47 } 48 49 func newFromConfig(cfg jsonconfig.Obj, host *importer.Host) (importer.Importer, error) { 50 apiKey := cfg.RequiredString("apiKey") 51 if err := cfg.Validate(); err != nil { 52 return nil, err 53 } 54 parts := strings.Split(apiKey, ":") 55 if len(parts) != 2 { 56 return nil, fmt.Errorf("Foursquare importer: Invalid apiKey configuration: %q", apiKey) 57 } 58 clientID, clientSecret := parts[0], parts[1] 59 im := &imp{ 60 host: host, 61 tokenCache: &tokenCache{}, 62 oauthConfig: &oauth.Config{ 63 ClientId: clientID, 64 ClientSecret: clientSecret, 65 AuthURL: "https://foursquare.com/oauth2/authenticate", 66 TokenURL: "https://foursquare.com/oauth2/access_token", 67 RedirectURL: host.BaseURL + "callback", 68 }, 69 } 70 // TODO: schedule work? 71 return im, nil 72 } 73 74 type tokenCache struct { 75 mu sync.Mutex 76 token *oauth.Token 77 } 78 79 func (tc *tokenCache) Token() (*oauth.Token, error) { 80 tc.mu.Lock() 81 defer tc.mu.Unlock() 82 if tc.token == nil { 83 return nil, errors.New("no token") 84 } 85 return tc.token, nil 86 } 87 88 func (tc *tokenCache) PutToken(t *oauth.Token) error { 89 tc.mu.Lock() 90 defer tc.mu.Unlock() 91 tc.token = t 92 return nil 93 94 } 95 func (im *imp) CanHandleURL(url string) bool { return false } 96 func (im *imp) ImportURL(url string) error { panic("unused") } 97 98 func (im *imp) Prefix() string { 99 im.mu.Lock() 100 defer im.mu.Unlock() 101 if im.user == "" { 102 // This should only get called when we're importing, but check anyway. 103 panic("Prefix called before authenticated") 104 } 105 return fmt.Sprintf("foursquare:%s", im.user) 106 } 107 108 func (im *imp) String() string { 109 im.mu.Lock() 110 defer im.mu.Unlock() 111 userId := "<unauthenticated>" 112 if im.user != "" { 113 userId = im.user 114 } 115 return fmt.Sprintf("foursquare:%s", userId) 116 } 117 118 func (im *imp) Run(intr importer.Interrupt) error { 119 token, err := im.tokenCache.Token() 120 if err != nil { 121 return fmt.Errorf("Foursquare importer can't run. Token error: %v", err) 122 } 123 124 res, err := im.host.HTTPClient().Get("https://api.foursquare.com/v2/users/self?oauth_token=" + token.AccessToken) 125 if err != nil { 126 log.Printf("Error fetching //api.foursquare.com/v2/users/self: %v", err) 127 return err 128 } 129 all, _ := ioutil.ReadAll(res.Body) 130 res.Body.Close() 131 log.Printf("Got: %s", all) 132 return nil 133 } 134 135 func (im *imp) getRootNode() (*importer.Object, error) { 136 root, err := im.host.RootObject() 137 if err != nil { 138 return nil, err 139 } 140 141 if root.Attr("title") == "" { 142 im.mu.Lock() 143 user := im.user 144 im.mu.Unlock() 145 146 title := fmt.Sprintf("Foursquare (%s)", user) 147 if err := root.SetAttr("title", title); err != nil { 148 return nil, err 149 } 150 } 151 return root, nil 152 } 153 154 func (im *imp) serveLogin(w http.ResponseWriter, r *http.Request) { 155 state := "no_clue_what_this_is" // TODO: ask adg to document this. or send him a CL. 156 authURL := im.oauthConfig.AuthCodeURL(state) 157 http.Redirect(w, r, authURL, 302) 158 } 159 160 func (im *imp) serveCallback(w http.ResponseWriter, r *http.Request) { 161 if r.Method != "GET" { 162 http.Error(w, "Expected a GET", 400) 163 return 164 } 165 code := r.FormValue("code") 166 if code == "" { 167 http.Error(w, "Expected a code", 400) 168 return 169 } 170 transport := &oauth.Transport{Config: im.oauthConfig} 171 token, err := transport.Exchange(code) 172 log.Printf("Token = %#v, error %v", token, err) 173 if err != nil { 174 log.Printf("Token Exchange error: %v", err) 175 http.Error(w, "token exchange error", 500) 176 return 177 } 178 im.tokenCache.PutToken(token) 179 http.Redirect(w, r, im.host.BaseURL+"?mode=start", 302) 180 } 181 182 func (im *imp) ServeHTTP(w http.ResponseWriter, r *http.Request) { 183 if strings.HasSuffix(r.URL.Path, "/login") { 184 im.serveLogin(w, r) 185 } else if strings.HasSuffix(r.URL.Path, "/callback") { 186 im.serveCallback(w, r) 187 } else { 188 httputil.BadRequestError(w, "Unknown path: %s", r.URL.Path) 189 } 190 }