2013年6月30日日曜日

Go の template パッケージで map の key を参照する

GAE + Go では HTML を html/template パッケージで出力することが基本になります。
このとき {{}} 構文によって Go 内の変数を HTML 内に展開することができます。

array や map や slice の中身を順次取り出しながら表示する場合は
{{range}}{{end}} 構文を使って書くことができます。
<ul>
  {{range .}}
    <li>{{.Name}}さんこんにちは</li>
  {{end}}
</ul>

ただ中身を表示するだけならこれでよいのですが、
map を使っている場合、どうしても key を埋め込みたい時があるでしょう。
(クリックした要素の key をサーバへ返したいときなど)

この場合は以下のように for each 文っぽく key とvalue を変数に入れることができます。
<ul id="gamelist">
  {{range $key, $val := .}}
    <li class="game" key="{{$key}}">
      <div class="title">{{$val.Name}}</div>
      <div class="description">{{$val.Description}}</div>
      <div class="thumbnail"><img width="200" src="/client/img/living.png"></div>
      <a href="/editor?game_key={{$key}}"><button class="edit">作る</button></a>
      <button class="copy">コピー</button>
      <button class="delete">消す</button>
    </li>
  {{end}}
</ul>

ちなみに $val は無くても同じように動きます。



地味に 100 本目の投稿です。

2013年6月29日土曜日

Xcode で grep 検索

Xcode でワークスペース内のすべてのファイルから任意の文字列を探す方法です。
Shift + ⌘ + F で grep 検索ができます。
コミットする前にデバッグラインが残ってないか探すときなどに便利です。

検索結果をクリックすると該当箇所へジャンプできる

2013年6月28日金曜日

GAE + Go でログを取る

GAE + Go では appengine パッケージにログを取る命令が入っています。

The appengine package - Google App Engine
https://developers.google.com/appengine/docs/go/reference

import (
    "appengine"
    "net/http"
)
func init() {
    http.HandleFunc("/", debug)
}
func debug(w http.ResponseWriter, r *http.Request) {
    c := appengine.NewContext(r)
    c.Debugf("Debug")
    c.Infof("Infomation")
    c.Warningf("Warning")
    c.Criticalf("Critical")
}

c.Infof() や Debugf() でログを取ることができます。
ローカル開発よサーバの場合、コマンドプロンプトにログが出力されます。
本番環境の場合はアプリ管理画面にログが出力されます。
Debug だけ表示、Error だけ表示などのように絞込みも可能です。


2013年6月27日木曜日

Go でバイナリの比較

バイナリとバイナリの比較方法です。
bytes パッケージの Compare() を使います。
a := randomBytes()
b := randomBytes()
if bytes.Compare(a, b) == 0 {
    log.Printf("同じです")
} else {
    log.Printf("違います")
}

bytes package - Go lang
http://golang.org/pkg/bytes/

2013年6月26日水曜日

GAE + Go でメールを送る

Google App Engine ではメールの送信もできます。
ユーザ新規登録時の確認メールなどに使えます。

送信元になるメールアドレスはデフォルトで

  • アプリ管理者のGmailアドレス
  • string@appid.appspotmail.com

が使えます。
string は任意の文字列、appid はアプリURLの ***.appspot.com と同じ文字列です。

メールを使うには app.yaml に以下を追加する必要があります。
inbound_services:
- mail

以下のように送信します。
message := new(mail.Message)
message.Sender = "infomartion@appid.appspotmail.com"
message.To = []string{"who@example.com"}
message.Subject = "タイトル"
message.Body = `

メールの本文です。
複数人にも送ることができます。

`

c := appengine.NewContext(r)
err := mail.Send(c, message)
if err != nil {
    c.Errorf(err.Error())
}

2013年6月25日火曜日

backlog バージョンアップ

backlog がバージョンアップされました。

詳しい情報は backlog 公式ブログ
http://www.backlog.jp/blog/2013/06/release-201306.html

まず、プロジェクトの色が設定できるようになっています。
画面の色が全体的に変えられます。

ピンクの豹柄とか

アラビアンな感じ

黒くしたらこんな感じでシック

そして、課題の親子関係
説明を読んでみたら超便利そうです
課題がツリー構造を取れるようで、
大きなタスクの処理中に出現した小さなタスクをぶら下げて定義できるそうです。
いままで関連タスクとしてURLを貼って参照しあっていたので欲しかった機能。
しかし、さっそくやってみようと思ったら設定項目が見当たらず・・・。



なんと有料プラン専用だそうです・・・。
無料プランなので試せませんでした。


あとは、課題のテキストエリアにボタンが表示されました。
いつもは [] とか {code}{/code} とか打ち込んでいたのがボタンで出せるようになります。
ちょっと便利ですね。

右上にボタンが出現

他にもコメントが折りたためるようになっているなど変わっているらしいです。

2013年6月24日月曜日

datastore の String にバイナリをいれてしまったら

GAE + Go で datastore の String 要素にバイナリデータを入れると、

localhost:8080/_ah/admin

のデータストア管理画面がエラーを吐くようになります。


これでは中身が見れないのでデータストアをクリアしなければなりません。
GAE のローカルサーバを起動するときに -c または --clear_datastore オプションを付けます。
$ dev_appserver.py --clear_datastore myapp

これでデータストアをクリアすることができ、エラーが出なくなります。
バイナリデータを保存するときには必ず []byte を設定するように注意しましょう。

2013年6月23日日曜日

datastore の要素名は大文字で始める

GAE + Go ではデータを保存するために datastore というデータベースが使われます。

Storing Data - Google App Engine
https://developers.google.com/appengine/docs/go/datastore/

datastore では構造体やマップを保存することができます。
このとき構造体のメンバ名は必ず大文字で始めなければいけません。
import (
    "appengine"
    "appengine/datastore"
)

// 小文字で要素名が始まっているので保存されない
type Entity struct {
    name string
    value string
}

/*
// 大文字で始めると保存される
type Entity struct {
    Name string
    Value string
}
*/

func save(w http.ResponseWriter, r *http.Request) {
    entity := new(Entity)
    entity.name = "hoge"
    entity.value = "piyo"
    /*
    entity.Name = "hoge"
    entity.Value = "piyo"
    */
    
    c := appengine.NewContext(r)
    key := datastore.IncompleteKey(c, "Entity", nil)
    _, err := datastore.Put(c, key, entity)
    if err != nil {
        c.Errorf(err.Error())
    }
}

小文字で始めた場合、datastore には空のデータが保存されます。
実際に中身を確認してみると name や value が保存されていないことがわかります。
(ID や Key Name はデータとは関係なく自動で作成されます。)



なぜなら Go では変数名の最初が大文字か小文字かによって意味が異なるためです。
大文字の変数はパッケージ外に公開され、小文字の変数は公開されません。
つまりパッケージ内だけで使いたい変数は小文字ではじめ、
パッケージ外から参照される変数は大文字で始めます。
クラス名を大文字で、インスタンス名を小文字で始めるような習慣と混ざってちょっとわかりづらい気もしますが・・・。

構造体の要素名は datastore パッケージから参照されるため大文字で始めます。
小文字で始まっているものは datastore パッケージからは見えていません。
つまり上のコードの Entity は空の構造体であるとみなされます。
// 中からは name や value が見える
type Entity struct {
    name string
    value string
}

// 外からは空に見える
type Entity struct {
}

よって、空のデータが保存されます。
エラーにはならないため、これを知らないと詰まります。

他にも Go のパッケージでは変数名を大文字で始めることを要求するものが多いので注意が必要です。

2013年6月22日土曜日

GAE でアプリのデプロイ処理が終わらない現象

GAE の appcfg.py update が永遠に終わらない現象に遭遇しました。
Mac を再起動すると治りました。


GAE でアプリをデプロイするときは以下の様なコマンドを使います。
$ appcfg.py update アプリ名

