話題の記事

IngressポータルLive状況を目視化&共有化できるWebツールを作った話

2015.07.10

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

大村@Enlightenedです。
前回のメイド喫茶の記事がやたらと波紋を呼んでおり、私としては自分のワークスタイルの一部を伝えられてよかったのかなと思います(震え声)。
社内で批判的な声が出てこなかったことで、私も市民権を得た気分で仕事ができるものです(震え声)。

今回は、以前使った「Google+ プラットフォームを利用してiOSのユーザーログインを簡潔にする」のWeb API版を使い、メールを解析してその結果を表示するというツールを作ってみた話をしたいと思います。

Ingressとは

Ingressとはなんぞや、という話をしようと思ったのですが、既にこのdevelopers.ioにて一度紹介していますので、詳しい話は「Ingress×ローソン 情報疲労時代の体験型マーケティング勉強会に参加してきました」を見てください。
ここでは、Ingressのどの部分を便利にするツールを作ったのかを説明したと思います。

そもそも何をしたかったのか

Ingressの世界では、ポータルと呼ばれるXMが溢れだすチェックポイントに行き、アイテムを取得したり自分の陣営に引き込んだり、自分の陣営の陣地を増やしたりします。
まずは、このポータルがすべての起点となります。
しかし、このIngressのポータルは、全てがナイアンティックラボ(運営元)が登録しているわけではありません。
ゲームの初期ポータルは準備されましたが、それ以降のポータルはエージェント(ゲームのプレイヤー)が申請し、それをナイアンティックラボがポータルの基準に沿ってるか審査して、Live(許可)、Duplicate(重複している)、too close(近すぎる)、Reject(相応しくない)を判断します。
Ingressのゲームの楽しみ方として、このポータルを新しく生やすという行為も含まれており、それらのポータルは近場のエージェント行動を左右すると言っても過言ではないです(実際に通勤通学経路が変化した人も多いです)。

このポータルを生やす遊び方はあるのですが、なかなかプレイヤーの実績として見えづらく、人に伝えるときも、ポータルに付属する写真程度でしか説明することが出来ませんでした。
また、自分がいつ何を申請したのかがゲーム上からはわからず、同じスポットを何度も登録してしまうという事態が発生していました。
そこで、このポータルの申請状況、ポータルの申請結果を、一覧として表示することが求められました。

どうやって解決をするのか

ではどうやってこれらのポータル申請状況を管理するのか。
Ingressのガイドラインには以下の文面が記載されています。

不正行為: 不正行為は禁止されています。ルールを守り、正規の Niantic Labs ソフトウェア以外は使わないでください。
Ingress は携帯端末でプレイし、プレーヤーに外に出て世界を探索してもらうことを目的としています。
残念なことに不正行為の手法は多岐にわたります。
よくある手口だけでも、改ざんされたソフトウェアや非正規のソフトウェアを使用する、複数のアカウントでプレイする
(プレーヤー 1 人につき 1 つのアカウントでお願いします)、アカウントを共有する、八百長行為をする、
ツールやテクニックを使って位置情報の変更や改ざんを行う、アカウントを売買するなどが挙げられます。

割とよく見る制約で「アプリを改ざんしたり影響与えるソフトウェアを使用して色々やらないで正規の手順でゲームを楽しんでね。」ってことです。アプリケーションからログを取得するなどは出来ないわけです(多分不可能ではないでしょうけれど、規約を違反してまでやることではないです)。
では、どうやってこれを解決するのかの話です。

実は、Ingress内での行動のうち、以下のものはメールにてレポートを受け取ることが出来ます。

  • ポータルの攻撃破壊通知
  • ポータルの申請通知
  • ポータルの審査結果通知

今回は、このメールでレポートされるポータル申請通知・審査通知を解析することで実現しています。
メールの内容はここで詳しく取り上げてもしょうがないので、こちらのIngressポータル申請のススメさんの中盤あたりを参考にして下さい。

Ingressの特徴としてGoogle+のアカウントでゲームを登録する→そのアカウントに通知メールなどが届く、という至って簡単な仕組みがあります。 ということは、Google+アカウントのメール=Gmailというわけです。
昨年夏からGmail Rest APIが公開されています。
これをうまく活用できれば、ユーザ認証はGoogle+のみでOK、後はメール内容をGmail Rest APIで抽出してゴリゴリ回して、そして結果を自由にできる感じの素敵な事ができるかなぁ、ということで、早速実践してみましょう。

Gmail Rest API

