GAE/GoでAppStoreのレビューをSlackに通知してくれるやつ作った


smart newsのレビューをslackに通知したサンプル

まだ、実際のAppStoreのレビューと見比べるとちゃんと全部拾えてないとか変なところありますが、そこそこできたので晒します。

リポジトリ

使い方とかはこちらで解説。アプリ3つ分ぐらいの処理であれば、GAEの無料分で十分運用できます。
https://github.com/yosukesuzuki/goisky-tools

フレームワークの選定

go言語の強者達はフレームワークなんていりません、net/httpでいいんじゃんて方が多いようなのですが、そんなに強者じゃないので、フレームワークがほしいです。

これまで、MartiniGorilla/Muxを使ったりしてましたが、Martiniは作者があまりやる気なくしていたり、Gorilla/Muxはフレームワークではなくtoolkitだと言っている通りフレームワークとしてのタガがゆるいので、自分のようなgo弱な人が使うとけっこうコードがぐじゃぐじゃになってしまいました。

Beegaeを採用

今回使ったのは、Beegoという上海在住の中国人?エンジニアが作っているフレームワークのGAE版=Beegaeです。割とわかりやすいMVC構造を持っています。

main.go <- ルーティング
┗ models/ <- モデル
┗ views/ <- ビュー/テンプレート
┗ controllers/ <- コントローラー

日本語のドキュメントはないですが、英語のドキュメントは整備されています。ユーザーコミュニティとしては中国人が多いようですが、githubでの会話は英語でできました。

その他、機能はいろいろ整備されていて、サービスをサッと作るにはなかなかいいです。

今回使ってないですが、GAEに特化したセッション機能とかも使えるようです。

ただ、ちょこちょこ、この機能なんで用意されてないの?みたいのがない気がします。ドキュメント全部把握してないだけかもしれないですが、JSONでレスポンス返すときにステータス・コード指定できないとか(JSONじゃなくてHTMLでエラーを返すなら簡単に指定できますが)。

MVCの構造

main.go

ルーティングの部分です。

func init() {
    beegae.TemplateLeft = "{{{"
    beegae.TemplateRight = "}}}"
    beegae.Router("/admin/api/v1/blobstoreimage/handler", &controllers.BlobStoreImageController{}, "*:Handler")
    beegae.Router("/admin/api/v1/blobstoreimage/uploadurl", &controllers.BlobStoreImageController{}, "*:UploadURL")
    beegae.Router("/admin/api/v1/blobstoreimage/:key_name", &controllers.BlobStoreImageController{}, "get:GetEntity;patch:UpdateEntity;delete:DeleteEntity")
    beegae.Router("/admin/api/v1/blobstoreimage", &controllers.BlobStoreImageController{})
    beegae.Router("/admin/task/iosapp/getappreview/:key_name", &controllers.IOSAppController{}, "*:GetAppReview")
    beegae.Router("/admin/task/iosapp/getreviews", &controllers.IOSAppController{}, "*:GetReviews")
    beegae.Router("/admin/api/v1/iosapp/:key_name", &controllers.IOSAppController{}, "get:GetEntity;patch:UpdateEntity;delete:DeleteEntity")
    beegae.Router("/admin/api/v1/iosapp", &controllers.IOSAppController{})
    beegae.Router("/admin/form", &controllers.AdminFormController{})
    beegae.Router("/admin/", &controllers.AdminController{})
    beegae.Router("/", &controllers.MainController{})
    beegae.Run()
}

REST APIでcontrollerのほうにGet()やPost()を実装していれば、何も書く必要ないです。メソッドごとに受けるfunctionを決めたい場合は第3引数で、

"get:GetEntity;patch:UpdateEntity;delete:DeleteEntity"

のように指定できます。

models

modelsには対応するDatastoreのKindごとにファイルを作ってます。annotationでjsonとdatastore上での名前を付けます。この部分の記述が長くなるのがイマイチなので、なんか勝手に命名規則にしたがって変えてくれるやつがほしい。というか知っていたら教えて下さい。