正常に終了すると以下の様なログが出力されます。
$ appcfg.py update escape3ds/
07:20 PM Host: appengine.google.com
07:20 PM Application: escape-3ds; version: 0
07:20 PM 
Starting update of app: escape-3ds, version: 0
07:20 PM Getting current resource limits.
07:20 PM Scanning files on local disk.
07:20 PM Cloning 10 static files.
07:20 PM Cloning 14 application files.
07:20 PM Uploading 1 files and blobs.
07:20 PM Uploaded 1 files and blobs
07:20 PM Compilation starting.
07:20 PM Compilation: 8 files left.
07:20 PM Compilation completed.
07:20 PM Starting deployment.
07:20 PM Checking if deployment succeeded.
07:20 PM Deployment successful.
07:20 PM Checking if updated app version is serving.
07:20 PM Completed update of app: escape-3ds, version: 0

しかし、以下のように永遠に終わらない状態になりました。
$ appcfg.py update escape3ds/
07:05 PM Host: appengine.google.com
07:05 PM Application: escape-3ds; version: 0
07:05 PM 
Starting update of app: escape-3ds, version: 0
07:05 PM Getting current resource limits.
07:05 PM Scanning files on local disk.
07:05 PM Cloning 10 static files.
07:06 PM Cloning 14 application files.
07:06 PM Compilation starting.
07:06 PM Compilation: 8 files left.
07:06 PM Compilation completed.
07:06 PM Starting deployment.
07:06 PM Checking if deployment succeeded.
07:06 PM Deployment successful.
07:06 PM Checking if updated app version is serving.
07:06 PM Will check again in 1 seconds.
07:06 PM Checking if updated app version is serving.
07:06 PM Will check again in 2 seconds.
07:06 PM Checking if updated app version is serving.
07:06 PM Will check again in 4 seconds.
07:06 PM Checking if updated app version is serving.
07:06 PM Will check again in 8 seconds.
07:06 PM Checking if updated app version is serving.
07:06 PM Will check again in 16 seconds.
07:06 PM Checking if updated app version is serving.
07:06 PM Will check again in 32 seconds.
07:07 PM Checking if updated app version is serving.
07:07 PM Will check again in 60 seconds.
07:08 PM Checking if updated app version is serving.
07:08 PM Will check again in 60 seconds.
07:09 PM Checking if updated app version is serving.
07:09 PM Will check again in 60 seconds.
07:10 PM Checking if updated app version is serving.
07:10 PM Will check again in 60 seconds.
07:11 PM Checking if updated app version is serving.
07:11 PM Will check again in 60 seconds.
07:12 PM Checking if updated app version is serving.
07:12 PM Will check again in 60 seconds.
07:13 PM Checking if updated app version is serving.
07:13 PM Will check again in 60 seconds.
07:14 PM Checking if updated app version is serving.
07:14 PM Will check again in 60 seconds.
07:15 PM Checking if updated app version is serving.
07:15 PM Will check again in 60 seconds.
07:16 PM Checking if updated app version is serving.
07:16 PM Will check again in 60 seconds.
......

⌘ + C で中断してもう1度実行しても同じ現象が起きます。
短い時間に連続してデプロイ処理を行なっていたので、それが原因かも。

調べてみると同じ現象が起こっている人がいました。

 Anyone else having difficulty deploying to GAE in the last two days?
 https://groups.google.com/forum/?fromgroups#!topic/google-appengine/4nacCuJXClk

1度 Mac を再起動してみたら、正常な動作に戻りました。

2013年6月21日金曜日

JavaScript でコールバックするときの注意

JavaScript では、this がどのオブジェクトを指しているのか注意が必要です。

例えば、クラスA が クラスB へ非同期処理をお願いして、終わったら渡しておいたコールバックを呼び出してもらうとします。
/**
 * クラスA
 * @class
 * @property {B} b クラスBのインスタンス
 */
var A = function() {
    this.myname = 'A';
    this.b = new B();
};

/**
 * クラスAの処理
 * B にコールバックを渡す
 * 非同期な処理が終わったらコールバックを呼び出してもらう
 * @method
 */
A.prototype.start = function() {
    this.b.start(this.callback);
};

/**
 * コールバック関数
 * @method
 */
A.prototype.callback = function() {
    alert('私の名前は ' + this.myname + ' です');
};

/**
 * クラスB
 * @class
 */
var B = function() {
}

/**
 * 非同期処理を行う
 * 終わったらコールバックを呼び出す
 * @method
 * @param {Function} callback コールバック関数
 */
B.prototype.start = function(callback) {
 setTimeout(function() {
  callback();
 });
};

// 実行
var a = new A();
a.start();    // "私の名前は undefined です"

this が a を指すと思いきや window を指します。
window.myname は未定義なので unndefined が出力されます。


こういう時は callback と一緒に自分自身を渡して、callback 呼び出し時に this として自分自身を指定してもらえば大丈夫です。
this を指定して関数を呼ぶときは call() を使います。
/**
 * クラスA
 * @class
 * @property {B} b クラスBのインスタンス
 */
var A = function() {
 this.myname = 'A';
    this.b = new B();
};

/**
 * クラスAの処理
 * B にコールバックを渡す
 * 非同期な処理が終わったらコールバックを呼び出してもらう
 * @method
 */
A.prototype.start = function() {
    this.b.start(this, this.callback);
};

/**
 * コールバック関数
 * @method
 */
A.prototype.callback = function() {
    alert('私の名前は ' + this.myname + ' です');
};

/**
 * クラスB
 * @class
 */
var B = function() {
}

/**
 * 非同期処理を行う
 * 終わったらコールバックを呼び出す
 * @method
 * @param {A} caller 呼び出し元のインスタンス
 * @param {Function} callback コールバック関数
 */
B.prototype.start = function(caller, callback) {
 setTimeout(function() {
  callback.call(caller);  // this = caller として callback() を実行
 });
};

// 実行
var a = new A();
a.start();    // "私の名前は A です"

2013年6月20日木曜日

Xcode で C 言語

久しぶりに C 言語を書こうと gcc や vi を使おうとしても、普段 Xcode を使ってるとちょっと面倒くさいかったりします。

Xcode で書けないのかなと調べてみたら書けるようですね。

File -> New -> Project

OSX Application -> Command Line Tool



Type で C を指定



これで C 言語のコンソールプログラムが書けます。

2013年6月19日水曜日

ニコ生とC言語

ペアプログラミング気分を味わいたいときはニコ生のプログラミング配信をよく見ています。

 ニコニコ生放送
 http://live.nicovideo.jp


「開発」や「プログラミング」で検索すると、意外と出てきます。
趣味でコードを書き始めたおじさん、Unity でバリバリゲームを作っているお兄さん、「やさしいC」と格闘する中学生まで様々です。
どんなことでも人に見せることを意識するとモチベーションが上がりますよね。

放送中に Stypi などのリアルタイムで共有できるエディタを使ってみんなでわいわいコードを書いたりすると結構楽しいものです。

 Stypi
 https://code.stypi.com/

そこで放送中に C 言語のコンソールゲーム書きました。
wasd で移動してアイテムを集めます。敵に当たったらゲームオーパーです。



C言語を触り始めた頃はよくこんなかんじのゲーム作りましたよね。
/**
 * ダンジョンRPG
 * C言語でダンジョンRPG的なものを作る
 *
 * - 画面の見方 -
 * @ : 主人公
 * X : 敵
 * * : アイテム
 *
 * - ルール -
 * 敵に当たらずにアイテムを集めろ
 *
 * - 操作方法 -
 * w: 上
 * a: 左
 * s: 下
 * d: 右
 */
#include <stdio .h="">
#include <stdlib .h="">

// プロトタイプ宣言
void showMap();
void showMessage(char message[]);
int getKey();
void clear();
void moveTo(int x, int y);
void setItems();
void initialize();
void gameClear();
void setEnemy();
void pickup();
void moveEnemies();
void gameOver();

// マップサイズ
const int WIDTH = 30;
const int HEIGHT = 10;

// アイテムの数
const int ITEMNUM = 10;

// キー操作
const char UP = 'w';
const char LEFT = 'a';
const char DOWN = 's';
const char RIGHT = 'd';

// 面倒くさいから関数間データ共有はグローバル
char map[HEIGHT][WIDTH];
int playerX = 5;
int playerY = 1;
int collected = 0; // 拾ったアイテムの数
int gameover = 0; // ゲームオーバーになったかどうか