Gmail Rest APIはAndroidやiOSをはじめ、.NETやJava、Python、Ruby、そしてPHPなどでGmailの便利な機能が利用できるRestfulなAPIです。
具体的には

  • Gmailの特徴である大容量のメールボックスを使える
  • アプリケーション内にGmail送受信機能をほぼほぼ取り込むことができる
  • 強力な検索機能が使える(さすがGoogleといったところ)

メール取得だけだったら、なにもGmail使わなくてもいいのですが、やはりGoogleといったところか、検索機能が充実しています。
キーワード検索だけではなく、キーワード除外検索、ラベル検索、添付ファイルの種類での検索といったことが細かく指定できます。

それでは実装をしていきましょう。

Googleアプリとして登録する

Google Developers Consoleでログインするためのアプリケーションを登録します。
アプリケーションを登録したら、Googleアカウントでのログイン時に同意画面が表示されるので、その同意画面を作ります。
今回は便宜上、Ingressを便利にするアプリという意味でIngress Usefulにしました。

oauthkey1

同意画面を作った後は、新しいクライアントIDを作成します。
アプリケーションの種類は「ウェブアプリケーション」、リダイレクトURIはOAuth認証の戻り先になるので、「http://ingress.abgist.com/callback.php」みたいな感じで。
JavaScript 生成元はとりあえず「http://ingress.abgist.com/」にしました。

これによって、クライアントIDとクライアントシークレットが作成されました。この情報はあとで使います。

oauthkey2

今度は、このGoogleアプリでGmail APIの利用を許可します。
よく使われているAPI一覧のGoogle Apps APIからGmail APIを選択し、上部の「APIを有効にする」を選択します。
なお、無料枠でのGmail APIの使用料の限度は1,000,000,000ユニット数/日ですが、よっぽどのアプリに成長しないとこの数値は超えそうにないので当面は無視して平気です。

gmail_api

サーバー

弊社クラスメソッドでは通常AWSを使って実装をすることが多いです。
が、個人で実装する上では敷居が高いので、私個人で契約している「さくらのVPS 2Gプラン」で実装してます。
個人的に1998年からさくらウェブ使ってた関係上とっつきやすかったのと、個人ではVPSのほうが利便性と遊べる環境として適してるイメージです。
最近さくらのクラウドが始まり、ちょっと触ってみたいとは思います。

コーディングする

流れとしては、まずはOAuthでログインする→トークンを使ってGmail APIを使う という流れになります。
その前にGoogle API PHP Clientを導入します。 ウェブで公開していないディレクトリにsrc/Google以下のファイルを配置して下さい。
ただし、PHPファイルからincludeできるところにして下さい。

私の場合、以下の様な配置にしています。
/var/www/
├ html トップページ他ingress.abgist.comの配下
└ google src/Google以下のファイル

これで終了です。
基本的に、GoogleのAPIやOAuthを介するページの先頭で、暗黙的に以下のコードを埋め込んで使用します。

<?php
require_once ('../google/autoload.php');
session_start ();
?>

ここではセッションも併せて張っています。

ログインの実装

まずはログインの実装を行います。
ログインしてたら各種メニューを、ログインしてなければログインページヘのリンクを、表示させるようにします。

index.php

<?php
if (! is_null ( $_SESSION ['token'] )) {
    /* ログイン確認する */
    $client = new Google_Client ();
    try {
        $client->setAccessToken ( $_SESSION ['token'] );
        if ($client->isAccessTokenExpired ()) {
            /* 更新作業 */
            unset ( $_SESSION ['token'] );
            header ( "Location: index.php" );
            }
?>
〜ログイン後処理〜
<?php
    } catch ( Exception $e ) {
        unset ( $_SESSION ['token'] );
        header ( "Location: index.php" );
    }
} else {
?>
〜ログイン前処理〜
<?php
}
?>

ログイン周りの実装は以下のようにしています。

login.php

$client = new Google_Client();
$client->setClientId ( 'クライアントID' );
$client->setClientSecret ( 'クライアントシークレット' );
$client->setRedirectUri ( "http://" . $_SERVER ['SERVER_NAME'] . "/callback.php" );
$client->setDeveloperKey ( 'デベロッパーKey' );

$scope = array(
    'https://www.googleapis.com/auth/userinfo.profile', // 基本情報(名前とか画像とか)
    'https://www.googleapis.com/auth/userinfo.email',   // メールアドレス
    'https://www.googleapis.com/auth/gmail.readonly', // Gmail のメール取得
);
$client->setScopes($scope);
// 認証用URL作成
$authUrl = $client->createAuthUrl();

// Redirect
    header("Location: " . $authUrl);