type IOSApp struct {
    KeyName    string    `json:"key_name" datastore:"KeyName"`
    AppID      string    `json:"app_id" datastore:"AppID"`
    Title      string    `json:"title" datastore:"Tite"`
    WebhookURL string    `json:"webhook_url" datastore:"WebhookURL"`
    IconURL    string    `json:"icon_url" datastore:"IconURL"`
    Content    string    `json:"content" datastore:"Content,noindex"`
    Region     string    `json:"region" datastore:"Region"`
    UpdatedAt  time.Time `json:"updated_at" datastore:"UpdatedAt"`
    CreatedAt  time.Time `json:"created_at" datastore:"CreatedAt"`
}

controllers

こちらも上記のmodelsに対応する形でファイルを作り、ルーティングされて来た処理を裁きます。

まず、下のように処理を書いて、this.Data["json"]のところにデータを渡すと最終的に、RenderのところでJSONにして返してくれます。このときにステータスコード渡したいのですが、そういう機能がないような。

func (this *IOSAppController) Get() {
    iosapps := []models.IOSApp{}
    _, err := datastore.NewQuery("IOSApp").Order("-UpdatedAt").GetAll(this.AppEngineCtx, &iosapps)
    if err != nil {
        this.Data["json"] = err
        return
    }
    listDataSet := map[string]interface{}{"items": iosapps}
    this.Data["json"] = listDataSet
}

func (this *IOSAppController) Render() error {
    if _, ok := this.Data["json"].(error); ok {
        this.AppEngineCtx.Errorf("iosapp error: %v", this.Data["json"])
    }
    this.ServeJson()
    return nil
}

また、JSONのREST APIで受ける処理が多いですが、一部HTMLを返すページ処理もあるので、こちらはその分ファイルが増えています。

ただ単に静的なHTMLを返す場合は以下のようにするだけです。

func (this *MainController) Get() {
    this.Layout = "layout.html"
    this.TplNames = "index.html"
}

appreviewについては、直接的にやり取りする必要がないので作ってません。

views

HTMLテンプレートが入っています。layout.htmlが外側の共通の枠になっていて、その中に各テンプレートが埋め込まれます。controllerで上のように指定します。

テンプレートの中で変数を使うにはgoでは通常 {{ variable }}のように記述しますが、今回使ったvuejsの標準のテンプレート表記とバッティングします。そこでgo側は{{{ variable }}}のように3つ括弧を続ける方式にしました。main.goのところで指定してます。

    beegae.TemplateLeft = "{{{"
    beegae.TemplateRight = "}}}"

レビューの取得処理

さて、レビューの取得処理をどうしているかというと、https://github.com/grych/AppStoreReviews というpython製のプログラムを発見してこちらの処理を参考にgoに移しました。

itunesアプリが使っていると思われるxmlを取得して、それをgoqueryというライブラリで処理しました。スクレイピングよりはだいぶいいですが、すごい処理しにくいXML構造で、ここの処理のコードはぶっちゃけだいぶ汚いです。標準のxml処理ライブラリとか使いたかったのですが、うまくいかなかったです。ベターな方法あれば書き直したいです。

GAEの特殊対応

travis ciとの連携

細かいことは前にGAE/GoのWebアプリをTravisCIで自動テスト&自動デプロイするで書きましたの参照してください。

今回新たにやったのは2つです。

最新のSDKを取得

GAEのSDKはどんどんバージョンアップしつつ、最新2つ分ぐらいしかダウンロードできなくなります。古いSDKは使わせたくないみたいですね。CIの環境づくりの時に1.9.17とかベタッとバージョン書いていると、SDKの取得処理で失敗してCIの実行が止まったりしてだるいです。これだけ直してコミットするのもだるいです。

いろいろ調べていたら、以下のURLから最新のSDKファイルのありかが、わかることがわかったのでそれを取得するスクリプト(ここはpython)を書きました。
https://www.googleapis.com/storage/v1/b/appengine-sdks/o?prefix=featured