int main(int argc, const char *argv[]) {
    int key;

    // 初期化
    initialize();

    // メインループ
    while(gameover == 0) {
        // 画面の更新
        clear();
        showMap();

     // アイテムを全部拾ったらゲームクリア
        if(collected &gt;= ITEMNUM) {
            gameClear();
            break;
        }

        // メッセージ出力
        showMessage("  コマンド入力後に Enter \n  上:w  左:a  下:s 右:d");

        // プレイヤーの入力
        key = getKey();
        
        // データの更新
        switch(key) {
        case UP:
            moveTo(0, -1);
            break;
        case DOWN:
            moveTo(0, 1);
            break;
        case RIGHT:
            moveTo(1, 0);
            break;
        case LEFT:
            moveTo(-1, 0);
            break;
        default:
            // それ以外のキーが押されたら次の入力をまつ 
            continue;
        }

        // 敵を動かす
        moveEnemies();
    }
}

/**
 * ゲームの初期化
 * @function
 */
void initialize() {
    int i, j;

    // マップ初期化
    for(i = 0; i &lt; HEIGHT; i++) {
        for(j = 0; j &lt; WIDTH; j++) {
            map[i][j] = ' ';
        }
    }

    // 主人公の配置
    map[playerY][playerX] = '@';
    
    // アイテムの配置
    setItems();
}


/**
 * アイテムを拾う
 * ついでに敵が出現する
 * @function
 */
void pickup() {
    collected++;
    setEnemy();
}

/**
 * ゲームクリア
 * @function
 */
void gameClear() {
    showMessage("すべてのアイテムを集めた!\nゲームをクリアした!おめでとう!");
}

/**
 * 敵を1対配置する
 * @function
 */
void setEnemy() {
    int x, y;
    do {
        x = rand() % WIDTH;
        y = rand() % HEIGHT;
    } while(map[y][x] != ' ');
    map[y][x] = 'X';
}

/**
 * 敵を動かす
 * ランダム方向に移動しようとしてアイテムにt回阻まれたら動かない
 * @function
 */
void moveEnemies() {
    int t = 1;
    int i, j, k;
    int dx, dy;
    int count = 0;
    int enemyCount = 0;
    int nowEnemyNumber = 0;
    int duplicated;

    // 既に動いた敵の移動先座標リスト
    // 長さが ITEMNUM なのは敵がアイテム数以上出現しないため
    // 同じ敵が2回以上動くことを防止する
    int movedListX[ITEMNUM];
    int movedListY[ITEMNUM];
    
    // 敵を数える
    for(i = 0; i &lt; HEIGHT; i++) {
        for(j = 0; j &lt; WIDTH; j++) {
            if(map[i][j] == 'X') {
                enemyCount++;
            }
        }
    }

    // 既に動いた敵リストを初期化
    for(i = 0; i &lt; ITEMNUM; i++) {
        movedListX[i] = -1;
        movedListY[i] = -1;
    }

    // 敵を順番に動かす
    for(i = 0; i &lt; HEIGHT; i++) {
        for(j = 0; j &lt; WIDTH; j++) {
            if(map[i][j] == 'X') {
                
                // 既に動かした敵リストに登録されているものは動かさない
                duplicated = 0;
                for(k = 0; k &lt; nowEnemyNumber; k++) {
                    if(i == movedListY[k] &amp;&amp; j == movedListX[k]) {
                        duplicated = 1;
                        break;
                    }
                }
                if(duplicated) {
                    continue;
                }
                
                do {
                    dx = (rand() % 2) == 0 ? 1 : -1;
                    dy = (rand() % 2) == 0 ? 1 : -1;

                    if(i + dy &lt; 0 || HEIGHT &lt; i + dy || j + dx &lt; 0 || WIDTH &lt; j + dx) {
                        continue;
                    }

                    count++;
                } while(map[i + dy][j + dx] != ' ' &amp;&amp; map[i + dy][j + dx] != '@' &amp;&amp; count &lt; t);

                if(map[i + dy][j + dx] == '@') {
                    map[i][j] = ' ';
                    map[i + dy][j + dx] = 'X';
                    movedListX[nowEnemyNumber] = j + dx;
                    movedListY[nowEnemyNumber] = i + dy;
                    gameOver();
                } else if(map[i + dy][j + dx] == ' ') {
                    map[i][j] = ' ';
                    map[i + dy][j + dx] = 'X';
                    movedListX[nowEnemyNumber] = j + dx;
                    movedListY[nowEnemyNumber] = i + dy;
                } else {
                    // 動かない
                }

                // 動かした敵を数えておく
                nowEnemyNumber++;
            }
        }
    }
}

/**
 * 敵に触ってゲームオーバー
 * @function
 */
void gameOver() {
    clear();
    showMap();
    showMessage("敵に襲われてゲームオーバー!");
    gameover = 1;
}

/**
 * アイテムの配置
 * マップ内にランダムにアイテムを配置する
 * @function
 */ 
void setItems() {
    int x, y, i;
    for(i = 0; i &lt; ITEMNUM; i++) {
        x = rand() % WIDTH;
        y = rand() % HEIGHT;
        if(map[y][x] == ' ') {
            map[y][x] = '*';
        } else {
            i--;
            continue;
        }
    }
}

/**
 * プレイヤーを現在地から移動させる
 * @function
 * @param {int} x X方向の移動距離
 * @param {int} y Y方向の移動距離
 */
void moveTo(int x, int y) {
    // 外の世界に出ないようにする
    if(playerX + x &lt; 0 || WIDTH &lt; playerX + x || playerY + y &lt; 0 || HEIGHT &lt; playerY + y) {
        return;
    }
    
    // 移動前の主人公を消す
    map[playerY][playerX] = ' ';
    
    // 座標の更新
    playerX += x;
    playerY += y;
    
 // 移動先にアイテムがあれば拾う
    if(map[playerY][playerX] == '*') {
        pickup();
    }

    // 移動後の主人公を追加
    map[playerY][playerX] = '@';
}

/**
 * プレイヤーからのキー入力を受け付ける
 * @function
 * @returns {int} プレイヤーが押したキー
 */
int getKey() {
    printf("コマンド? =&gt; ");
    return getchar();
}

/**
 * 画面を消す
 * @function
 */
void clear() {
    int i;
    for(i = 0; i &lt; 30; i++) {
        putchar('\n');
    }
}

/**
 * 枠線を表示(メッセージ用)
 * @function
 */
void line() {
    int i;
    putchar('+');
    for(i = 0; i &lt; WIDTH - 2; i++) {
        putchar('-');
    }
    putchar('+');
    putchar('\n');    
}

/**
 * 画面下部にメッセージ表示
 * @function
 * @param {char[]} 表示するメッセージ
 */
void showMessage(char message[]) {
    line();
    puts(message);
    printf("  アイテム( %d / %d )\n", collected, ITEMNUM);
    line();
}

/**
 * マップの出力
 * @function
 */
void showMap() {
 int i, j;
    for(i = 0; i &lt; HEIGHT; i++) {
        for(j = 0; j &lt; WIDTH; j++) {
            putchar(map[i][j]);
        }
        putchar('\n');
    }
    map[playerY][playerX] = '@';
}

2013年6月18日火曜日

GAE + Go で Sign in with Twitter (4)

Authentication Header が作れたので、リクエストを送信する部分を作ります。
OAuth 1.0a の通信の流れを簡単に描くと以下の用になります。





まず、リクエストトークンを要求します。事前に Twitter へ登録しているアプリであればリクエストトークンがもらえます。登録されたアプリかどうかの判断は、Authorization ヘッダによって判断されます。登録されたアプリであると判断されたらリクエストトークンが帰ってきます。

次に、データのアクセスについてユーザの同意を得ます。Twitter のログインページヘリダイレクトして、Twitter へのログイン・ユーザデータへのアクセス許可をしてもらいます。リダイレクトする際にリクエストトークン(未認証)を渡すと、アクセス許可された後ににリクエストトークン(認証済)が帰ってきます。

次に、認証済のリクエストトークンとアクセストークンを交換します。アクセストークンとはユーザのデータへアクセスするために使うパスワードのようなものです。Twitter ではアクセストークンと一緒にユーザのID、ユーザ名が返ってくるため、Twitter アカウントを使ってログインするだけならここまでで OK です。

それ以降は、アクセストークンを使ってユーザのデータへアクセスすることができます。今回はアクセスしませんのでこちらはやらないです。


HTTP リクエスト送信処理

