github.com/slspeek/camlistore_namedsearch@v0.0.0-20140519202248-ed6f70f7721a/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 "fmt" 22 "log" 23 "net/http" 24 "net/url" 25 "path" 26 "path/filepath" 27 "sort" 28 "strconv" 29 "strings" 30 "sync" 31 "time" 32 33 "camlistore.org/pkg/blob" 34 "camlistore.org/pkg/context" 35 "camlistore.org/pkg/httputil" 36 "camlistore.org/pkg/importer" 37 "camlistore.org/pkg/schema" 38 "camlistore.org/third_party/code.google.com/p/goauth2/oauth" 39 ) 40 41 const ( 42 apiURL = "https://api.foursquare.com/v2/" 43 authURL = "https://foursquare.com/oauth2/authenticate" 44 tokenURL = "https://foursquare.com/oauth2/access_token" 45 46 // runCompleteVersion is a cache-busting version number of the 47 // importer code. It should be incremented whenever the 48 // behavior of this importer is updated enough to warrant a 49 // complete run. Otherwise, if the importer runs to 50 // completion, this version number is recorded on the account 51 // permanode and subsequent importers can stop early. 52 runCompleteVersion = "1" 53 54 // Permanode attributes on account node: 55 acctAttrUserId = "foursquareUserId" 56 acctAttrUserFirst = "foursquareFirstName" 57 acctAttrUserLast = "foursquareLastName" 58 acctAttrAccessToken = "oauthAccessToken" 59 acctAttrCompletedVersion = "completedVersion" 60 ) 61 62 func init() { 63 importer.Register("foursquare", &imp{ 64 imageFileRef: make(map[string]blob.Ref), 65 }) 66 } 67 68 var _ importer.ImporterSetupHTMLer = (*imp)(nil) 69 70 type imp struct { 71 mu sync.Mutex // guards following 72 imageFileRef map[string]blob.Ref // url to file schema blob 73 74 importer.OAuth2 // for CallbackRequestAccount and CallbackURLParameters 75 } 76 77 func (im *imp) NeedsAPIKey() bool { return true } 78 79 func (im *imp) IsAccountReady(acctNode *importer.Object) (ok bool, err error) { 80 if acctNode.Attr(acctAttrUserId) != "" && acctNode.Attr(acctAttrAccessToken) != "" { 81 return true, nil 82 } 83 return false, nil 84 } 85 86 func (im *imp) SummarizeAccount(acct *importer.Object) string { 87 ok, err := im.IsAccountReady(acct) 88 if err != nil { 89 return "Not configured; error = " + err.Error() 90 } 91 if !ok { 92 return "Not configured" 93 } 94 if acct.Attr(acctAttrUserFirst) == "" && acct.Attr(acctAttrUserLast) == "" { 95 return fmt.Sprintf("userid %s", acct.Attr(acctAttrUserId)) 96 } 97 return fmt.Sprintf("userid %s (%s %s)", acct.Attr(acctAttrUserId), 98 acct.Attr(acctAttrUserFirst), acct.Attr(acctAttrUserLast)) 99 } 100 101 func (im *imp) AccountSetupHTML(host *importer.Host) string { 102 base := host.ImporterBaseURL() + "foursquare" 103 return fmt.Sprintf(` 104 <h1>Configuring Foursquare</h1> 105 <p>Visit <a href='https://foursquare.com/developers/apps'>https://foursquare.com/developers/apps</a> and click "Create a new app".</p> 106 <p>Use the following settings:</p> 107 <ul> 108 <li>Download / welcome page url: <b>%s</b></li> 109 <li>Your privacy policy url: <b>%s</b></li> 110 <li>Redirect URI(s): <b>%s</b></li> 111 </ul> 112 <p>Click "SAVE CHANGES". Copy the "Client ID" and "Client Secret" into the boxes above.</p> 113 `, base, base+"/privacy", base+"/callback") 114 } 115 116 // A run is our state for a given run of the importer. 117 type run struct { 118 *importer.RunContext 119 im *imp 120 incremental bool // whether we've completed a run in the past 121 122 mu sync.Mutex // guards anyErr 123 anyErr bool 124 } 125 126 func (r *run) token() string { 127 return r.RunContext.AccountNode().Attr(acctAttrAccessToken) 128 } 129 130 func (im *imp) Run(ctx *importer.RunContext) error { 131 r := &run{ 132 RunContext: ctx, 133 im: im, 134 incremental: ctx.AccountNode().Attr(acctAttrCompletedVersion) == runCompleteVersion, 135 } 136 137 if err := r.importCheckins(); err != nil { 138 return err 139 } 140 141 r.mu.Lock() 142 anyErr := r.anyErr 143 r.mu.Unlock() 144 145 if !anyErr { 146 if err := r.AccountNode().SetAttrs(acctAttrCompletedVersion, runCompleteVersion); err != nil { 147 return err 148 } 149 } 150 151 return nil 152 } 153 154 func (r *run) errorf(format string, args ...interface{}) { 155 log.Printf(format, args...) 156 r.mu.Lock() 157 defer r.mu.Unlock() 158 r.anyErr = true 159 } 160 161 // urlFileRef slurps urlstr from the net, writes to a file and returns its 162 // fileref or "" on error 163 func (r *run) urlFileRef(urlstr, filename string) string { 164 im := r.im 165 im.mu.Lock() 166 if br, ok := im.imageFileRef[urlstr]; ok { 167 im.mu.Unlock() 168 return br.String() 169 } 170 im.mu.Unlock() 171 172 res, err := r.Host.HTTPClient().Get(urlstr) 173 if err != nil { 174 log.Printf("couldn't get image: %v", err) 175 return "" 176 } 177 defer res.Body.Close() 178 179 fileRef, err := schema.WriteFileFromReader(r.Host.Target(), filename, res.Body) 180 if err != nil { 181 r.errorf("couldn't write file: %v", err) 182 return "" 183 } 184 185 im.mu.Lock() 186 defer im.mu.Unlock() 187 im.imageFileRef[urlstr] = fileRef 188 return fileRef.String() 189 } 190 191 type byCreatedAt []*checkinItem 192 193 func (s byCreatedAt) Less(i, j int) bool { return s[i].CreatedAt < s[j].CreatedAt } 194 func (s byCreatedAt) Len() int { return len(s) } 195 func (s byCreatedAt) Swap(i, j int) { s[i], s[j] = s[j], s[i] } 196 197 func (r *run) importCheckins() error { 198 limit := 100 199 offset := 0 200 continueRequests := true 201 202 for continueRequests { 203 resp := checkinsList{} 204 if err := r.im.doAPI(r.Context, r.token(), &resp, "users/self/checkins", "limit", strconv.Itoa(limit), "offset", strconv.Itoa(offset)); err != nil { 205 return err 206 } 207 208 itemcount := len(resp.Response.Checkins.Items) 209 log.Printf("foursquare: importing %d checkins (offset %d)", itemcount, offset) 210 if itemcount < limit { 211 continueRequests = false 212 } else { 213 offset += itemcount 214 } 215 216 checkinsNode, err := r.getTopLevelNode("checkins", "Checkins") 217 if err != nil { 218 return err 219 } 220 221 placesNode, err := r.getTopLevelNode("places", "Places") 222 if err != nil { 223 return err 224 } 225 226 sort.Sort(byCreatedAt(resp.Response.Checkins.Items)) 227 sawOldItem := false 228 for _, checkin := range resp.Response.Checkins.Items { 229 placeNode, err := r.importPlace(placesNode, &checkin.Venue) 230 if err != nil { 231 r.errorf("Foursquare importer: error importing place %s %v", checkin.Venue.Id, err) 232 continue 233 } 234 235 _, dup, err := r.importCheckin(checkinsNode, checkin, placeNode.PermanodeRef()) 236 if err != nil { 237 r.errorf("Foursquare importer: error importing checkin %s %v", checkin.Id, err) 238 continue 239 } 240 241 if dup { 242 sawOldItem = true 243 } 244 245 err = r.importPhotos(placeNode, dup) 246 if err != nil { 247 r.errorf("Foursquare importer: error importing photos for checkin %s %v", checkin.Id, err) 248 continue 249 } 250 } 251 if sawOldItem && r.incremental { 252 break 253 } 254 } 255 256 return nil 257 } 258 259 func (r *run) importPhotos(placeNode *importer.Object, checkinWasDup bool) error { 260 photosNode, err := placeNode.ChildPathObject("photos") 261 if err != nil { 262 return err 263 } 264 265 if err := photosNode.SetAttrs( 266 "title", "Photos of "+placeNode.Attr("title"), 267 "camliDefVis", "hide"); err != nil { 268 return err 269 } 270 271 nHave := 0 272 photosNode.ForeachAttr(func(key, value string) { 273 if strings.HasPrefix(key, "camliPath:") { 274 nHave++ 275 } 276 }) 277 nWant := 5 278 if checkinWasDup { 279 nWant = 1 280 } 281 if nHave >= nWant { 282 return nil 283 } 284 285 resp := photosList{} 286 if err := r.im.doAPI(r.Context, r.token(), &resp, 287 "venues/"+placeNode.Attr("foursquareId")+"/photos", 288 "limit", strconv.Itoa(nWant)); err != nil { 289 return err 290 } 291 292 var need []*photoItem 293 for _, photo := range resp.Response.Photos.Items { 294 attr := "camliPath:" + photo.Id + filepath.Ext(photo.Suffix) 295 if photosNode.Attr(attr) == "" { 296 need = append(need, photo) 297 } 298 } 299 300 if len(need) > 0 { 301 log.Printf("foursquare: importing %d photos for venue %s", len(need), placeNode.Attr("title")) 302 for _, photo := range need { 303 attr := "camliPath:" + photo.Id + filepath.Ext(photo.Suffix) 304 if photosNode.Attr(attr) != "" { 305 continue 306 } 307 url := photo.Prefix + "original" + photo.Suffix 308 log.Printf("foursquare: importing photo for venue %s: %s", placeNode.Attr("title"), url) 309 ref := r.urlFileRef(url, "") 310 if ref == "" { 311 r.errorf("Error slurping photo: %s", url) 312 continue 313 } 314 if err := photosNode.SetAttr(attr, ref); err != nil { 315 r.errorf("Error adding venue photo: %#v", err) 316 } 317 } 318 } 319 320 return nil 321 } 322 323 func (r *run) importCheckin(parent *importer.Object, checkin *checkinItem, placeRef blob.Ref) (checkinNode *importer.Object, dup bool, err error) { 324 checkinNode, err = parent.ChildPathObject(checkin.Id) 325 if err != nil { 326 return 327 } 328 329 title := fmt.Sprintf("Checkin at %s", checkin.Venue.Name) 330 dup = checkinNode.Attr("startDate") != "" 331 if err := checkinNode.SetAttrs( 332 "foursquareId", checkin.Id, 333 "foursquareVenuePermanode", placeRef.String(), 334 "camliNodeType", "foursquare.com:checkin", 335 "startDate", schema.RFC3339FromTime(time.Unix(checkin.CreatedAt, 0)), 336 "title", title); err != nil { 337 return nil, false, err 338 } 339 return checkinNode, dup, nil 340 } 341 342 func (r *run) importPlace(parent *importer.Object, place *venueItem) (*importer.Object, error) { 343 placeNode, err := parent.ChildPathObject(place.Id) 344 if err != nil { 345 return nil, err 346 } 347 348 catName := "" 349 if cat := place.primaryCategory(); cat != nil { 350 catName = cat.Name 351 } 352 353 icon := place.icon() 354 if err := placeNode.SetAttrs( 355 "foursquareId", place.Id, 356 "camliNodeType", "foursquare.com:venue", 357 "camliContentImage", r.urlFileRef(icon, path.Base(icon)), 358 "foursquareCategoryName", catName, 359 "title", place.Name, 360 "streetAddress", place.Location.Address, 361 "addressLocality", place.Location.City, 362 "postalCode", place.Location.PostalCode, 363 "addressRegion", place.Location.State, 364 "addressCountry", place.Location.Country, 365 "latitude", fmt.Sprint(place.Location.Lat), 366 "longitude", fmt.Sprint(place.Location.Lng)); err != nil { 367 return nil, err 368 } 369 370 return placeNode, nil 371 } 372 373 func (r *run) getTopLevelNode(path string, title string) (*importer.Object, error) { 374 childObject, err := r.RootNode().ChildPathObject(path) 375 if err != nil { 376 return nil, err 377 } 378 379 if err := childObject.SetAttr("title", title); err != nil { 380 return nil, err 381 } 382 return childObject, nil 383 } 384 385 func (im *imp) getUserInfo(ctx *context.Context, accessToken string) (user, error) { 386 var ui userInfo 387 if err := im.doAPI(ctx, accessToken, &ui, "users/self"); err != nil { 388 return user{}, err 389 } 390 if ui.Response.User.Id == "" { 391 return user{}, fmt.Errorf("No userid returned") 392 } 393 return ui.Response.User, nil 394 } 395 396 func (im *imp) doAPI(ctx *context.Context, accessToken string, result interface{}, apiPath string, keyval ...string) error { 397 if len(keyval)%2 == 1 { 398 panic("Incorrect number of keyval arguments") 399 } 400 401 form := url.Values{} 402 form.Set("v", "20140225") // 4sq requires this to version their API 403 form.Set("oauth_token", accessToken) 404 for i := 0; i < len(keyval); i += 2 { 405 form.Set(keyval[i], keyval[i+1]) 406 } 407 408 fullURL := apiURL + apiPath 409 res, err := doGet(ctx, fullURL, form) 410 if err != nil { 411 return err 412 } 413 err = httputil.DecodeJSON(res, result) 414 if err != nil { 415 log.Printf("Error parsing response for %s: %v", fullURL, err) 416 } 417 return err 418 } 419 420 func doGet(ctx *context.Context, url string, form url.Values) (*http.Response, error) { 421 requestURL := url + "?" + form.Encode() 422 req, err := http.NewRequest("GET", requestURL, nil) 423 if err != nil { 424 return nil, err 425 } 426 res, err := ctx.HTTPClient().Do(req) 427 if err != nil { 428 log.Printf("Error fetching %s: %v", url, err) 429 return nil, err 430 } 431 if res.StatusCode != http.StatusOK { 432 return nil, fmt.Errorf("Get request on %s failed with: %s", requestURL, res.Status) 433 } 434 return res, nil 435 } 436 437 // auth returns a new oauth.Config 438 func auth(ctx *importer.SetupContext) (*oauth.Config, error) { 439 clientId, secret, err := ctx.Credentials() 440 if err != nil { 441 return nil, err 442 } 443 return &oauth.Config{ 444 ClientId: clientId, 445 ClientSecret: secret, 446 AuthURL: authURL, 447 TokenURL: tokenURL, 448 RedirectURL: ctx.CallbackURL(), 449 }, nil 450 } 451 452 func (im *imp) ServeSetup(w http.ResponseWriter, r *http.Request, ctx *importer.SetupContext) error { 453 oauthConfig, err := auth(ctx) 454 if err != nil { 455 return err 456 } 457 oauthConfig.RedirectURL = im.RedirectURL(im, ctx) 458 state, err := im.RedirectState(im, ctx) 459 if err != nil { 460 return err 461 } 462 http.Redirect(w, r, oauthConfig.AuthCodeURL(state), http.StatusFound) 463 return nil 464 } 465 466 func (im *imp) ServeCallback(w http.ResponseWriter, r *http.Request, ctx *importer.SetupContext) { 467 oauthConfig, err := auth(ctx) 468 if err != nil { 469 httputil.ServeError(w, r, fmt.Errorf("Error getting oauth config: %v", err)) 470 return 471 } 472 473 if r.Method != "GET" { 474 http.Error(w, "Expected a GET", 400) 475 return 476 } 477 code := r.FormValue("code") 478 if code == "" { 479 http.Error(w, "Expected a code", 400) 480 return 481 } 482 transport := &oauth.Transport{Config: oauthConfig} 483 token, err := transport.Exchange(code) 484 log.Printf("Token = %#v, error %v", token, err) 485 if err != nil { 486 log.Printf("Token Exchange error: %v", err) 487 http.Error(w, "token exchange error", 500) 488 return 489 } 490 491 u, err := im.getUserInfo(ctx.Context, token.AccessToken) 492 if err != nil { 493 log.Printf("Couldn't get username: %v", err) 494 http.Error(w, "can't get username", 500) 495 return 496 } 497 if err := ctx.AccountNode.SetAttrs( 498 acctAttrUserId, u.Id, 499 acctAttrUserFirst, u.FirstName, 500 acctAttrUserLast, u.LastName, 501 acctAttrAccessToken, token.AccessToken, 502 ); err != nil { 503 httputil.ServeError(w, r, fmt.Errorf("Error setting attribute: %v", err)) 504 return 505 } 506 http.Redirect(w, r, ctx.AccountURL(), http.StatusFound) 507 508 }