smart newsのレビューをslackに通知したサンプル
まだ、実際のAppStoreのレビューと見比べるとちゃんと全部拾えてないとか変なところありますが、そこそこできたので晒します。
リポジトリ
使い方とかはこちらで解説。アプリ3つ分ぐらいの処理であれば、GAEの無料分で十分運用できます。
https://github.com/yosukesuzuki/goisky-tools
フレームワークの選定
go言語の強者達はフレームワークなんていりません、net/httpでいいんじゃんて方が多いようなのですが、そんなに強者じゃないので、フレームワークがほしいです。
これまで、MartiniとGorilla/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のあとの数字は任意に変更できる)
で、この機能が便利なので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にあげるような動きが必要になるとか、有料だとかで断念しました。