YAMAGUCHI::weblog

土足で窓から失礼いたします。今日からあなたの息子になります。 当年とって92歳、下町の発明王、エジソンです。

名言集botをTwitter API 1.1に対応&GAE/Goに載せ替えた

はじめに

こんにちは、Go界の左卜全です。出張で韓国に来ています。アニョハセヨ。 「Twitter API 1.0が廃止になるよ」というアナウンスがあってからぼーっとしていたのですが、よく考えたら、それで動かしている @wisesawTwitter API 1.1に対応させなきゃまずいなと気づきました。さらに、なんと恐ろしいことにこのbotPython 2.5で動いていたのです。GAE/PythonももうPython 2.5のサポート打ち切りですよ。 ダブルパンチでサポート打ち切られちゃうんで、コードを書きなおそうと思い立ち、GAE/Goで動かすことにしましたよ。

はまったところ

結構詰まっちゃうかなあと思ったけど、3rd partyライブラリ使ったらさくさく書けてしまいました。はまったところは2つくらい。

  • code.google.com/p/go.net/html を使うときはnetレポジトリに入ってる他の余計なパッケージのディレクトリを消し去らないと、bad import "syscall"のエラーがでてデプロイ出来ない。(ipv4パッケージがsyscall呼んでる)
  • appengine/urlfetch経由で生成されたhttp.Clientを使わないといけないので、勝手にhttp.DefaultClient使うようなライブラリは全滅

あとは普通に、app.yamlとcron.yaml書いて、30分おきに特定のURL(下のコードでは/hoge)を叩くようにしただけ。楽ちん。

コード

とりあえず動かせばいいや、ということで書いたのがこんな感じ。Twitterの場合Access TokenとAccess Token Secretをダッシュボードで生成出来て、しかもずーっと使いまわせるので下のようなハイパー適当なコードが動かせてしまう。 他のサービスだったら、oauth.ClientのRequestTokenメソッドを使ってちゃんと取得しないとダメです。しかしいい加減TwitterもOAuth 2.0に移行してくれないかな。。。そうすればもっと楽に実装できるんだけど...

package wisesaw

import (
    "fmt"
    "io/ioutil"
    "net/http"
    "net/url"

    "code.google.com/p/go.net/html"
    "github.com/garyburd/go-oauth/oauth"

    "appengine"
    "appengine/urlfetch"
)

const (
    MeigenURI                     = "http://www.meigensyu.com/quotations/view/random"
    TemporaryCredentialRequestURI = "https://api.twitter.com/oauth/request_token"
    ResourceOwnerAuthorizationURI = "https://api.twitter.com/oauth/authenticate"
    TokenRequestURI               = "https://api.twitter.com/oauth/access_token"
    StatusesUpdateURI             = "https://api.twitter.com/1.1/statuses/update.json"
    UserAgent                     = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_8_3) AppleWebKit/537.31" +
        " (KHTML, like Gecko) Chrome/26.0.1410.43 Safari/537.31"
)

var IndexPageText = []byte(`<!doctype html>
<html>
<head><title>名言集.com bot on GAE/Go</title></head>
<body>
<h1>名言集.com bot on GAE/Go</h1>
<p><a href="https://twitter.com/wisesaw/">Follow me on Twitter</a></p>
</body>
</html>
`)

var oauthClient = oauth.Client{
    TemporaryCredentialRequestURI: TemporaryCredentialRequestURI,
    ResourceOwnerAuthorizationURI: ResourceOwnerAuthorizationURI,
    TokenRequestURI:               TokenRequestURI,
    Credentials: oauth.Credentials{
        Token:  "xxxxxxxxxxxxxxxxx",
        Secret: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    },
}

var accessCredentails = &oauth.Credentials{
    Token:  "xxxxxxxx-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
    Secret: "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
}

// TweetStatus posts status using http.Client generated from utlfetch.Client(c).
func TweetStatus(c *http.Client, cred *oauth.Credentials, status string) ([]byte, error) {
    data := url.Values{
        "status": {status},
    }
    resp, err := oauthClient.Post(
        c,
        cred,
        StatusesUpdateURI,
        data)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return nil, err
    }
    return body, nil
}

// FetchWisesaw fethes wisesaw text and its author.
func FetchWisesaw(c *http.Client) (string, string) {
    req, _ := http.NewRequest("GET", MeigenURI, nil)
    req.Header.Set("User-Agent", UserAgent)
    resp, err := c.Do(req)
    if err != nil {
        return "", ""
    }
    defer resp.Body.Close()

    node, err := html.Parse(resp.Body)
    if err != nil {
        return "", ""
    }

    var meigen func(n *html.Node, text, author *string)
    var f func(n *html.Node, text, author *string)
    f = func(n *html.Node, text, author *string) {
        if n.Type == html.ElementNode && n.Data == "div" {
            for _, a := range n.Attr {
                if a.Key == "class" && a.Val == "meigenbox" {
                    meigen(n, text, author)
                    break
                }
            }
        }
        for c := n.FirstChild; c != nil; c = c.NextSibling {
            f(c, text, author)
        }
    }

    meigen = func(n *html.Node, text, author *string) {
        if n.Type == html.ElementNode && n.Data == "div" {
            for _, a := range n.Attr {
                switch {
                case a.Key == "class" && a.Val == "text":
                    *text = n.FirstChild.Data
                case a.Key == "class" && a.Val == "link":
                    ul := n.FirstChild
                    li := ul.FirstChild
                    a := li.FirstChild
                    *author = a.FirstChild.Data
                }
            }
        }
        for c := n.FirstChild; c != nil; c = c.NextSibling {
            meigen(c, text, author)
        }

    }

    var text, author string
    f(node, &text, &author)
    return text, author
}

func init() {
    http.HandleFunc("/", rootHandler)
    http.HandleFunc("/hoge", wisesawHandler)
}

func rootHandler(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "text/html; charset=utf-8")
    w.Write(IndexPageText)
}

func wisesawHandler(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    client := urlfetch.Client(c)
    text, author := FetchWisesaw(client)
    if text != "" && author != "" {
        status := fmt.Sprintf("%v (%v)", text, author)
        body, err := TweetStatus(client, accessCredentails, status)
        if err != nil {
            http.Error(w, err.Error(), 500)
        }
        fmt.Fprintf(w, "%v", body)
    } else {
        http.Error(w, "No contents", 500)
    }
}