はじめに
※ この記事は Tech KAYAC Advent Calendar 2015 11日目の記事です
はじめまして。「ぼくらの甲子園!ポケット」というスマフォゲームアプリの開発と運用を担当している@laoujiです。
サービスインしているゲームの運用をしているエンジニアにとってユーザの声を聞くことが非常に重要だと思います。 約半年前にApp StoreやGoogle Playのアプリレビューの内容を社内IRCに流すバッチスクリプトを作って、定時的にレビューを読むことを習慣にしています。
それから時間が経って、7日に@fujiwaraが説明したように、9月末ぐらいからチーム内のコミュニケーションをほぼ全部Slackに移行しました。
その作ったバッチスクリプトの結果がnopasteのマルチホスト対応のおかげで引き続きSlack上でも見れますが、Slackになるとやっぱりplain textがちょっとつまらなく見えるし、もうちょっとSlackっぽいもの作れるんじゃないかなと思うようになりました。
ということでjordgubbeというSlack向けwebhookを作ってみました。最近のレビューを引っ張ってこんな感じにSlackに流すツールです:
その作り方の話をさせてください。
iTunesのRSSフィード
App StoreのレビューはiTunesのフィードから取得できます。 アップル社が運用しているフィードですが、非公開でドキュメントなど存在しません。でも半年以上IRCに流すスクリプトは問題なく運用してきましたので、わりと安定している感じです。
フィードのエンドポイントは以下のようなurlになっています:
http://itunes.apple.com/jp/rss/customerreviews/page=1/id=アプリID/sortBy=mostRecent/xml
iTunesストアに公開されているアプリなら上記のようなurlを使ってその国のストアのレビューが取得できるはずです。 上記のurlの場合 .com/の後に /jp/があるので、日本のストアのものになります。/us/とか他の国のコードで置き換えることももちろん可能です。
page=1のところですが、フィードのレスポンスがレビュー50個のページに分かれていて、基本的に過去10ページまで取得可能です。 一応以下のような項目がフィードに埋め込んであるので、レビューが50個以上必要な時"next"のurlをクロールしながら過去のデーターを取りにいくといいでしょう。
<link rel="first" href="https://itunes.apple.com/jp/rss/customerreviews/page=1/id=アプリID/sortby=mostrecent/xml?urlDesc=/customerreviews/page=1/id=830062153/sortBy=mostRecent/xml"/> <link rel="last" href="https://itunes.apple.com/jp/rss/customerreviews/page=10/id=アプリID/sortby=mostrecent/xml?urlDesc=/customerreviews/page=1/id=830062153/sortBy=mostRecent/xml"/> <link rel="previous" href="https://itunes.apple.com/jp/rss/customerreviews/page=1/id=アプリID/sortby=mostrecent/xml?urlDesc=/customerreviews/page=1/id=830062153/sortBy=mostRecent/xml"/> <link rel="next" href="https://itunes.apple.com/jp/rss/customerreviews/page=2/id=アプリID/sortby=mostrecent/xml?urlDesc=/customerreviews/page=1/id=830062153/sortBy=mostRecent/xml"/>
後は、最後のxmlのところをjsonに置き換えるとJSONのフィードもありますが、 なぜかJSONのフィードのほうがxmlにある項目が全て揃っていません。 レビューの日付などかなり重要な項目もJSONのフィードにはありません・・・
XMLのほうは以下のようなレビューデーターが取得できます:
<entry> <updated>2015-12-04T14:28:00-07:00</updated> <id>1295884497</id> <title>自分自身がチームの個性になるゲーム</title> <content type="text"> このゲームの他の野球ゲームとの違いは、1人が1チームをもってやるのではなく、最大15人が1チームの学校の1選手になって甲子園を目指すという点です。 1人が強かったとしても、相手がチーム一丸となって試合に参加できるようなチームなら勝てないという、本当に甲子園をめざすようなリアルシミュレーションゲームです。 8時、12時、19時、23時ごろ行われる試合に、参加できない環境なら、本来のゲームの面白さを感じられないかもですが、その時間をゲームに費やせるならこれほど面白い野球ゲームはないと思います。ぜひ1度プレイしてみてください もし、このレビューが参考になったとお感じになられたら、このコードをゲーム内の友達紹介ページにコピぺしてください。 ゲームの課金ポイントがてにはいります。 SGC7ECYD </content> <im:contentType term="Application" label="アプリケーション"/> <im:voteSum>0</im:voteSum> <im:voteCount>0</im:voteCount> <im:rating>5</im:rating> <im:version>0</im:version> <author> <name>焙煎筋肉</name> <uri>https://itunes.apple.com/jp/reviews/id479949249</uri> </author> <link rel="related" href="https://itunes.apple.com/jp/review?id=830062153&type=Purple%20Software"/> <content type="html"> <table border="0" width="100%"> <tr> <td> <table border="0" width="100%" cellspacing="0" cellpadding="0"> <tr valign="top" align="left"> <td width="100%"> <b><a href="https://itunes.apple.com/jp/app/bokurano-jia-zi-yuan! poketto/id830062153?mt=8&uo=2">自分自身がチームの個性になるゲーム</a></b><br/> <font size="2" face="Helvetica,Arial,Geneva,Swiss,SunSans-Regular"> </font> </td> </tr> </table> </td> </tr> <tr> <td> <font size="2" face="Helvetica,Arial,Geneva,Swiss,SunSans-Regular"><br/>このゲームの他の野球ゲームとの違いは、1人が1チームをもってやるのではなく、最大15人が1チームの学校の1選手になって甲子園を目指すという点です。<br/>1人が強かったとしても、相手がチーム一丸となって試合に参加できるようなチームなら勝てないという、本当に甲子園をめざすようなリアルシミュレーションゲームです。<br/>8時、12時、19時、23時ごろ行われる試合に、参加できない環境なら、本来のゲームの面白さを感じられないかもですが、その時間をゲームに費やせるならこれほど面白い野球ゲームはないと思います。ぜひ1度プレイしてみてください<br/><br/>もし、このレビューが参考になったとお感じになられたら、このコードをゲーム内の友達紹介ページにコピぺしてください。<br/>ゲームの課金ポイントがてにはいります。<br/>SGC7ECYD</font><br/> </td> </tr> </table> </content> </entry>
そしてJSONのほうは以下:
{
"author":{
"uri":{"label":"https://itunes.apple.com/jp/reviews/id479949249"},
"name":{"label":"焙煎筋肉"},
"label":""
},
"im:version":{"label":"0"},
"im:rating":{"label":"5"},
"id":{"label":"1295884497"},
"title":{"label":"自分自身がチームの個性になるゲーム"},
"content":{
"label":"このゲームの他の野球ゲームとの違いは、1人が1チームをもってやるのではなく、最大15人が1チームの学校の1選手になって甲子園を目指すという点です。\n1人が強かったとしても、相手がチーム一丸となって試合に参加できるようなチームなら勝てないという、本当に甲子園をめざすようなリアルシミュレーションゲームです。\n8時、12時、19時、23時ごろ行われる試合に、参加できない環境なら、本来のゲームの面白さを感じられないかもですが、その時間をゲームに費やせるならこれほど面白い野球ゲームはないと思います。ぜひ1度プレイしてみてください\n\nもし、このレビューが参考になったとお感じになられたら、このコードをゲーム内の友達紹介ページにコピぺしてください。\nゲームの課金ポイントがてにはいります。\nSGC7ECYD",
"attributes":{"type":"text"}
},
"link":{
"attributes":{"rel":"related", "href":"https://itunes.apple.com/jp/review?id=830062153&type=Purple%20Software"}
},
"im:voteSum":{"label":"0"},
"im:contentType":{"attributes":{"term":"Application", "label":"アプリケーション"}},
"im:voteCount":{"label":"0"}
}
今回、レビューの日付もSlackに流したかったのでXMLのフィードのほうを使いました。
今まで使ってきた言語ではxmlのパージングってjsonと比較してちょっと面倒臭い感じありますが、 go言語のencoding/xmlがとても使いやすくて便利でした。
まず取りたいデーターを以下のようなstructで定義します:
import "encoding/xml"
type ReviewData struct {
Entries []struct {
Id string `xml:"id"`
Updated string `xml:"updated"`
Title string `xml:"title"`
Content []string `xml:"content"`
Rating int `xml:"rating"`
Author struct {
Name string `xml:"name"`
Uri string `xml:"uri"`
} `xml:"author"`
} `xml:"entry"`
}
Contentのタイプをsliceにしているのがxmlではtextとhtmlの2つのタイプが定義してあるからです。HTMLのほうは使う予定が特にないですが、これで両方の値が取得できます。
これでフィードをGETしてxml.Umarshalに渡すだけです。
uri := "http://itunes.apple.com/jp/rss/customerreviews/id=" + appId + "/sortBy=mostRecent/xml"
res, err := http.Get(uri)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
rawXml, _ := ioutil.ReadAll(res.Body)
reviewData := ReviewData{}
err := xml.Unmarshal(rawXml, &reviewData)
if err != nil {
log.Fatal(err)
}
Slackに投稿
今回作ったbotはcronを使って定時的にiTunesのフィードを叩いて、新着レビューをSlackに投稿するためのものなので、 Slackの incoming webhookでいい感じに実装できました。
SlackのRTM APIを使うと、シンプルなメッセージぐらいしか送れないんですが、webhookだったらAttachmentを使ってリッチなフォーマッティングを含めたメッセージも組み合わせることができます。
Slackに送るJSONのPayloadをこんな感じに組み合わせました:
type SlackPayload struct {
Text string `json:"text"`
UserName string `json:"username"`
IconEmoji string `json:"icon_emoji"`
Attachments []SlackAttachment `json:"attachments"`
}
type SlackAttachment struct {
Title string `json:"title"`
TitleLink string `json:"title_link"`
Text string `json:"text"`
Fallback string `json:"fallback"`
Fields []AttachmentField `json:"fields"`
}
type SlackAttachmentField struct {
Title string `json:"title"`
Value string `json:"value"`
Short bool `json:"short"`
}
フィードのentryタグに関する注意点が一つですが、最初の項目だけはユーザのレビューではなく、 iTunesのプレビューページで表示されるアプリの説明や詳細データーなので、Entries[0]を丸ごとスキップしておきました。
attachments := []SlackAttachment{}
for i, entry := range reviewData.Entries {
// first entry is the summary of the app so skip it
if i == 0 {
continue
}
updatedTime := time.Parse("2006-01-02T15:04:05-07:00", entry.Updated)
fields := []SlackAttachmentField{}
fields = append(fields, SlackAttachmentField{Title: "Rating", Value: strings.Repeat(":star:", review.Rating), Short: true})
fields = append(fields, SlackAttachmentField{Title: "Updated", Value: updatedTime.Format("2006-01-02 15:04:05"), Short: true})
attachments = append(attachments, SlackAttachment{
Title: entry.Title,
TitleLink: entry.Author.Uri,
Text: entry.Content[0], //type=textのみ
Fallback: entry.Title + " " + entry.Author.Uri,
Fields: fields,
})
}
jsonPayload, _ := json.Marshal(SlackPayload{
UserName: conf.BotName,
IconEmoji: conf.IconEmoji,
Text: conf.MessageText,
Attachments: attachments,
})
最後にjson化したpayloadデーターをSlackのAPIにPOSTするだけです。
slackUrl := "https://hooks.slack.com/services/dummy/dummy/dummy"
req, _ := http.NewRequest("POST", slackUrl, bytes.NewBuffer([]byte(jsonPayload)))
req.Header.Set("Content-Type", "application/json")
client := http.DefaultClient
res, err := client.Do(req)
if err != nil {
log.Fatal(err)
}
defer res.Body.Close()
以下のようなメッセージができあがりました:
まとめ
App StoreのフィードとSlackのincoming webhookの紹介でした。
これで毎日ユーザの大事な意見をしっかり読んで、もっともっといいゲームアプリ作って行けそうですね!
明日
明日はトレンディな@ryusukefudaです!