まず、HTTP リクエストを送ってレスポンスを得る汎用的なメソッドを書きます。
すべてのリクエストには Authorization Header が含まれるので、先日作った createHeader() メソッドを使います。
/**
 * リクエストを送信してレスポンスを受信する
 * メソッドは POST 固定
 * @method
 * @memberof OAuth
 * @param {string} targetUrl 送信先
 * @param {string} body リクエストボディ
 * @returns {string} レスポンス
 */
func (this *OAuth) request(targetUrl string, body string) string {

    // リクエストごとに変わるパラメータを設定
    this.params["oauth_nonce"] = this.createNonce()
    this.params["oauth_timestamp"] = strconv.Itoa(int(time.Now().Unix()))
    this.params["oauth_signature"] = this.createSignature(targetUrl)
 
    // Authorization Header を作成
    header := this.createHeader()
 
    // リクエストの作成
    var request *http.Request
    var err error
    if body == "" {
        request, err = http.NewRequest("POST", targetUrl, nil)
    } else {
        request, err = http.NewRequest("POST", targetUrl, NewReader(body))
    }
    check(this.context, err)
    request.Header.Add("Authorization", header)
 
    // リクエストの送信とレスポンスの受信
    client := urlfetch.Client(this.context)
    response, err := client.Do(request)
    check(this.context, err)
 
    // レスポンスボディの読み取り
    result := make([]byte, 256)
    response.Body.Read(result)
 
    return string(result)
}

エラーチェックのために OAuth クラスへ context を追加したり細かい修正をしていますが省きます。詳しくは Github のコードをご覧ください。

Go から HTTP リクエストを作成する場合、普通は net/http パッケージを使うのですが、GAE 上で動いている場合には GAE の appengine/urlfetch ライブラリを使うことになっています。

 URL Fetch Go API Overview - Google App Engine
 https://developers.google.com/appengine/docs/go/urlfetch/

また、リクエストボディは io.Reader インタフェースを実装している必要があるため、Read メソッドを実装します。
/**
 * リクエストボディ用のリーダー
 * request() で body を送信するために使う
 * @class
 * @member {[]byte} body 本文
 * @member {int} pointer 何バイト目まで読み込んだか表すポインタ
 */
type Reader struct {
 io.Reader
 body []byte
 pointer int
}

/**
 * Reader のインスタンスを作成する
 * @param {string} body 本文
 * @returns {*Reader} 作成したインスタンス
 */
func NewReader(body string) *Reader {
 reader := new(Reader)
 reader.body = []byte(body)
 reader.pointer = 0
 return reader
}


/**
 * 本文を読み出す
 * 2回目以降は前回の続きから読み出せる
 * @method
 * @memberof *Reader
 * @param {[]byte} p 読みだしたデータの保存先
 * @returns {int} 読みだしたバイト数
 * @returns {error} エラー
 */
func (this *Reader) Read(p []byte) (int, error) {
 var l int
 var err error
 if this.pointer + len(p) < len(this.body) {
  l = len(p)
  err = nil
 } else {
  l = len(this.body) - this.pointer
  err = io.EOF
 }
 
 for i := 0; i < l; i++ {
  p[i] = this.body[i + this.pointer]
 }
 
 this.pointer = l + this.pointer
 
 return l, err
}


このリクエスト送信メソッドを使って実際にリクエストを送る部分を書きます。


リクエストトークンの要求

まず最初に、リクエストトークンの要求です。
Twitter の場合、https://api.twitter.com/oauth/request_token にリクエストを送信します。
/**
 * Twitter へリクエストトークンを要求する
 * @method
 * @memberof OAuth
 * @returns {map[string]string} リクエスト結果
 */
func (this *OAuth) requestToken() map[string]string {
    response := this.request("https://api.twitter.com/oauth/request_token", "")
    datas := strings.Split(response, "&")
    result := make(map[string]string, len(datas))
    for i := 0; i < len(datas); i++ {
        data := strings.Split(datas[i], "=")
        result[data[0]] = data[1]
    }
 
    return result
}

レスポンスボディにリクエストトークンが入っています。
パラメータ名=値&パラメータ名=値
という感じになっているのでパラメータ毎に切り離して返します。


ユーザの許可を得る

次に、ユーザ認証画面へリダイレクトします。
Sign in with Twitter ボタンが押された時にリダイレクトするようにしましょう。
リダイレクト先は
https://api.twitter.com/oauth/authenticate?oauth_token=***
です。***にはリクエストトークンが入ります。

ユーザが Twitter へログインして、データへのアクセスを許可します。
許可が完了したら oauth_callback へリダイレクトされます。
このとき、パラメータとして認証済リクエストトークン(oauth_token) が渡されるので取り出します。

/**
 * OAuthで他のサイトでログインしてから戻ってきた時
 * @method
 * @memberof Controller
 * @param {http.ResponseWriter} w 応答先
 * @param {*http.Request} r リクエスト
 */
