Start of Foursquare importer.

Change-Id: I56b0e71a3aad697b82710ebea6fc941436a3c119
This commit is contained in:
Brad Fitzpatrick 2014-01-14 20:45:23 -08:00
parent c7946165a1
commit d1f2ae227f
6 changed files with 236 additions and 4 deletions

View File

@ -288,6 +288,14 @@
}
},
"/importer-foursquare/": {
"handler": "importer-foursquare",
"enabled": ["_env", "${CAMLI_FOURSQUARE_ENABLED}", false],
"handlerArgs": {
"apiKey": ["_env", "${CAMLI_FOURSQUARE_API_KEY}", ""]
}
},
"/share/": {
"handler": "share",
"handlerArgs": {

View File

@ -55,9 +55,10 @@ type serverCmd struct {
mini bool
publish bool
openBrowser bool
flickrAPIKey string
extraArgs string // passed to camlistored
openBrowser bool
flickrAPIKey string
foursquareAPIKey string
extraArgs string // passed to camlistored
// end of flag vars
listen string // address + port to listen on
@ -92,6 +93,7 @@ func init() {
flags.BoolVar(&cmd.openBrowser, "openbrowser", false, "Open the start page on startup.")
flags.StringVar(&cmd.flickrAPIKey, "flickrapikey", "", "The key and secret to use with the Flickr importer. Formatted as '<key>:<secret>'.")
flags.StringVar(&cmd.foursquareAPIKey, "foursquareapikey", "", "The key and secret to use with the Foursquare importer. Formatted as '<clientID>:<clientSecret>'.")
flags.StringVar(&cmd.root, "root", "", "A directory to store data in. Defaults to a location in the OS temp directory.")
flags.StringVar(&cmd.extraArgs, "extraargs", "",
"List of comma separated options that will be passed to camlistored")
@ -254,6 +256,10 @@ func (c *serverCmd) setEnvVars() error {
setenv("CAMLI_FLICKR_ENABLED", "true")
setenv("CAMLI_FLICKR_API_KEY", c.flickrAPIKey)
}
if c.foursquareAPIKey != "" {
setenv("CAMLI_FOURSQUARE_ENABLED", "true")
setenv("CAMLI_FOURSQUARE_API_KEY", c.foursquareAPIKey)
}
setenv("CAMLI_CONFIG_DIR", "config")
return nil
}

View File

@ -0,0 +1,18 @@
Foursquare Importer
===================
This is an incomplete Camlistore importer for Foursquare.com.
To use:
1) Visit https://foursquare.com/developers/apps and "Create a new app"
to get a Foursquare Client ID and secret.
2) Start the devcam server with foursquareapikey flag:
$ devcam server -foursquareapikey=<apikey>:<secret>
3) Navigate to http://<server>/importer-foursquare/login
4) Watch import progress on the command line
TODO:
https://code.google.com/p/camlistore/issues/list?q=feature%3Aimportfoursquare

View File