scopeで、ログイン中のアプリケーションがアクセスできる権限をユーザーに許可をもらいます。
ここでは、ログイン情報を使う関係上ユーザー情報とメールアドレスを、そして送られてくるgmailを読み込む設定をしています。
Gmail関係のscopeは以下のものが設定できます。

Scope URI 権限内容
https://www.googleapis.com/auth/gmail.readonly メールリソースの読み込み
https://www.googleapis.com/auth/gmail.compose Draft(下書き)に対してのアクセス
https://www.googleapis.com/auth/gmail.insert メッセージの作成を行える
https://www.googleapis.com/auth/gmail.labels ラベルの作成・削除など管理
https://www.googleapis.com/auth/gmail.modify メールの完全削除を除く読み書き更新のアクセス
https://mail.google.com/ Gmailへのフルアクセス。削除も込み

リダイレクトされてGoogle+へのログインを求められます。
Ingressからのメールを受信しているアカウントでログインします。

ログインすると権限に対しての同意画面が表示されます。
同意するとリダイレクトで設定したURIへとコールバックされます。
コールバック先では下記のような実装を指定ます。

callback.php

$client = new Google_Client ();
$client->setClientId ( 'クライアントID' );
$client->setClientSecret ( 'クライアントシークレット' );
$client->setRedirectUri ( "http://" . $_SERVER ['SERVER_NAME'] . "/callback.php" );
$client->setDeveloperKey ( 'デベロッパーKey' );

/* codeが返却される */
if (isset ( $_GET ['code'] )) {
    $client->authenticate ( $_GET ['code'] );
    $_SESSION ['token'] = $client->getAccessToken ();
}
if ($client->getAccessToken () && isset ( $_GET ['url'] )) {
    $url = new Google_Service_Urlshortener_Url ();
    $url->longUrl = $_GET ['url'];
    $short = $service->url->insert ( $url );
    $_SESSION ['token'] = $client->getAccessToken ();
}
try {
    $oauth = new Google_Service_Oauth2 ( $client );

    /* 各種情報取得 */
    $guid = $oauth->userinfo->get ()->getId ();
    $mail = $oauth->userinfo->get ()->getEmail ();
    $url = $oauth->userinfo->get ()->getLink ();
    $name = $oauth->userinfo->get ()->getName ();
} catch ( Exception $e ) {
    unset ( $_SESSION ['token'] );
    /* 何らかの理由で失敗 */
    header ( "Location: index.php" );
}

ここで返却されたtokenをこの後使っていくことになります。

Gmailの読み込み

Gmailを読み込みます。
メッセージIDを取得して、その後メール本文を受信します。

portal_mail.php

$client = new Google_Client ();
$client->setAccessToken ( $_SESSION ['token'] );
/* Gmail APIクラス */
$gmail = new Google_Service_Gmail ( $client );

/* 実際のメール処理はMailPortalクラスで行う */
$portal_mail = new MailPortal ( $gmail );
/* 一覧を取得する */
$maillist = $portal_mail->getMailList ();
while ( count ( $maillist ) ) {
    $mail_id = array_pop ( $maillist )->getId ();
    /* メール情報を取得する */
    $mail = $portal_mail->getMailInfo ( $mail_id );
}

/* Gmailを処理するクラス */
class MailPortal {
    var $gmail; /* gmailアクセスクラス */
    function MailPortal($mail) {
        $this->gmail = $mail;
        return $this;
    }

    /* メール一覧を取得する */
    function getMailList() {
        /* メールリスト初期化 */
        $maillist = array ();
        $count = "今までの処理件数"; /* 本題ではないのでここでは省略する */
        if($count == 0){
            /* 今までユーザーの申請処理を行っていない */
            $query = 'subject:"Ingress Portal " from:ingress-support@google.com';
        } else {
            /* 処理されている */
            $judge_time = new DateTime("最終取得日");
            $judgedate = $judge_time->modify('-1 days')->format('Y/m/d');
            $query = 'subject:"Ingress Portal " from:ingress-support@google.com newer:' . $judgedate;
        }
        
        /* $nextTokenが無くなるまで取得し続ける */
        do {
            $optParams = [ ];
            /* 一覧取得条件を設定する */
            $optParams ['q'] = $query;
            if (isset ( $nextToken )) {
                $optParams ['pageToken'] = $nextToken;
            }
            $messages = $this->gmail->users_messages->listUsersMessages ( 'me', $optParams );
            /* 一覧を取得する(GmailMessage配列) */
            $list = $messages->getMessages ();
            /* 取得した一覧をマージする */
            $maillist = array_merge ( $maillist, $list );
            $nextToken = $messages->getNextPageToken ();
        } while ( isset ( $nextToken ) );

        return $maillist;
    }
    〜続く〜

ここで、一覧を取得する時の条件がポイントです。
今回は、既に取得済みの時は、最後の取得時間より新しいデータを取ってくる必要があります。
そのため、「'subject:"Ingress Portal " from:ingress-support@google.com newer:' . $judgedate;」となっています。
逆に、1件も取得していない場合は全件取得する必要があるため、「'subject:"Ingress Portal " from:ingress-support@google.com」という条件になります。
ここで指定できる条件は、「Gmailヘルプ 詳細設定」に載っています。

あとは、受け取ったメールを解析して

