diff --git a/api/Gopkg.lock b/api/Gopkg.lock index 4cc5215..8a0f34c 100644 --- a/api/Gopkg.lock +++ b/api/Gopkg.lock @@ -25,6 +25,14 @@ revision = "b4deda0973fb4c70b50d226b1af49f3da59f5265" version = "v1.1.0" +[[projects]] + branch = "master" + digest = "1:d03d0fae6a7a80e89c540787a69ab6e0d3b773fdb3303c0b3d96a15490c6ef32" + name = "github.com/gomodule/oauth1" + packages = ["oauth"] + pruneopts = "UT" + revision = "9a59ed3b0a84f454c260f2f8f82918223fc5630f" + [[projects]] digest = "1:c79fb010be38a59d657c48c6ba1d003a8aa651fa56b579d959d74573b7dff8e1" name = "github.com/gorilla/context" @@ -153,6 +161,7 @@ analyzer-version = 1 input-imports = [ "github.com/adtac/go-akismet/akismet", + "github.com/gomodule/oauth1/oauth", "github.com/gorilla/handlers", "github.com/gorilla/mux", "github.com/lib/pq", diff --git a/api/config.go b/api/config.go index 6886338..78d2c04 100644 --- a/api/config.go +++ b/api/config.go @@ -50,6 +50,9 @@ func configParse() error { "GITHUB_KEY": "", "GITHUB_SECRET": "", + + "TWITTER_KEY": "", + "TWITTER_SECRET": "", } for key, value := range defaults { diff --git a/api/oauth.go b/api/oauth.go index 845caec..5550cee 100644 --- a/api/oauth.go +++ b/api/oauth.go @@ -15,5 +15,9 @@ func oauthConfigure() error { return err } + if err := twitterOauthConfigure(); err != nil { + return err + } + return nil } diff --git a/api/oauth_twitter.go b/api/oauth_twitter.go new file mode 100644 index 0000000..753df2a --- /dev/null +++ b/api/oauth_twitter.go @@ -0,0 +1,51 @@ +package main + +import ( + "github.com/gomodule/oauth1/oauth" + "os" + "sync" +) + +type twitterOauthState struct { + CommenterToken string + Cred *oauth.Credentials +} + +var twitterClient *oauth.Client +var twitterCredMapLock sync.RWMutex +var twitterCredMap map[string]twitterOauthState + +func twitterOauthConfigure() error { + twitterClient = nil + if os.Getenv("TWITTER_KEY") == "" && os.Getenv("TWITTER_SECRET") == "" { + return nil + } + + if os.Getenv("TWITTER_KEY") == "" { + logger.Errorf("COMMENTO_TWITTER_KEY not configured, but COMMENTO_TWITTER_SECRET is set") + return errorOauthMisconfigured + } + + if os.Getenv("TWITTER_SECRET") == "" { + logger.Errorf("COMMENTO_TWITTER_SECRET not configured, but COMMENTO_TWITTER_KEY is set") + return errorOauthMisconfigured + } + + logger.Infof("loading twitter OAuth config") + + twitterClient = &oauth.Client{ + TemporaryCredentialRequestURI: "https://api.twitter.com/oauth/request_token", + ResourceOwnerAuthorizationURI: "https://api.twitter.com/oauth/authenticate", + TokenRequestURI: "https://api.twitter.com/oauth/access_token", + Credentials: oauth.Credentials{ + Token: os.Getenv("TWITTER_KEY"), + Secret: os.Getenv("TWITTER_SECRET"), + }, + } + + configuredOauths = append(configuredOauths, "twitter") + + twitterCredMap = make(map[string]twitterOauthState, 1e3) + + return nil +} diff --git a/api/oauth_twitter_callback.go b/api/oauth_twitter_callback.go new file mode 100644 index 0000000..f97d864 --- /dev/null +++ b/api/oauth_twitter_callback.go @@ -0,0 +1,92 @@ +package main + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "net/url" +) + +func twitterCallbackHandler(w http.ResponseWriter, r *http.Request) { + token := r.FormValue("oauth_token") + verifier := r.FormValue("oauth_verifier") + + twitterCredMapLock.RLock() + s, ok := twitterCredMap[token] + twitterCredMapLock.RUnlock() + + commenterToken := s.CommenterToken + + if !ok { + fmt.Fprintf(w, "no such token/verifier combination found") + return + } + + _, err := commenterGetByCommenterToken(commenterToken) + if err != nil && err != errorNoSuchToken { + fmt.Fprintf(w, "Error: %s\n", err.Error()) + return + } + + x, _, err := twitterClient.RequestToken(nil, s.Cred, verifier) + if err != nil { + fmt.Fprintf(w, "Error: %s\n", err.Error()) + return + } + + twitterCredMapLock.Lock() + delete(twitterCredMap, token) + twitterCredMapLock.Unlock() + + resp, err := twitterClient.Get(nil, x, "https://api.twitter.com/1.1/account/verify_credentials.json", url.Values{"include_email": {"true"}}) + if err != nil { + fmt.Fprintf(w, "Error getting email: %s\n", err.Error()) + return + } + defer resp.Body.Close() + + if resp.StatusCode != 200 { + msg, _ := ioutil.ReadAll(resp.Body) + fmt.Fprintf(w, "Error: status %d: %s\n", resp.StatusCode, msg) + return + } + + var res map[string]interface{} + if err = json.NewDecoder(resp.Body).Decode(&res); err != nil { + fmt.Fprintf(w, "Error: %s\n", err.Error()) + return + } + + name := res["name"].(string) + handle := res["screen_name"].(string) + link := "https://twitter.com/" + handle + photo := "https://twitter.com/" + handle + "/profile_image" + email := res["email"].(string) + + c, err := commenterGetByEmail("twitter", email) + if err != nil && err != errorNoSuchCommenter { + fmt.Fprintf(w, "Error: %s", err.Error()) + return + } + + var commenterHex string + + // TODO: in case of returning users, update the information we have on record? + if err == errorNoSuchCommenter { + commenterHex, err = commenterNew(email, name, link, photo, "twitter", "") + if err != nil { + fmt.Fprintf(w, "Error: %s", err.Error()) + return + } + } else { + commenterHex = c.CommenterHex + } + + if err := commenterSessionUpdate(commenterToken, commenterHex); err != nil { + fmt.Fprintf(w, "Error: %s", err.Error()) + return + } + + fmt.Fprintf(w, "") +} diff --git a/api/oauth_twitter_redirect.go b/api/oauth_twitter_redirect.go new file mode 100644 index 0000000..6693f6c --- /dev/null +++ b/api/oauth_twitter_redirect.go @@ -0,0 +1,39 @@ +package main + +import ( + "fmt" + "net/http" + "os" +) + +func twitterRedirectHandler(w http.ResponseWriter, r *http.Request) { + if twitterClient == nil { + logger.Errorf("twitter oauth access attempt without configuration") + fmt.Fprintf(w, "error: this website has not configured twitter OAuth") + return + } + + commenterToken := r.FormValue("commenterToken") + + _, err := commenterGetByCommenterToken(commenterToken) + if err != nil && err != errorNoSuchToken { + fmt.Fprintf(w, "error: %s\n", err.Error()) + return + } + + cred, err := twitterClient.RequestTemporaryCredentials(nil, os.Getenv("ORIGIN")+"/api/oauth/twitter/callback", nil) + if err != nil { + logger.Errorf("cannot get temporary twitter credentials: %v", err) + fmt.Fprintf(w, "error: %v", errorInternal.Error()) + return + } + + twitterCredMapLock.Lock() + twitterCredMap[cred.Token] = twitterOauthState{ + CommenterToken: commenterToken, + Cred: cred, + } + twitterCredMapLock.Unlock() + + http.Redirect(w, r, twitterClient.AuthorizationURL(cred, nil), http.StatusFound) +} diff --git a/api/router_api.go b/api/router_api.go index 3f62f1d..96fa6bb 100644 --- a/api/router_api.go +++ b/api/router_api.go @@ -38,6 +38,9 @@ func apiRouterInit(router *mux.Router) error { router.HandleFunc("/api/oauth/github/redirect", githubRedirectHandler).Methods("GET") router.HandleFunc("/api/oauth/github/callback", githubCallbackHandler).Methods("GET") + router.HandleFunc("/api/oauth/twitter/redirect", twitterRedirectHandler).Methods("GET") + router.HandleFunc("/api/oauth/twitter/callback", twitterCallbackHandler).Methods("GET") + router.HandleFunc("/api/comment/new", commentNewHandler).Methods("POST") router.HandleFunc("/api/comment/list", commentListHandler).Methods("POST") router.HandleFunc("/api/comment/count", commentCountHandler).Methods("POST") diff --git a/frontend/js/commento.js b/frontend/js/commento.js index b9529e9..53fb956 100644 --- a/frontend/js/commento.js +++ b/frontend/js/commento.js @@ -262,6 +262,8 @@ attrSet(avatar, "src", commenter.photo + "?sz=50"); } else if (commenter.provider === "github") { attrSet(avatar, "src", commenter.photo + "&s=50"); + } else if (commenter.provider === "twitter") { + attrSet(avatar, "src", commenter.photo + "?size=normal"); } else { attrSet(avatar, "src", commenter.photo); } @@ -745,6 +747,8 @@ attrSet(avatar, "src", commenter.photo + "?sz=50"); } else if (commenter.provider === "github") { attrSet(avatar, "src", commenter.photo + "&s=50"); + } else if (commenter.provider === "twitter") { + attrSet(avatar, "src", commenter.photo + "?size=normal"); } else { attrSet(avatar, "src", commenter.photo); } diff --git a/frontend/sass/commento-oauth.scss b/frontend/sass/commento-oauth.scss index e6c0d05..7895026 100644 --- a/frontend/sass/commento-oauth.scss +++ b/frontend/sass/commento-oauth.scss @@ -25,5 +25,12 @@ font-size: 13px; width: 70px; } + + .commento-twitter-button { + background: #00aced; + text-transform: uppercase; + font-size: 13px; + width: 70px; + } } }