はじめに
こんにちは、Go界の左卜全です。出張で韓国に来ています。アニョハセヨ。 「Twitter API 1.0が廃止になるよ」というアナウンスがあってからぼーっとしていたのですが、よく考えたら、それで動かしている @wisesaw もTwitter API 1.1に対応させなきゃまずいなと気づきました。さらに、なんと恐ろしいことにこのbotはPython 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) } }