func (this *Controller) oauthCallback(w http.ResponseWriter, r *http.Request) {
    token := r.FormValue("oauth_token")
    verifier := r.FormValue("oauth_verifier")
 
    c := appengine.NewContext(r)
    oauth := NewOAuth(c)
    result := oauth.exchangeToken(token, verifier, "https://api.twitter.com/oauth/access_token")
    view := new(View)
    view.login(c, w)
    fmt.Fprintf(w, "あなたのidは %s です
あなたのユーザ名は %s です", result["user_id"], result["screen_name"])
 
    u, _ := user.CurrentOAuth(c, "")
    log.Printf("ユーザ: %#v", u)
}

この中で、認証済みリクエストトークンとアクセストークンを交換しています。
/**
 * リクエストトークンをアクセストークンに変換する
 * @memberof OAuth1
 * @method
 * @param {string} token リクエストトークン
 * @param {string} verifier 認証データ
 * @param {string} targetUrl リクエストの送信先
 * @returns {map[string]string} アクセストークンとユーザデータ
 */
func (this *OAuth1) exchangeToken(token string, verifier string, targetUrl string) map[string]string {
    this.params["oauth_token"] = token
    body := fmt.Sprintf("oauth_verifier=%s", verifier)
    response := this.request(targetUrl, body)
 
    datas := strings.Split(response, "&")
    result := make(map[string]string, len(datas))
    for i := 0; i < len(datas); i++ {
        data := strings.Split(datas[i], "=")
        result[data[0]] = data[1]
    }
    return result
}

アクセストークンと一緒に、ユーザ名(screen_name)、ユーザID(user_id)が帰ってくるので、これをログインデータとして使えばOKです。

2013年6月17日月曜日

GAE + Go で Sign in with Twitter (3)

Go + GAE で OAuth1.0a の Authorization Header を作成します。

ソースコードは GitHub にアップロードしてあります。
https://github.com/yokano/escape3ds_angularjs/blob/master/server/oauth1.go

まず、通信を行うための構造体 OAuth を作成します。
クラスっぽく扱うのでコメントではクラスと書いています。
送信するパラメータなどを保存しておけるようにしておきます。
/**
 * OAuthの通信を行うクラス
 * @class
 */
type OAuth struct {
    params map[string]string
}

インスタンス化する関数を作成します。
/**
 * OAuthクラスのインスタンス化
 * @function
 * @returns {*OAuth} OAuthインスタンス
 */
func NewOAuth() *OAuth {
    params := make(map[string]string, 7)
    params["oauth_callback"] = "http://localhost:8080/oauth_callback"
    params["oauth_consumer_key"] = config["consumer_key"]
    params["oauth_signature_method"] = "HMAC-SHA1"
    params["oauth_version"] = "1.0"
 
    oauth := new(OAuth)
    oauth.params = params
    return oauth
}

初期化する際に、固定できるパラメータを指定しています。

  • oauth_callback
  • oauth_consumer_key
  • oauth_signature_method
  • oauth_version

上の4つはリクエストによらず不変なので初期化時に指定しておきます。

  • oauth_nonce
  • oauth_timestamp
  • oauth_signature

この3つはリクエストごとに異なる値を取ります。
そのため、初期化時に指定せずリクエスト毎に作り直します。


まずは、oauth_nonce からです。
oauth_nonce はリクエストごとに異なるランダムな文字列を指定します。
この値は、クライアントから同じリクエストが過去に送られてきたかどうかをサーバが判断するための値です。
ここでは仕様書に載っている例と同様に生成してみます。
32byte のランダムなデータを Base64 エンコードして記号を削除した文字列を oauth_nonce とします。8byte で同じ事をする関数を別で作ったので、それを4回繰り返しています。
/**
 * oauth_nonce を作成する
 * @method
 * @memberof OAuth
 * @returns {string} 作成したoauth_nonce
 */
func (this *OAuth) createNonce() string {
    nonce := ""
    for i := 0; i < 4; i++ {
        nonce = strings.Join([]string{nonce, string(getRandomizedString())}, "")
    }
    return nonce
}
/**
 * ランダムな文字列を取得する
 * 64bit のランダムデータを Base64 エンコードして記号を抜いたもの
 * @function
 * @returns {string} ランダムな文字列
 */
func getRandomizedString() string {
    r := rand.Int63()
    b := make([]byte, binary.MaxVarintLen64)
    binary.PutVarint(b, int64(r))
    e := base64.StdEncoding.EncodeToString(b)
    e = strings.Replace(e, "+", "", -1)
    e = strings.Replace(e, "/", "", -1)
    e = strings.Replace(e, "=", "", -1)
    return e
}

次に oauth_timestamp です。
これはリクエストが作成された時の Unix タイムです。
現在時刻から離れていると Twitter からエラーが来るので注意が必要です。
oauth_nonce と oauth_timestamp はワンタイムパスワードのような役割を持ち、反射攻撃を防ぎます。
タイムスタンプをとるだけなのでコードは以下のように簡単になります。
this.params["oauth_timestamp"] = strconv.Itoa(int(time.Now().Unix()))

次に oauth_signature です。
oauth_signature は他のパラメータと比べると複雑でバグが出やすいです。
oauth_signature は送信するデータと秘密鍵 consumer_secret を使って oauth_signature_method 方式で暗号化した文字列です。
Twitter の場合は HMAC-SHA1 が使われています。
サーバ側でも同じように oauth_signature を計算して、クライアント側から送られてきた oauth_signature と一致するかどうか調べます。
これによって consumer_secret を知っている人(アプリ開発者)だけのアクセスが受け入れられます。

暗号化する前の文字列(base string)は以下のようになっています。

メソッド名&送信先URL&パラメータストリング

メソッド名は GET or POST。必ず大文字です。
送信先URL はリクエストの送信先の URL をパーセントエンコードしたものです。
パラメータストリングは oauth_signature 以外の oauth パラメータをまとめた文字列です。

パラメータストリングの作り方
  1. oauth パラメータをアルファベット順にならべる
  2. パラメータ名と値をパーセントエンコードする
  3. パラメータ名と値を = でくっつける
  4. さらにパラメータ同士を & でくっつける
oauth_signature の作成部分を Go で書くと以下のようになります。
/**
 * oauth_signature を作成する
 * @method
 * @memberof OAuth
 * @param {string} targetUrl リクエスト送信先のURL
 * @returns {string} oauth_signature
 */
func (this *OAuth) createSignature(targetUrl string) string {
    // アルファベット順に並び替える
    keys := make([]string, 0)
    for key := range this.params {
        keys = append(keys, key)
    }
    sort.Strings(keys)
 
    // パラメータストリングの作成
    params := make([]string, len(keys))
    for i := 0; i < len(keys); i++ {
        key := keys[i]
        val := this.params[key]
        params[i] = fmt.Sprintf("%s=%s", url.QueryEscape(key), url.QueryEscape(val))
    }
    paramString := strings.Join(params, "&")
 
    // ベース文字列の作成
    baseString := fmt.Sprintf("POST&%s&%s", url.QueryEscape(targetUrl), url.QueryEscape(paramString))
 
    // consumer secret をエンコード
    signatureKey := fmt.Sprintf("%s&", url.QueryEscape(config["consumer_secret"]))
 
    // HMAC-SHA1 で暗号化
    hash := hmac.New(sha1.New, []byte(signatureKey))
    hash.Write([]byte(baseString))
    signature := hash.Sum(nil)
    return base64.StdEncoding.EncodeToString(signature)
}


アルファベット順に並び替えるのは sort パッケージ
文字列の連結は strings パッケージ
URL エスケープは url パッケージ
HMAC-SHA1 は hmac パッケージと sha1 パッケージが使えます。

 sort package - Go lang

 url package - Go lang

 sha1 package - Go lang

 hmac package - Go lang


以上で 7 つの oauth パラメータが揃いました。
最後に Authorization Header を作ります。

  1. oauth パラメータ名と値をパーセントエンコードする
  2. 「パラメータ名="値"」 という感じでくっつける(ダブルクォート必須)
  3. すべてのパラメータを 「, 」で区切る(カンマの後のスペースはなくてもOK)
  4. 先頭に 「OAuth 」をくっつける

/**
 * ヘッダを作成する
 * @method
 * @memberof OAuth
 * @returns {string} ヘッダ
 */
func (this *OAuth) createHeader() string {
    params := make([]string, 0)
    for key, val := range this.params {
        key = url.QueryEscape(key)
        val = url.QueryEscape(val)
        set := fmt.Sprintf(`%s="%s"`, key, val)
        params = append(params, set)
    }
    header := strings.Join(params, ", ")
    header = fmt.Sprintf("OAuth %s", header)
    return header
}


Authorization Header が作れたら、ほぼできたも同然です。
今日はここまで。

2013年6月16日日曜日

GAE + Go で Sign in with Twitter (2)

昨日に続きまして GAE + Go で Twitter ログインです。

公式ドキュメントはこちら
 Sign in with Twitter
 https://dev.twitter.com/docs/auth/sign-twitter



今回の内容はこちらの Step.1 に該当します
 Implementing Sign in with Twitter
 https://dev.twitter.com/docs/auth/implementing-sign-twitter


Authorization Header

GAE のアプリから Twitter へ HTTP リクエストを送信することで認証を進めます。
Twitter はセキュリティ上、リクエストを送ってきたアプリが、事前に登録されたアプリであることを確認する必要があります。

登録されたアプリかどうかは、アプリ登録時にもらえる consumer keyconsumer secret を使って確認します。consumer key はアプリのID、consumer secret はアプリのパスワードのようなものです。

Twitter へ送信されるリクエストの1つを見てみましょう。
POST /oauth/request_token HTTP/1.1
User-Agent: themattharris' HTTP Client
Host: api.twitter.com
Accept: */*
Authorization: 
        OAuth oauth_callback="http%3A%2F%2Flocalhost%2Fsign-in-with-twitter%2F",
              oauth_consumer_key="cChZNFj6T5R0TigYB9yd1w",
              oauth_nonce="ea9ec8429b68d6b77cd5600adbbb0456",
              oauth_signature="F1Li3tvehgcraF8DMJ7OyxO4w9Y%3D",
              oauth_signature_method="HMAC-SHA1",
              oauth_timestamp="1318467427",
              oauth_version="1.0"


ここで重要になるのが Authorization ヘッダです。Tiwtter へ送るすべてのリクエストには Authorization ヘッダが含まれます。

この中の oauth_signature が登録したアプリ自身であることを証明する署名データです。上の例では oauth_signature="F1Li3tvehgcraF8DMJ7OyxO4w9Y%3D" の部分です。

また、oauth_signature 以外にも色々と送らなければいけません。何を送るかは OAuth 1.0a にて定義されています。


Authorization ヘッダに関する Twitter のドキュメントはこちら
 Authorizing a request - Twitter Developers
 https://dev.twitter.com/docs/auth/authorizing-request



Authorization ヘッダは基本的に 7 つのパラメータから構成されています。
それぞれ oauth_****** という名前が付いています。


1. oauth_callback

Twitter のログインが完了した後にリダイレクトされる URL を指定します。
とりあえずアプリの URL をそのまま使います。
開発が進んだら適切な URL へ変更してください。 

この画面から戻る先のURLです


2. oauth_consumer_key


Twitter にアプリを登録した際に1つだけ割り当てられる ID です。
登録した Twitter アプリケーションの詳細ページから確認できます。
アプリの ID だと思って大丈夫です。

ここの右側に表示されている文字列です

3. oauth_nonce

他のリクエストと識別するためのランダムな文字列です。
oauth_timestamp と合わせて過去に同じリクエストが送られたかどうかを判断するために使われます。
同じリクエストを1度しか受け付けないことで、リプレイ攻撃を防げます。
ランダムな文字列ならなんでも良いですが、今回は Twitter の公式ドキュメントと同じように、「32byte のランダムなデータを base64 エンコードした後、記号を削除した文字列」を使います。

4. oauth_signature

登録されたアプリであることを証明する署名データです。
oauth パラメータや HTTP メソッドなどを含む文字列を、Twitter とアプリ管理者だけが知っている秘密鍵 consumer_secret で暗号化することで、署名とデータの改竄検出が可能になります。
暗号化に使われるメソッドは Twitter の場合、HMAC-SHA1 が使われます。
oauth_signature の作成は他のデータと比べると複雑でバグが出やすいので要注意です。

5. oauth_signature_method

署名データを暗号化するアルゴリズムを指定します。
Twitter は HMAC-SHA1 で固定です。

6. oauth_timestamp

リクエストが作成された時刻を表す UNIX Timestampです。
oauth_nonce と合わせてリプレイ攻撃を防ぎます。
また、現在よりも離れている時間が設定されているとエラーが返されます。
コンピュータの時計が正しく設定されているかを確認することが大事です。
ただし、GAE の場合は時計のずれは気にしなくてよいでしょう。

7.  oauth_version

OAuth のバージョンです。Twitter では "1.0" で固定です。
ちなみに正確には OAuth 1.0 を修正した OAuth 1.0 Revision A(OAuth1.0a) が採用されています。

以上の 7 つの oauth パラメータを Authorization ヘッダに含めて送信します。
リクエストを受け取った Twitter は自分でも HMAC-SHA1 で oauth_signature の値を計算して、リクエストに含まれる oauth_signature と一致するか調べます。
一致したら、登録したアプリであることを認めてリクエストに答えます。


今日はここまで

2013年6月15日土曜日

GAE + Go で Sign in with Twitter (1)

Google App Engine + Go のアプリで "Sign in with Twitter" をやる方法です。
Twitter は OAuth1.0a を採用しています。
長いので数回に分けます。

Twitter の公式ドキュメントはこちら
    Sign in with Twitter - Twitter Developers
    https://dev.twitter.com/docs/auth/sign-twitter

OAuth 1.0 の仕様書はこちら
 OAuth 1.0 Revision A
 http://oauth.net/core/1.0a/


Twitter にアプリを登録する

Twitter でログインするためには事前にWebアプリを登録しなければなりません。

Twitter を開きます。
    https://twitter.com

画面左下の「開発者」リンクをクリックします。




画面右上の Sign in をクリックしてログインします。



画面右上のユーザアイコンから My applications メニューをクリックします。




ログインします。



登録したアプリ一覧が表示されます。
アプリを登録していない場合は何も表示されません。
Create a new application ボタンを押します。



アプリ名、アプリの説明、Web サイトの URL を入力します。
Web サイトの URL には GAE のアプリ URL をそのまま入力します。
Callback URL は後ほど改めて設定します。
利用規約を読み、キャプッチャを入力し、
Create your Twitter application ボタンをクリックします。




アプリの詳細画面が表示されます。
この画面に、通信で使用する暗号鍵など重要な情報が含まれています。



このアプリで Sign in with Twitter を使えるようにしましょう。
アプリの Setting タブを開き、Allow this application to be used to Sign in with Twitter にチェックを入れ、Update this Twitter application's settings ボタンをクリックします。





まずはここまで。続きは明日へ。

2013年6月14日金曜日

Magic Trackpad のボタンが埋まる

Magic Trackpad を使っているのですが、先日から調子が悪いです。
左下のゴムのスイッチ部分が埋まった(沈んだ)感じになり、
クリックしてもカチカチしてくれません。
少し力を入れて押せばボタンが押されるのですが、
押し心地がとても悪いです。

いつも親指で左下のボタンを押していたので、
負荷がかかっていたのかも。



確か保証期間内なので Apple Store へ持って行ってみようかと思っているのですが、
なぜか Tackpad だけ箱が見当たらず・・・汗。

実は Magic Mouse もおかしくて、クリックはできるもののマウスカーソルが動かない状態です。
こちらは保証が切れているので放置中。

インタフェースが不調だとちょっとテンション下がりますね。

2013年6月13日木曜日

Go で整数をバイト配列に変換する

Go で int64 などの整数を []byte へ変換する方法です。
encoding/binary パッケージを使います。
package main

import (
    "encoding/binary"
    "fmt"
)

func main() {
    src := int64(123456789123456789)
    result := make([]byte, binary.MaxVarintLen64)
    binary.PutVarint(result, src)
    fmt.Printf("Bytes: %X", result) // Bytes: AAFC82CDF5D2CDB60300
}

変換後の []byte のサイズが小さいとクラッシュしてしまいます。
binary パッケージに変換元のデータ型に合わせて []byte の長さが定義されています。
int64 を変換するなら binary.MaxVarintLen64 といった具合です。

変換では各バイトの先頭ビットを continuation bit として使用しているため、
int64 だからといって 8 バイトになる訳ではなく、決め打ちするとクラッシュすることがあります。

binary package source - Go Lang
http://golang.org/src/pkg/encoding/binary/varint.go?m=text

2013年6月12日水曜日

Go で Unix タイムの取得

Go言語では time パッケージを使って現在の Unix タイムを取得出来ます。
import (
    "time"
    "log"
)

log.Printf("UnixTime: %d", time.Now().Unix())
log.Printf("UnixTimeNano: %d", time.Now().UnixNano())

time.Now() で現在時刻を取得し、Unix()でUnixタイムに変換しています。
UnixNano() にするとナノ秒で表示できます。

2013年6月11日火曜日

JavaScript で Base64 エンコード

結局使わなかった。

Base64 - Wikipedia
http://ja.wikipedia.org/wiki/Base64

/**
 * 文字列をBase64エンコードする
 * @function
 * @param {String} str エンコード前の文字列
 * @returns {String} エンコードされた文字列
 */
encodeBase64: function(str) {

    // バイト配列に変換
    var bytes = [];
    var char = 0;
    for(var i =  0; i < str.length; i++) {
        char = str.charCodeAt(i);
        for(var j = 0; j < 8; j++) {
            bytes.push((char & 0x80) >>> 7);
            char = char << 1;
        }
    }

    // 6bit で割り切れるようにビットを追加する
    var surplus = bytes.length % 6;
    for(var i = 0; i < 6 - surplus; i++) {
        bytes.push(0);
    }

    // 6bit ごとにグループ化
    var groups = [];
    for(var i = 0; i < bytes.length / 6; i++) {
        groups[i] = parseInt(bytes.slice(i * 6, i * 6 + 6).join(''), 2);
    }
  
    // 変換する
    var result = "";
    var Base64Table = ['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L',
        'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z', 'a',
        'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p',
        'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z', '0', '1', '2', '3', '4',
        '5', '6', '7', '8', '9', '+', '/'];
    for(var i = 0; i < groups.length; i++) {
        result += Base64Table[groups[i]];
    }
 
    // 4 で割り切れる文字数へ = を追加して調整する
    var surplus = result.length % 4;
    for(var i = 0; i < surplus; i++) {
        result += '=';
    }
  
    return result;
}

2013年6月10日月曜日

GAE で複数のアプリを1つにまとめる

Google App Engine は無料で使える Web アプリのホスティングサービスです。
データベース(data store)やユーザ認証などのサービスが付いています。
サーバサイドの言語として Python, Java, Go, PHP が使えます。

Google App Engine - Google
https://developers.google.com/appengine/?hl=ja

ただし、無料アカウントの場合は 10 個までしかアプリを作成できません。
ちょっとしたミニゲームやツールを次々に公開していては、
すぐに 10 個を超えてしまいます。

そこで、ミニゲームやツールをまとめて 1 つのアプリ内に配置するのがお勧めです。
1 つのアプリ内に複数のディレクトリを作って、リクエストされたURLパターンから振り分けることができます。

ただし、GAE では 1アプリに付きデータベースが 1 つしか使えません。
単純なランキングなら1つのデータベースにまとめていれてしまっても問題ないのですが、複雑な操作をする場合は別のアプリとして独立させたほうが良いでしょう。

以下、実際に1つのアプリ内に複数アプリを共存させる方法です。


ディレクトリの配置

今回は okanoworld というアプリ内に複数のアプリを共存させてみます。
okanoworld 直下に複数のミニゲームディレクトリを置きます。
ミニゲームとは別に sever というディレクトリを作っておきます。

okanoworld の中に reversi3ds を配置



URLによる振り分け

app.yaml の header: ディレクティブでURLパターンとファイルパスを関連付けます。
http://okanoworld.appengine.com/アプリ名
でアプリのファイルが参照できるようにします。

app.yaml
application: okanoworld
version: 1
runtime: go
api_version: go1

handlers:

# リバーシ for 3ds
- url: reversi3ds
  static_files: reversi3ds/index.html
  upload: reversi3ds/index.html
- url: /reversi3ds
  static_dir: reversi3ds

# アプリその2
......

url に reversi3ds を指定することで http:okanoworld.appspot.com/reversi3ds に対する設定ができます。
まず、 reversi3ds をリクエストした時に index.html が表示されるように設定しています。
更に、画像ファイルなどのリソースへアクセスできるようにディレクトリ自体を static_dir に設定しています。


リソースの参照

アプリ内で画像などのリソースを読み込む場合は、
先ほど設定した URL パターンに合わせて以下のように書く必要があります。
<img src="/reversi3ds/hoge.png" />

これは <base> で相対パスの基準を変えることによって、簡単に出来ます。
<head>
    ...
    <base href="/reversi3ds/">
    ...
</head>
<body>
    ...
    <img src="hoge.png" />
    ...
</body>

GAE 以外からアプリを持ってきた場合、<base> を加えるだけでいいので楽ちんです。
以上で複数のミニゲームやツールを1つのアプリにまとめることができました。

2013年6月9日日曜日

3dsブラウザで blur が効かない

3ds のウェブブラウザでは、画面上に <button> や <input> や <a> などがあるとき十字キーでフォーカスを合わせることができます。

フォーカスを合わせると青いラインで囲われて、その状態でAボタンを押せばイベントが発生します。

十字キーでフォーカスが当たる

Bボタンを押すとフォーカスが解除されます。

この Bボタンの動作をJavaScriptから発生させる方法がわかりません。
他のブラウザなら blur イベントを使ってフォーカスを外すことができるのですが、
3ds では blur に反応してくれません。

フォーカスが当たっている状態で、CSS の display を none にして画面から消すと、
青い枠は消えてくれます。
しかし、display を block にして再表示すると、十字キーでフォーカスが合わなくなってしまいます。
この場合、画面上を1度タッチすることで、再びフォーカスを合わせることができるようになります。
内部で選択状態を保存していて、CSS を操作するだけではダメなようです。

ゲーム内のボタンやメニューを十字キーで操作させたいときは、
<div> タグと JavaScript で自分で作成するのが良いと思います。
リバーシ for 3DS では、メニューはすべて <div> にしています。

2013年6月8日土曜日

リバーシ for 3DS を公開

Nintendo 3DS のウェブブラウザで遊べるリバーシを公開しました。

公式サイト: http://yokano.github.io/reversi3ds/
アプリ本体: http://okanoworld.appspot.com/reversi3ds
(アプリは 3ds のウェブブラウザ専用です)

MITライセンスです。公式サイトからソースコードが落とせます。

3DSのWebブラウザ上で動きます

十字キーとAボタンで操作出来ます


ウェブアプリですが、十字キーとAボタンで遊べます。
もちろんタッチペンでも操作出来ます。
CPU と対戦することができます。1台で2人対戦することもできます。
2人対戦はタッチペンが2本あるとやりやすいかも。

十字キーの上下で画面がスクロールされないようにコンテンツを下画面にフィットさせています。(320x212px)
イベントのコールバック中に return false; しても、
preventDefault() してもイベントがキャンセルできないため、
コンテンツを2画面に広げてしまうと十字キーの上下でスクロールが発生してしまいます。
2画面で十字キーの上下を使えれば理想なのですが・・・。

Canvas は使わず DOM で動作しています。
アニメーションはスプライトを作成して、CSS の background-position を操作しています。
3ds は @keyframe にも対応しているのですが、step() を指定できずパラパラアニメができません。
結局、JavaScript から定期的に background-position を操作しています。
アニメはすべて CSS でやりたいのですが、良い方法が無いものか・・・。

2013年6月7日金曜日

3ds で animation-timing-function:steps() が動かない

3ds のウェブブラウザは CSS の keyframe-animation に対応しています。
animation-timing-functionlinearcubic-bezier() を指定すると動作するのですが、
steps() を指定しても動作しませんでした。

steps(1) を指定すると PC ではフレームの切り替えを 1 ステップでやってくれるため、
パラパラ漫画になるのですが、
3ds では ease を指定した時と同じように切り替えがなめらかになってしまい、
パラパラ漫画になりません。余計な切り替えアニメーションが表示されてしまいます。

そのため、キーフレームアニメーションを使ってパラパラアニメが作れません。
cubic-bezier() を使ってフレームの遷移速度を速くすることはできるものの、
フレームが切り替わる余計な部分も表示されてしまいます。

@-webkit-keyframe 内で background-image を変更してみても
PC だけしか反応してくれませんでした。

3ds で -webkit-animation-timing-function: steps(1) が指定出来れば楽なんですが・・・。
パラパラアニメは JavaScript のタイマーを使って css をいじるしかないのでしょうか。
うーむ・・・。


#animation {
 position: absolute;
 left: 100px;
 top: 100px;
 width: 25px;
 height: 25px;
 
 background-image: url('img/piece.png');
 background-position: 0px;
 -webkit-animation-name: play;
 -webkit-animation-duration: 1s;
 -webkit-animation-timing-function: steps(1);  /* 3dsで無効 */
 -webkit-animation-iteration-count: infinite;
}

