1// Copyright 2014 Gary Burd 2// 3// Licensed under the Apache License, Version 2.0 (the "License"): you may 4// not use this file except in compliance with the License. You may obtain 5// a copy of the License at 6// 7// http://www.apache.org/licenses/LICENSE-2.0 8// 9// Unless required by applicable law or agreed to in writing, software 10// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT 11// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the 12// License for the specific language governing permissions and limitations 13// under the License. 14 15package app 16 17import ( 18 "encoding/json" 19 "fmt" 20 "io/ioutil" 21 "net/http" 22 "text/template" 23 24 "github.com/garyburd/go-oauth/oauth" 25 26 "golang.org/x/net/context" 27 28 "google.golang.org/appengine" 29 "google.golang.org/appengine/datastore" 30 "google.golang.org/appengine/log" 31 "google.golang.org/appengine/memcache" 32 "google.golang.org/appengine/urlfetch" 33 "google.golang.org/appengine/user" 34) 35 36var oauthClient = oauth.Client{ 37 TemporaryCredentialRequestURI: "https://api.twitter.com/oauth/request_token", 38 ResourceOwnerAuthorizationURI: "https://api.twitter.com/oauth/authorize", 39 TokenRequestURI: "https://api.twitter.com/oauth/access_token", 40} 41 42// context stores context associated with an HTTP request. 43type Context struct { 44 c context.Context 45 r *http.Request 46 w http.ResponseWriter 47 u *user.User 48} 49 50// handler adapts a function to an http.Handler 51type handler func(c *Context) error 52 53func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 54 ctx := appengine.NewContext(r) 55 c := Context{ 56 c: ctx, 57 r: r, 58 w: w, 59 u: user.Current(ctx), 60 } 61 62 if c.u == nil { 63 url, _ := user.LoginURL(c.c, c.r.URL.Path) 64 http.Redirect(w, r, url, 301) 65 return 66 } 67 68 err := h(&c) 69 if err != nil { 70 http.Error(w, "server error", 500) 71 log.Errorf(c.c, "error %v", err) 72 } 73} 74 75// userInfo is stored in the App Engine datastore with key email. 76type userInfo struct { 77 TwitterCred oauth.Credentials 78} 79 80// getUserInfo returns information about the currently logged in user. 81func (c *Context) getUserInfo() (*userInfo, error) { 82 key := datastore.NewKey(c.c, "user", c.u.Email, 0, nil) 83 var u userInfo 84 err := datastore.Get(c.c, key, &u) 85 if err == datastore.ErrNoSuchEntity { 86 err = nil 87 } 88 return &u, err 89} 90 91// updateUserInfo updates information about the currently logged in user. 92func (c *Context) updateUserInfo(f func(u *userInfo)) error { 93 key := datastore.NewKey(c.c, "user", c.u.Email, 0, nil) 94 return datastore.RunInTransaction(c.c, func(ctx context.Context) error { 95 var u userInfo 96 err := datastore.Get(ctx, key, &u) 97 if err != nil && err != datastore.ErrNoSuchEntity { 98 return err 99 } 100 f(&u) 101 _, err = datastore.Put(ctx, key, &u) 102 return err 103 }, nil) 104} 105 106type connectInfo struct { 107 Secret string 108 Redirect string 109} 110 111// serveTwitterConnect gets the OAuth temp credentials and redirects the user to the 112// Twitter's authorization page. 113func serveTwitterConnect(c *Context) error { 114 httpClient := urlfetch.Client(c.c) 115 callback := "http://" + c.r.Host + "/twitter/callback" 116 tempCred, err := oauthClient.RequestTemporaryCredentials(httpClient, callback, nil) 117 if err != nil { 118 return err 119 } 120 121 ci := connectInfo{Secret: tempCred.Secret, Redirect: c.r.FormValue("redirect")} 122 err = memcache.Gob.Set(c.c, &memcache.Item{Key: tempCred.Token, Object: &ci}) 123 if err != nil { 124 return err 125 } 126 http.Redirect(c.w, c.r, oauthClient.AuthorizationURL(tempCred, nil), 302) 127 return nil 128} 129 130// serveTwitterCallback handles callbacks from the Twitter OAuth server. 131func serveTwitterCallback(c *Context) error { 132 token := c.r.FormValue("oauth_token") 133 var ci connectInfo 134 _, err := memcache.Gob.Get(c.c, token, &ci) 135 if err != nil { 136 return err 137 } 138 memcache.Delete(c.c, token) 139 tempCred := &oauth.Credentials{ 140 Token: token, 141 Secret: ci.Secret, 142 } 143 144 httpClient := urlfetch.Client(c.c) 145 tokenCred, _, err := oauthClient.RequestToken(httpClient, tempCred, c.r.FormValue("oauth_verifier")) 146 if err != nil { 147 return err 148 } 149 150 if err := c.updateUserInfo(func(u *userInfo) { u.TwitterCred = *tokenCred }); err != nil { 151 return err 152 } 153 http.Redirect(c.w, c.r, ci.Redirect, 302) 154 return nil 155} 156 157// serveTwitterDisconnect clears the user's Twitter credentials. 158func serveTwitterDisconnect(c *Context) error { 159 if err := c.updateUserInfo(func(u *userInfo) { u.TwitterCred = oauth.Credentials{} }); err != nil { 160 return err 161 } 162 http.Redirect(c.w, c.r, c.r.FormValue("redirect"), 302) 163 return nil 164} 165 166func serveHome(c *Context) error { 167 if c.r.URL.Path != "/" { 168 http.NotFound(c.w, c.r) 169 return nil 170 } 171 u, err := c.getUserInfo() 172 if err != nil { 173 return err 174 } 175 176 var data = struct { 177 Connected bool 178 Timeline []map[string]interface{} 179 }{ 180 Connected: u.TwitterCred.Token != "" && u.TwitterCred.Secret != "", 181 } 182 183 if data.Connected { 184 httpClient := urlfetch.Client(c.c) 185 resp, err := oauthClient.Get(httpClient, &u.TwitterCred, "https://api.twitter.com/1.1/statuses/home_timeline.json", nil) 186 if err != nil { 187 return err 188 } 189 defer resp.Body.Close() 190 if resp.StatusCode != 200 { 191 p, _ := ioutil.ReadAll(resp.Body) 192 return fmt.Errorf("get %s returned status %d, %s", resp.Request.URL, resp.StatusCode, p) 193 } 194 if err := json.NewDecoder(resp.Body).Decode(&data.Timeline); err != nil { 195 return err 196 } 197 } 198 199 c.w.Header().Set("Content-Type", "text/html; charset=utf-8") 200 return homeTmpl.Execute(c.w, &data) 201} 202 203func init() { 204 b, err := ioutil.ReadFile("config.json") 205 if err != nil { 206 panic(err) 207 } 208 if err := json.Unmarshal(b, &oauthClient.Credentials); err != nil { 209 panic(err) 210 } 211 http.Handle("/", handler(serveHome)) 212 http.Handle("/twitter/connect", handler(serveTwitterConnect)) 213 http.Handle("/twitter/disconnect", handler(serveTwitterDisconnect)) 214 http.Handle("/twitter/callback", handler(serveTwitterCallback)) 215} 216 217var homeTmpl = template.Must(template.New("home").Parse( 218 `<html> 219<head> 220</head> 221<body> 222{{if .Connected}} 223 <a href="/twitter/disconnect?redirect=/">Disconnect Twitter account</a> 224 {{range .Timeline}} 225 <p><b>{{html .user.name}}</b> {{html .text}} 226 {{end}} 227{{else}} 228 <a href="/twitter/connect?redirect=/">Connect Twitter account</a> 229{{end}} 230</body> 231</html>`)) 232