Go Google+ API

http://developer.mixi.co.jp/connect/mixi_graph_api/mixi_io_spec_top/examples/

mixi Developer Centerでmixi Graph APIを実行するいろんな言語のサンプルが公開されています。されていますっていうか、しましたっていうか。とにかくすばらしいですね。

で、これはmixi Graph APIのサンプルなんですが、ぶっちゃけOAuthで認可するRestfulなAPImixiに限らずどれもだいたい同じようなものです。

ということでmixi Graph APIサンプルのGo言語版を元にGoogle+APIを実行できるようにしてみました。

変更点はだいたい以下の四点。

  • エンドポイントの変更
  • 出力フォーマットが違うのでとりあえず生JSON表示
  • grant_type=refresh_tokenで新しいrefresh_tokenをもらえないみたいなので、一度取得したらそれをずっと使う
  • Google+のエンドポイントはHTTPSなのでnet.Dialの代わりにtls.Dialを使用

あんまり変更してないですね。それでは実行してみます。

$ 6g plus.go
$ 6l plus.6
$ ./6.out
Please open http://localhost:8008 on a web browser.


結果。

ということで無事に実行できました。ありがとうmixi。ありがとう自分。

// plus.go
package main

import (
  "crypto/tls"
  "fmt"
  "http"
  "io/ioutil"
  "json"
  "log"
  "os"
  "strings"
  "template"
)

type tokens struct {
  AccessToken string
  RefreshToken string
}

const (
  AUTHORIZE_URL_BASE = "https://accounts.google.com/o/oauth2/auth?response_type=code&scope=https%%3A%%2F%%2Fwww.googleapis.com%%2Fauth%%2Fplus.me&client_id=%v&redirect_uri=http%%3A%%2F%%2Flocalhost%%3A8008%%2Foauth2callback"
  TOKENS_ENDPOINT = "https://accounts.google.com/o/oauth2/token"
  PEOPLE_ENDPOINT= "https://www.googleapis.com/plus/v1/people/112986010883461255400"
  CONFIG_FILENAME = "config.json"
  TOKENS_FILENAME = "tokens.txt"
  TEMPLATE = ` 
<html>
  <head><title>People API</title></head>
  <body>
    <pre>{Response}</pre>
  </body>
</html>
`
)

var config map[string]string
var currentTokens *tokens

func NewTokens(accessToken string, refreshToken string) (*tokens) {
  return &tokens{accessToken, refreshToken}
}

func RestoreTokens() (*tokens) {
  if bytes, err := ioutil.ReadFile(TOKENS_FILENAME); err == nil {
    pair := strings.Split(string(bytes), "\n", -1)
    return &tokens{pair[0], pair[1]}
  }
  return nil
}

func (t *tokens)store() {
  ioutil.WriteFile(TOKENS_FILENAME, []byte(t.AccessToken +
    "\n" + t.RefreshToken), 0666)
}

func authorizeCode(authCode string) (err os.Error) {
  return updateTokens(map[string][]string{
    "grant_type":{"authorization_code"},
    "client_id":{config["client_id"]},
    "client_secret":{config["client_secret"]},
    "code":{authCode},
    "redirect_uri":{"http://localhost:" + config["redirect_port"] + "/oauth2callback"},
  })
}

func refreshToken() (err os.Error) {
  return updateTokens(map[string][]string{
    "grant_type":{"refresh_token"},
    "client_id":{config["client_id"]},
    "client_secret":{config["client_secret"]},
    "refresh_token":{currentTokens.RefreshToken},
  })
}

func updateTokens(params map[string][]string) (err os.Error) {
  println("Call: " + TOKENS_ENDPOINT)
  response, _ := http.PostForm(TOKENS_ENDPOINT, params)
  b, _ := ioutil.ReadAll(response.Body)
  println(string(b))

  var responseJson map[string]interface{}
  json.Unmarshal(b, &responseJson)
  if errorMessage, ok := responseJson["error"]; ok {
    return os.ErrorString(errorMessage.(string))
  } else {
    if responseJson["refresh_token"] == nil {
      currentTokens = &tokens{responseJson["access_token"].(string), currentTokens.RefreshToken}
    } else {
      currentTokens = &tokens{responseJson["access_token"].(string), responseJson["refresh_token"].(string)}
    }
    currentTokens.store()
    return nil
  }
  return os.ErrorString("access_token is null")
}

func oauthGet(accessToken string, urlString string) (*http.Response, os.Error) {
  url, _ := http.ParseURL(urlString)
  conn, _ := tls.Dial("tcp", url.Host + ":443", nil)

  clientConn := http.NewClientConn(conn, nil)
  header := map[string][]string {"Authorization":{"OAuth " + accessToken}}
  request := http.Request{Method:"GET", URL:url, Header:header}
  clientConn.Write(&request)
  return clientConn.Read(&request)
}

func getPeople() (result string, err os.Error) {
  println("Call: " + PEOPLE_ENDPOINT)
  response, _ := oauthGet(currentTokens.AccessToken, PEOPLE_ENDPOINT)
  if response.StatusCode == 401 {
    if err = refreshToken(); err == nil {
      return getPeople()
    }
    return "", err
  }
  b, _ := ioutil.ReadAll(response.Body)
  result = string(b)
  println(result)
  return result, nil
}

func redirect(writer http.ResponseWriter, request *http.Request, redirectUrl string) {
  println("Redirect to: " + redirectUrl)
  http.Redirect(writer, request, redirectUrl, http.StatusFound)
}

func handleFriendList(writer http.ResponseWriter, request *http.Request) {
  if request.RawURL == "/favicon.ico" { return }

  var (
    people string
    err os.Error
  )

  authorizeUrl := fmt.Sprintf(AUTHORIZE_URL_BASE, config["client_id"])
  parts := strings.Split(request.RawURL, "?code=", -1)
  if 2 <= len(parts) {
    if err = authorizeCode(parts[1]); err != nil {
      redirect(writer, request, authorizeUrl)
    } else {
      redirect(writer, request, "/")
    }
    return
  } else if currentTokens == nil {
    redirect(writer, request, authorizeUrl)
    return
  }

  if people, err = getPeople(); err != nil {
    redirect(writer, request, authorizeUrl)
  } else {
    params := new(struct{Response string})
    params.Response = people
    tmpl, _ := template.Parse(TEMPLATE, nil)
    tmpl.Execute(writer, params)
  }
}

func main() {
  var (
    bytes []byte
    err os.Error
  )

  if bytes, err = ioutil.ReadFile(CONFIG_FILENAME); err != nil {
    log.Fatal("ioutil.ReadFile:", err)
  }
  if err = json.Unmarshal(bytes, &config); err != nil {
    log.Fatal("json.Unmarshal:", err)
  }
  currentTokens = RestoreTokens()

  http.Handle("/", http.HandlerFunc(handleFriendList))

  addr := "localhost:" + config["redirect_port"]
  println("Please open http://" + addr + " on a web browser.")
  if err = http.ListenAndServe(addr, nil); err != nil {
    log.Fatal("http.ListenAndServe:", err)
  }
}
/* config.json */
{
    "client_id":"YOUR CLIENT ID",
    "client_secret":"YOUR CLIENT SECRET",
    "redirect_port":"8008"
}


なお、mixi Graph APIサンプルコード集には他にもVBAのサンプルとかあったりするので、その気になればExcelからもGoogle+ APIが実行できるはず。変態ですね。