@-webkit-keyframes play {
 0% { background-position: 0px; }
 17% { background-position: 25px; }
 34% { background-position: 50px; }
 51% { background-position: 75px; }
 68% { background-position: 100px; }
 85% { background-position: 125px; }
 100% { background-position: 150px; }
}

2013年6月6日木曜日

クリックイベントの取得

AngularJS でクリックイベントを取得する方法です。
ng-click でクリック時に処理を指定出来ます。

2013年6月5日水曜日

ページごとに異なるコントローラを設定する

AngularJS の $routeProvider でページを切り替えた際に、
各ページごとにコントローラを設定出来ます。

when() メソッドの第二引数に渡すオブジェクトの controller にコントローラ名を渡します。

page.js
angular.module('page', []).config(['$routeProvider', function($routeProvider) {
 $routeProvider.when('/page1', {templateUrl: 'page1.html', controller: Page1Controller});
 $routeProvider.when('/page2', {templateUrl: 'page2.html', controller: Page2Controller});
 $routeProvider.otherwise({redirectTo: '/page1'});
}]);

var Page1Controller = function($scope) {
 $scope.message = 'ここはページ1です';
}

var Page2Controller = function($scope) {
 $scope.message = 'ここはページ2です';
}

page1.html
<h1>ページ1</h1>
<p>{{message}}</p>
<a href="#/page2">ページ2へ移動する</a>