  • ポータルの申請通知
  • ポータルの審査結果通知

のどちらかなのかを振り分けていきます。
申請通知1通に対して、必ず後日審査結果通知1通が来ます。
ただし、申請通知は届かないこともある(オプション設定でOFFにしてたなど)ので、必ずしも必要というわけではない感じで紐付けをしていきます。

メールの本文やタイトルなどは、以下で取得することが出来ます。

portal_mail.php 続き

    〜上記から〜
    /* メール1件を取得する */
    /* GmailMessageのgetId()で取得できるIDを引数とする */
    function getMailInfo($id) {
        $message = $this->gmail->users_messages->get ( 'me', $id );
        $parts = $message->getPayload ()->getParts ();
        /* メール本文を取得する */
        $body = $parts [1] ['body']->data; /* 0:text 1:html */
        /* ヘッダを取得する */
        $headers = $message->getPayload ()->getHeaders ();
        /* ヘッダはリストで返却される */
        foreach ( $headers as $header ) {
            switch ($header->getName ()) {
                case "Subject" :
                    $subject = $header->getValue ();
                    break;
                case "Date" :
                    $date = DateTime::createFromFormat ( DateTime::RFC2822, $header->getValue () );
                    $date->setTimezone(new DateTimeZone('Asia/Tokyo'));
                    $datetime = $date->format ( 'Y-m-d H:i:s' );
                    break;
            }
        }
        $sanitizedData = strtr ( $body, '-_', '+/' );
        $decodedMessage = base64_decode ( $sanitizedData );
        $response = array (
            'subject' => $subject,
            'date' => $datetime,
            'body' => $decodedMessage 
        );
        return $response;
    }
}

解析自体は今回は主目的ではないため、今回は省略します。

取得したデータを見やすい形でアウトプットする

取得したデータを人の見やすい形に整形します。
今現在見れる方法は、表の形式と、地図の形式です。
地図の形式に関しては今後触れることにして、アウトプットがどんなものかを簡単に紹介します。

ポータル申請解析表

シェア用のIDを引数にアクセスすると、どのアイテムを申請したかが判る表です。
他人にリンクをシェアすることで、申請状況を共有することが可能です。
私の申請結果はこちらで見ることが出来ます

ingress_table

自ポータルLiveマップ

同じく、シェア用のIDを引数にアクセスすると、自分の申請したもののうち、申請が通った物がどこにあるかを見ることができるマップです。
併せて、どのくらいの日数がかかったかも併記しています。
表同様、リンクをシェアすることで申請結果を共有することが可能です。
私の申請結果はこちらで見ることが出来ます

ingress_map

おわりに

今回はGmail APIを使ってIngressがより便利になるツールを作った話をしました。
もともと、Enlightenedで動いている人向けにこっそりやっていたのですが、割と某所で拡散されてしまって存在がおおっぴらに出たため、だったらこのような形でTipsを残そうという思考に至りました。
こういったアイテムは「あったらいいな~誰か作らないかな〜だったら自分が作ろうかな〜」から始まるものだと信じています。
自分が欲しかったので作りました。今後もなにか面白いもの作れないかなーと思って活動していきます。

また、Ingressをはじめる際は、緑色のEnlightenedがいいと思います。
目に優しいし、ゲームのストーリーでは新しいテクノロジーを受け入れる派です。
といったところで、今回はこのへんで。

注意

メールのフォーマットが先月か先々月ぐらいに変更になったため、本ツールを使うと最近のポータル申請結果が反映されないという問題が発生しています。
対応自体は行うと思っているのですが、時間がとれた時にちまちまとやっているため、もう少しだけお待ちください。
また、作り始めた時は一般公開とか考えずやってたので、デザイン面を気にしていませんでした。せめてFuelPHPで作り始めればよかったです。
フレームワークを使えば、もっと簡単に実装できると思います・・・。