go getを別スクリプトに

go get を.travis.ymlに書いて、依存ライブラリの取得が可能ですが、beegaeが依存するappengineライブラリはgo getではとれないのでエラーが出て、実行が止まってしまいます。それはappengineまわりは別途取得するのでエラー出ても無視したい。そこでgo getするシェルスクリプトを作って、実行しています。

#!/bin/sh
echo "running go get to fetch dependencies..."
go get github.com/PuerkitoBio/goquery
go get github.com/yosukesuzuki/beegae
echo "dependencies fetched."
exit 0

blobstore 対応

slack通知とは直接的に関係しないですが、blobstoreに画像をアップできる機能をつけています。blobstoreに画像アップしたうえで、picasaインフラで配信するURLを取得すると使い勝手がいいです。

Slack通知の際のアイコンをカスタマイズできる機能を用意してますが、その際に使いたい画像をアップロードして、picasaインフラで配信するURLの後ろに「=s57-c」とつけると57x57ピクセルの正方形画像に成形できます。(sのあとの数字は任意に変更できる)

例えば以下のとおりです。
https://lh4.ggpht.com/hwT-2OO2D1481EgmkATIR5WzfGlmh2KUkEs9ph6hBA1I9maQ7cDo7tVo9TeTppQMsBfD8e_Uezt_W0ExGaNE70CBTbTm=s57-c

元画像

=s57-cパラメータ付き

で、この機能が便利なのでblobstoreに画像をアップロードしたあとのhandler処理で、Datastoreへの登録とかしようと思っていたのですが、標準のbeegaeでは動きません。router.goのところで、ちょこちょこrequestのデータをいじくっているらしくて、それが悪さしているようでした。

他のユーザーも困っていて、pull-requestもあがってますが、取り込まれていません。
https://github.com/astaxie/beegae/pull/16

しかたないのでforkして、このpull-requestを取り込んだバージョンを自分で用意しました。

ちなみにgofmtを使うとimportのところを一気に書きなおすことができます。

$ gofmt -r '"github.com/astaxie/beegae" -> "github.com/yosukesuzuki/beegae"' -w ./

ただし、go界の巨人、mattnさんによるとgofmtとよりgorenameがいいようです。
http://mattn.kaoriya.net/software/lang/go/20150113141338.htm

local unit testがうまくいかない

この問題は、クリティカルなのですが、いったん放置しています。

$ goapp test
main.go:4:2: cannot find package "controllers" in any of:

goapp serveを実行した時はちゃんとbuildできているのに、goapp testでできないってどういうことなの?MLとかで情報探していますが、解決方法がまだわかりません。

かわりにcasperjsでのe2eテストを実行しています。

フロントエンド

GAE/Goとは関係ないですが、フロントエンドについても書いておきます。

Vuejs

angularも、jqueryも選択肢から外しつつ、reactjsは学習コスト的にきつそうだったのでvuejsで記述。goでAPIだけ用意してCRUDの部分はフロントでやっています。

Coffee Script

サービス用のコードをcoffeeで書くのは初めてでしたが、mizchiさんの解説読んだらうまくコンパイル環境ができました。chromeのdeveloper toolでもブレークポイントをcoffeeのまま付けられるのでデバッグも問題無いです。

Todo

  • unit testできるようにする
  • golintとするといろいろ怒られるので修正する

おまけというか経緯

自分個人で何か作ろうと思ったら基本的に、Google App Engineです。今回は下の記事を見て、この機能欲しいと思い作ってみました。

デフォルトの連携機能は用意されていませんが、App Store / Google Play でのレビューも Slack に流して皆で日々チェックしています。ちょっと前まで自前でストアのレビューをスクレイピングして流していましたが、メンテが大変なので、今は AppFigures と Zapier を組み合わせて実現しています。

AppFigures見て登録しようと思ったのですが、自分のアカウントをAppFiguresにあげるような動きが必要になるとか、有料だとかで断念しました。