page2.html
<h1>ページ2</h1>
<p>{{message}}</p>
<a href="#/page1">ページ1へ移動する</a>

2013年6月4日火曜日

テンプレートによるページ切り替え

AngularJS では Ajax を使ってページの切り替えができます。
URL のフラグメント以下のパターンによって表示する内容を変えることができます。

http://example.com/#/page1  ->  ページ1を表示
http://example.com/#/page2  ->  ページ2を表示
それ以外  ->  ページ1を表示

という感じです。
実際には外部の HTML ファイルを Ajax で取得して、ページ内へ挿入しています。


index.html の作成

まず index.html を作成します。
テンプレートの HTML を表示したいタグに、ng-view を設定します。

index.html
<!DOCTYPE html>
<html>
 <head>
  <meta charset="utf-8">
  <title>ページ切り替えテスト</title>
 </head>
 <body ng-app>
  <div ng-view></div>
  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.6/angular.min.js"></script>
  <script src="page.js"></script>
 </body>
</html>

ngView - AngularJS
http://docs.angularjs.org/api/ng.directive:ngView


テンプレート用 HTML の作成

index.html 内の ng-view 内に挿入される HTML ファイルを作成します。
URL フラグメントを指定してリンクを張ることで別のページヘジャンプすることができます。

page1.html
<h1>ページ1</h1>
<a href="#/page2">ページ2へ移動する</a>