@ -0,0 +1,190 @@
/*
Copyright 2014 The Camlistore Authors
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
// Package foursquare implements an importer for foursquare.com accounts.
package foursquare
import (
"errors"
"fmt"
"io/ioutil"
"log"
"net/http"
"strings"
"sync"
"camlistore.org/pkg/httputil"
"camlistore.org/pkg/importer"
"camlistore.org/pkg/jsonconfig"
"camlistore.org/third_party/code.google.com/p/goauth2/oauth"
)
func init() {
importer.Register("foursquare", newFromConfig)
}
type imp struct {
host *importer.Host
oauthConfig *oauth.Config
tokenCache oauth.Cache
mu sync.Mutex
user string
}
func newFromConfig(cfg jsonconfig.Obj, host *importer.Host) (importer.Importer, error) {
apiKey := cfg.RequiredString("apiKey")
if err := cfg.Validate(); err != nil {
return nil, err
}
parts := strings.Split(apiKey, ":")
if len(parts) != 2 {
return nil, fmt.Errorf("Foursquare importer: Invalid apiKey configuration: %q", apiKey)
}
clientID, clientSecret := parts[0], parts[1]
im := &imp{
host: host,
tokenCache: &tokenCache{},
oauthConfig: &oauth.Config{
ClientId: clientID,
ClientSecret: clientSecret,
AuthURL: "https://foursquare.com/oauth2/authenticate",
TokenURL: "https://foursquare.com/oauth2/access_token",
RedirectURL: host.BaseURL + "callback",
},
}
// TODO: schedule work?
return im, nil
}
type tokenCache struct {
mu sync.Mutex
token *oauth.Token
}
func (tc *tokenCache) Token() (*oauth.Token, error) {
tc.mu.Lock()
defer tc.mu.Unlock()
if tc.token == nil {
return nil, errors.New("no token")
}
return tc.token, nil
}
func (tc *tokenCache) PutToken(t *oauth.Token) error {
tc.mu.Lock()
defer tc.mu.Unlock()
tc.token = t
return nil
}
func (im *imp) CanHandleURL(url string) bool { return false }
func (im *imp) ImportURL(url string) error { panic("unused") }
func (im *imp) Prefix() string {
im.mu.Lock()
defer im.mu.Unlock()
if im.user == "" {
// This should only get called when we're importing, but check anyway.
panic("Prefix called before authenticated")
}
return fmt.Sprintf("foursquare:%s", im.user)
}
func (im *imp) String() string {
im.mu.Lock()
defer im.mu.Unlock()
userId := "<unauthenticated>"
if im.user != "" {
userId = im.user
}
return fmt.Sprintf("foursquare:%s", userId)
}
func (im *imp) Run(intr importer.Interrupt) error {
token, err := im.tokenCache.Token()
if err != nil {
return fmt.Errorf("Foursquare importer can't run. Token error: %v", err)
}
res, err := im.host.HTTPClient().Get("https://api.foursquare.com/v2/users/self?oauth_token=" + token.AccessToken)
if err != nil {
log.Printf("Error fetching //api.foursquare.com/v2/users/self: %v", err)
return err
}
all, _ := ioutil.ReadAll(res.Body)
res.Body.Close()
log.Printf("Got: %s", all)
return nil
}
func (im *imp) getRootNode() (*importer.Object, error) {
root, err := im.host.RootObject()
if err != nil {
return nil, err
}
if root.Attr("title") == "" {
im.mu.Lock()
user := im.user
im.mu.Unlock()
title := fmt.Sprintf("Foursquare (%s)", user)
if err := root.SetAttr("title", title); err != nil {
return nil, err
}
}
return root, nil
}
func (im *imp) serveLogin(w http.ResponseWriter, r *http.Request) {
state := "no_clue_what_this_is" // TODO: ask adg to document this. or send him a CL.
authURL := im.oauthConfig.AuthCodeURL(state)
http.Redirect(w, r, authURL, 302)
}
func (im *imp) serveCallback(w http.ResponseWriter, r *http.Request) {
if r.Method != "GET" {
http.Error(w, "Expected a GET", 400)
return
}
code := r.FormValue("code")
if code == "" {
http.Error(w, "Expected a code", 400)
return
}
transport := &oauth.Transport{Config: im.oauthConfig}
token, err := transport.Exchange(code)
log.Printf("Token = %#v, error %v", token, err)
if err != nil {
log.Printf("Token Exchange error: %v", err)
http.Error(w, "token exchange error", 500)
return
}
im.tokenCache.PutToken(token)
http.Redirect(w, r, im.host.BaseURL+"?mode=start", 302)
}
func (im *imp) ServeHTTP(w http.ResponseWriter, r *http.Request) {
if strings.HasSuffix(r.URL.Path, "/login") {
im.serveLogin(w, r)
} else if strings.HasSuffix(r.URL.Path, "/callback") {
im.serveCallback(w, r)
} else {
httputil.BadRequestError(w, "Unknown path: %s", r.URL.Path)
}
}

View File

@ -48,7 +48,8 @@ type Host struct {
// client optionally specifies how to fetch external network
// resources. If nil, http.DefaultClient is used.
client *http.Client
client *http.Client
transport http.RoundTripper
mu sync.Mutex
running bool
@ -132,6 +133,14 @@ func (h *Host) HTTPClient() *http.Client {
return h.client
}
// HTTPTransport returns the HTTP transport to use.
func (h *Host) HTTPTransport() http.RoundTripper {
if h.transport == nil {
return http.DefaultTransport
}
return h.transport
}
type ProgressMessage struct {
ItemsDone, ItemsTotal int
BytesDone, BytesTotal int64

View File

@ -81,6 +81,7 @@ import (
// Importers:
_ "camlistore.org/pkg/importer/dummy"
_ "camlistore.org/pkg/importer/flickr"
_ "camlistore.org/pkg/importer/foursquare"
)
var (