page2.html
<h1>ページ2</h1>
<a href="#/page1">ページ1へ移動する</a>


モジュールを作成

JavaScript でページ切り替えの設定をします。
設定はページの初期化時に行います。
AngularJS では初期設定をするために angular.module を使います。

module - AngularJS
http://docs.angularjs.org/guide/module

まず、任意の名前でモジュールを作成します。
今回は page という名前にします。
第二引数を省略しなかった場合、新たなモジュールが作られます。

page.js
angular.module('page', []);

作成したモジュールを ng-app と関連付けます。
<body> タグの ng-app を ng-app="page" に変えます。
これで page モジュールを使って初期化が行われるようになります。

index.html
<!DOCTYPE html>
<html>
 <head>
  <meta charset="utf-8">
  <title>ページングテスト</title>
 </head>
 <body ng-app="page">
  <div ng-view></div>
  <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.0.6/angular.min.js"></script>
  <script src="page.js"></script>
 </body>
</html>


$routeProvider の設定

URLフラグメントによるページ切り替えは $routeProvider によって行います。

$routeProvider - AngularJS
http://docs.angularjs.org/api/ng.$routeProvider

モジュール内で $routeProvider の初期設定をすることを通知します。
設定はモジュールの config() メソッドで行います。

page.js
angular.module('page', []).config(['$routeProvider', function($routeProvider) {
    // ここに設定処理を書く
}]);


URL パターンと、表示する HTML ファイルを設定します。

page.js
angular.module('page', []).config(['$routeProvider', function($routeProvider) {
    $routeProvider.when('/page1', {templateUrl: 'page1.html'});
    $routeProvider.when('/page2', {templateUrl: 'page2.html'});
    $routeProvider.otherwise({redirectTo: '/page1'});
}]);


これでページの切り替えができるようになります。
指定したパターンが URL フラグメントの後ろに現れた場合、
該当するテンプレート HTML が ng-view の中に表示されます。



ソースコードはこちらからダウンロード出来ます(ファイル->ダウンロード)
https://docs.google.com/file/d/0B0qhX3iM3_v0LWtqM2o4clBmOEE/edit?usp=sharing


2013年6月3日月曜日

セレクトボックスで並び替え

オブジェクトのリストを、要素の値によって並び替える方法です。
セレクトボックスで、どの要素で並び替えるのかをリアルタイムに変えられるようにしてみます。




Controller と Model を作る

並び替える条件として name と sex を準備します
var Controller = function($scope) {
    $scope.persons = [
        {name: 'tanaka', sex: 'man'},
        {name: 'sato', sex: 'woman'},
        {name: 'suzuki', sex: 'woman'},
        {name: 'okano', sex: 'man'},
        {name: 'kumagai', sex: 'man'}
    ];
}


View を作る

何もしないセレクトボックスを作成します
更にデータの一覧表示をします
<div ng-app ng-controller="Controller">
    <select>
        <option value="name">name</option>
        <option value="sex">sex</option>
    </select>
    <ul>
        <li ng-repeat="person in persons">
            {{person.name}} : {{person.sex}}
        </li>
    </ul>
</div>


<select> の値を取得する

ng-model を設定することで、選択した <option> の value を取得出来ます。
ここでは sort という名前のモデルに name または sex が入るようにします。
<div ng-app ng-controller="Controller">
    <select ng-model="sort">
        <option value="name">name</option>
        <option value="sex">sex</option>
    </select>
    <ul>
        <li ng-repeat="person in persons">
            {{person.name}} : {{person.sex}}
        </li>
    </ul>
</div>

<select> についてはこちらを参照

select - AngularJS
http://docs.angularjs.org/api/ng.directive:select


orderBy フィルタで並び替える

最後に <select> で取得した要素でリストを並び替えます。
orderBy フィルタに要素名を渡すと、その要素によって並び替えが行われます。
<div ng-app ng-controller="Controller">
    <select ng-model="sort">
        <option value="name">name</option>
        <option value="sex">sex</option>
    </select>
    <ul>
        <li ng-repeat="person in persons | orderBy:sort">
            {{person.name}} : {{person.sex}}
        </li>
    </ul>
</div>

orderBy - AngularJS
http://docs.angularjs.org/api/ng.filter:orderBy


2013年6月2日日曜日

検索フォームを作る

リスト内の文字列から検索条件に一致する文字列だけを表示する機能です。
Result タブをクリックして検索してみてください。




データの作成

Controller と Model を作成します。
var Controller = function($scope) {
    $scope.names = ['tanaka', 'sato', 'suzuki', 'nakamura', 'okano', 'yamada', 'ida'];
}


ビューの作成

まずはデータを一覧表示するだけのビューを作成します。
検索ボックスも作ります。
<div ng-app>
    <div ng-controller="Controller">
        検索:<input type="text"></input>
        <ul>
            <li ng-repeat="name in names">
                {{name}}
            </li>
        </ul>
    </div>
</div>


検索文字列の取得

検索ボックスに入力された文字列を取得します。
ng-model ディレクティブを使ってフォームに入力されたデータを取得出来ます。
入力された文字列を condition という名前で保存します。

ngModel - AngularJS
http://docs.angularjs.org/api/ng.directive:ngModel

<div ng-app>
    <div ng-controller="Controller">
        検索:<input type="text" ng-model="condition"></input>
        <ul>
            <li ng-repeat="name in names">
                {{name}}
            </li>
        </ul>
    </div>
</div>

フィルターを設定

検索文字列をフィルターとして ng-repeat に設定します。
ng-repeat 部分に | filter:condition を追加すると、文字列に condition が含まれるものだけがループへ入るようになります。

filter - AngularJS
http://docs.angularjs.org/api/ng.filter:filter

<div ng-app>
    <div ng-controller="Controller">
        検索:<input type="text" ng-model="condition"></input>
        <ul>
            <li ng-repeat="name in names | filter:condition">
                {{name}}
            </li>
        </ul>
    </div>
</div>

これで検索条件に引っかかる要素のみが表示されるようになります。

2013年6月1日土曜日

ModelのリストをViewで表示

View から Model のリストを順次読みだしながら表示する方法です。


View を作成

まず HTML で View を作成します。
コントローラを指定しておきます。
<div ng-app>
    <div ng-controller="Controller">
        <p>リストの中身を表示</p>
        <ul>
            <li>ここに配列の中身を表示したい</li>
            <li>ここに配列の中身を表示したい</li>
            <li>ここに配列の中身を表示したい</li>
        </ul>
    </div>
</div>


Controller と Model を作成

View で指定した名前の Controller を作成します。
Model として $scope に配列を追加します。
$scope に names を追加したことで View からも names へアクセスできるようになります。
var Controller = function($scope) {
    $scope.names = [
        'tanaka', 'suzuki', 'sato'
    ];
};


View でリストの表示方法を指定する

HTML の li 要素に ng-repeat を追加して配列内の要素の表示方法を指定します。
以下の例では Model の names から要素を1つずつ取り出して name へセットしています。
names の要素数だけ繰り返し li 要素を作成し、{{name}} が解釈されて文字列へ置き換わります。
<div ng-app>
    <div ng-controller="Controller">
        <p>リストの中身を表示</p>
        <ul>
            <li ng-repeat="name in names">
                {{name}}
            </li>
        </ul>
    </div>
</div>

これで以下の様に出力されます。
・tanaka
・sato
・suzuki


配列内のオブジェクトを読み出す

配列の中身がオブジェクトだった場合は、いつもどおり . をつけて要素を参照できます。
また、ng-repeat で要素を取り出している時に {{$index}} と書くことで現在の要素番号を表示できます。
ng-repeat について詳しい情報はこちらを参照してください。

ngRepeat - AngularJS
http://docs.angularjs.org/api/ng.directive:ngRepeat


以下は $index による番号表示と、配列内のオブジェクトの要素へのアクセス例です。