黒い画面を屠る会

letsspeakの技術ブログです。

コードを書き直すということ

まず初めにこれはリファクタリングの話ではありません。既に動いているコードがある場合の書き直し=リファクタリングについてはすべてをひっくるめたコストパフォーマンスを重視するのは当然の話です。この記事を書き始める前に少しググってみたらすぐに色々と記事が出てきたので、そちらを参考にしてもらえばと思います。

この記事での「コードを書き直す」場面というのは初期開発(設計〜土台構築+αくらいまで)の話で、このフェーズの設計をミスると後々かなり大きな負債になるので、見積もりの範囲内であればガンガンコードを書き直すべきじゃないかという話です。

拙いコードの共通点

これまでの仕事を通して色んなプロダクトのコードを見てきた中で、拙いコードにはいくつか共通点があります。筆頭はもちろん「命名を軽んじている」(正しく表現していない、英単語の並び順が不適切、同じ意味を表す単語の表記揺れ)で、これについてはわたしも十家言くらいはあり(そもそもプログラミング=定義なんだから名前を正しくつけないとまともなコードになるわけないじゃんとか...)、一度きちんとした記事を書きたい気持ちもありつつ、リーダブルコードを筆頭にQiitaなどでも良い記事が色々あるのでそちらを見てくれという感じなのですが、その次が「コードを書き直すことのハードルを上げすぎている」なんじゃないかと強くおもいます。

コードを書き直すということ

もちろん設計から見直してコードを書き直しテストまで実行するとなるとそれなりの時間がかかるので、拙いからこそ、そんな時間を捻出できずにそうなってしまっているのかもしれないです。

しかし個人的な経験則ではありますが、初期段階に自身で設計して書いたコードはほぼ脳内にイデアが出来上がっているので、書き直しには思ったより時間がかからないですし、書き直すことで未来の負債を減らすための大きな一手になります。そして設計・コード品質についてのイテレーションが1周することは、書き手にとっても大きな成長の機会になります。

良いコードを書くということ

結局のところ良いコードを書くということは、世の中にどのような悪いコードがあるのかを知り、なぜ悪いのかを知り、どうすればよくなるのかを知り、あるいは想像し、実際に手を動かした「分量」がものをいいます。(もちろんここに頭の回転が早いとか、想像力が優れているとか、3を知って10を学べるとかの素質による係数はかかるとは思いますがそんなことを議論しても仕方がない。)

この「分量」を稼ぐことこそが良いコード、プロダクトの品質向上にも繋がるので、良いコードが書けなくて困っている人は、まずはタイピングの練習から始めましょう!(丸投げ)

ブートローダーとカーネルローダー

ポトリスというゲームで「継続ハ力ナリ」という名前のプレイヤーがいて、仲間内では「継続はかなり」さんと呼んでいました。いつも丸太をやっていた消防士(当時50)のごんべえさん、元気でしょうか?ワシャシャシャシャシャ!

ぐだぐだOSプロジェクト

さて、VirtualBoxでOS動いたら楽しいじゃん?から始まったプロジェクトを始めて半月くらいが経ちました。

letsspeak/myos · GitHub

金曜日は色々とアルコール摂取が必要なので仕方ないとしても、なんとか生活習慣として根付かせようと頑張った結果、今日やっとカーネルに突入しました。

f:id:letsspeak:20151021233049p:plain

この画像にあるBheckkingのBの部分がカーネルが動いている証拠で、今のカーネルの仕事はそこで終了です。

生活習慣の中にきちんと趣味を組み込むとやっぱり人に話したくなるもので、カーネルローダーの話とかみなさんまったく興味はないと思いますし、聞き流すのが普通だとおもいますが、それなりに濃い話をできるというのはとても楽しいものだなと実感しています。

ふつうのプログラムの開発でにっちもさっちも行かず前に進めない状態というのは中々ないのですが、今回は本当に日進月歩でした。
つまらないメモですがこれまでのなんとなくの履歴を残しておきます。

ブートローダ

まずブートローダー。VirtualBoxでOSを起動しようとすると、まずBIOSが動きます。
BIOSフロッピーディスクドライブのブートセクタ512バイトを自動的に読み込んで中に書いてあるプログラムを実行してくます。

512バイトというと、アセンブラの命令で結構たくさんのプログラムが組み込めるように見えますが、なにせ初心者なのでできるだけ画面に文字を表示させて安心したいものです。

たとえば、現在の状況がわかるように"Loading Kernel Loader..."という22文字を表示させる場合、文字列を512バイトの中に組み込むので残りは490バイトという感じで減っていきます。手順が5段階で各手順の状況を出力すると512バイトのおよそ2割が文字列の情報だけで埋まってしまうのです。

また、単に文字列を表示させるだけでは正直中の状態がよくわからない!ということでAXレジスターの値を16進数で表示させるプログラムを組んで状態を見ながら開発を進めていましたが、後半になるにつれてどんどん残りのサイズが減っていきました。
最後は本当は必要な処理を一旦コメントアウトして、デバッグ用の16進数表示のプログラムを中に入れたりしながら、ブートローダーが完成しました。

現代ではこんな経験はたぶんここでしか有りえないと思いますし、作ったところで特に意味はないですが、1週間くらいでできる中々に楽しい経験なので、プログラマーには割とおすすめです。

カーネルローダー

ブートローダーの16bitの状態ではセグメントレジスタ(指定値を16倍にしてくれる)を利用しても0xFFFF0+0xFFFF=0x10FFEFつまり111kB分のメモリアドレスまでしかアクセスできないので、カーネルローダーの中で32bitに移行してカーネルイメージの読み込みを行います。

これまで利用していたBIOSの機能は16bit用のプログラムなので、32bitに移行してしまうとこれらが一気に使えなくなります。これがかなりの鬼門でしたが、実はVRAMの領域に直接アクセスして値を書き換えてあげると画面に文字を表示することができるので、それを利用して32bit用の独自の画面表示の仕組み(文字送りなど)を512バイトの制限に囚われることなく書くことができます。

A20やGDTの指定、意図しないEDXレジスタへの副作用(おそらく)など、鬼門となる箇所が非常に多く「なぜ動かないのかが分からない」という事態に何度もぶち当たりましたが、毎日継続しながら小さな可能性をひとつひとつ潰していくことでひとまずは何とかなりました。

カーネル

よーしカーネルローダーが終わった!あとはカーネルを動かすだけだ!
と思ってから1週間ほどカーネルローダーを直し、カーネル側のコンパイルにも数日かかりました。

自作のプログラムを動かす都合上バイナリの先頭からプログラムが始まっていなければならず、リンカスクリプトをうまく読み込ませてコンパイルさせることが必要になります。このあたりgccなど慣れている人なら簡単に終わるのかもしれません。

まとめ

以上、そんな感じでひとまずC言語に到達できました。
今後はC言語で基本的な仕組みを整えつつ、ネットワークまわりから1MB以上のデータを貰ってくる必要がありそうです。

プログラマーとしてアセンブラとかいう非生産的な言語には一種の憧れがありましたが、実際に使ってみると何の役にもたたないただのクソ言語だったと感じます。
おそらくアセンブラ機械語を書いていた大昔のプログラマーも「こんなん絶対別の記述したほうがいいだろ」とか頭の中に現代を描きながら悶々とプログラムを書いていたに違いないと思います。

なんだかよく分からない纏めになりましたが、
継続すればなんとかなるというところは大事にしたいなと思います。

P.S. valley of the cityで韓日ギルドのダブルを盗んだ時の快感を今でも思い出します。

Ruby on RailsのMySQLバックアップタスクを書いてみた

以前Fuelで書いたバックアップタスクがあるのですが、あれの安心感が結構あったのと、いま作っているものが Ruby on Rails だったので、さっくりrake版を書いてみました。
大したものではありませんが、なんとなーくConfigの触り方が分かりました。

rubyyaml読むの楽ですね!
database.ymlに追加でbackupdirを設定できるようにしてみました。
コマンドラインと自動実行、apacheから呼ばれた際のユーザーの違いによる権限エラーにご注意ください。

mysql.rake
https://gist.github.com/letsspeak/7988055
Ruby on Rails mysql backup rake task

javascriptのcanvasで画像を自由にトリミングしてアップロードする機能を作ってみた

今回はjavascriptで画像をアップロードする際にトリミングするコードをcanvasを使って書いてみました。


趣味でゲームのスクリーンショットをアップロードしてみんなで見せ合うようなWebアプリケーションを作っていたのですが、元の画像サイズやファイル形式って人によってまちまちだったりします。


ゲームということもあり、デジカメのような大きすぎる画像サイズになることはないのですが、どうしてもプライベートなチャット内容が入ってしまうのは嫌です。できれば自分の見せたい範囲だけをアップロードしたいものです。


また無圧縮のpngbmpなんかでアップロードされた場合、数MBの画像をやりとりする分けにはいきません。そこですこし劣化したjpeg等の圧縮された画像形式に変換することになりますが、これをサーバー側で行うと、プレビュー画像をユーザーに見せるタイミングがサーバーで処理を行った後になってしまい、ユーザーの時間とサーバーリソースが無駄に消費されてしまいます。


そんな訳ですべてjsのcanvasでやってしまおう!という感じで実験しつつ作ってみました!

trimming_upload.js
https://github.com/letsspeak/trimming_upload.js


行っていることは、

1.ファイルが選択されたら画像を読み込む
2.この際、画面表示用のlayer1には設定された最大サイズで比率を維持して読み込む
3.また裏のbaseに元画像のサイズのまま読み込んでおく
4.画面表示部分がクリックされた際にlayer2にトリミング用の矩形を描画する
5.画面には常にlayer1とlayer2を合成して表示する
6.トリミング確定後、アップロードボタンが押されたら元サイズのbaseボタンからトリミングサイズに基づいた比率で切り抜いてデータを送信する。

という感じです。

f:id:letsspeak:20131122020607p:plain


こんな感じで設定した最大さいずでプレビューしつつ、トリミングは元画像の比率で行うようになっています。

今回のハマりポイント
context.drawImage() の際の第一引数になるcanvasサイズが width, height ともに 0 の場合に例外が発生するようです。これに気づかず20分程はまってしまいました...。


書いていて気がつきましたが、元のトリミングサイズのままアップロードしたい場合の処理がありませんねwこの辺を次の課題としつつ今回はこの辺で終わりとさせていただきます。

ではよいお年を。

ec2にhaskell-platformをインストールする

噂のyesodさんを試してみたいと思いつつ、手元のmacで失敗した経緯をもとにインスタンスをすぐ消したりできるec2を使って試してみました。

インスタンスの立ち上げ方については詳しいページがあると思うので割愛!
最初はさくらVPSで使っているCentOSと同じRed Hat Enterprise Linux Server release 6.4 (Santiago) を選択しましたが、haskell-platformのmakeで失敗しました。
はっきりいって初心者の自分には何が理由で失敗したのかさっぱり分らなかったので、気を取り直してAMIでインスタンスを建て直し、試しにRed Hat と同じコマンドを打ってみたところhaskell-platformのmakeが順調に進み....謎の失敗。

ググってみると下記のブログがヒット!

AWSのアカウントを作ってみた
http://amkkun.hatenablog.com/entry/2013/02/06/183901

どうやらマイクロインスタンスではメモリが足りないという事で、一時的にスモールインスタンスに変更してmakeを実行します。いつもいつも先駆者の方々に感謝です。

macであれほどうまくいかなかったcabal-installについても、一度は失敗したものの先述のmake時と同様にスモールインスタンスで実行したところ無事成功しました。
一通りの手順は下記に纏めておきました。

set up yesod on ec2 Amazon Linux AMI 2013.03.1 64bit
https://gist.github.com/letsspeak/5650158

haskellぐぬぬって感じは未だに拭えないです。
無事yesod環境構築できた暁には、AMIのスナップショットを保存しておきたいなー

CentOSにGrowthForecastをいれて色々表示してみた

最近CentOSでFuel先生とごにょごにょしているのですが、データがあまりにも大量なため基本的には静的コンテンツをタスク生成してリクエスト発生時はSELECTかmemcachedからフェッチするだけーみたいな感じで動かしていました。

そうなるとfuel/app/tasksにタスクが増える増える。crontab先生大活躍な訳なんですが、はたから見てるとちゃんと動いているのかどうかまったく分からないので、統計をとってみようとfluentdとか調べ始めたところでGrowthForecastというグラフ生成Webツールを発見して、さっそく入れてみました!

ついでに、先日の作業中0時頃にとつぜんフリーズでBrokenPipeした件も調べたいと思い、CPU使用率、メモリ使用率も集計してみることにしました。

GrowthForecastのインストール

ここが一番大変でした。
最初はこちらのgist installing_growth_forecast の通り進めたのですが、root権限でインストールしてこれで良かったのか..。と思っていた所に良い記事を発見したので下記記事を参考に進めました。

GrowthForecast を CentOS 6.3 にインストールして Supervisor で管理してみた

主にcpanmとGrowthForecastのインストールに結構時間がかかりました。

GrowthForecastのアクセス制限

動作確認後、新しいホスト名を追加してnginxでBASIC認証かけてポートを閉じます。

nginx.conf
  upstream growthforecast {
    server 127.0.0.1:5125;
  }

    server {
    listen 80;
    server_name growthforecast.yourdomain.com;

    proxy_connect_timeout 60;
    proxy_read_timeout    60;
    proxy_send_timeout    60;


    auth_basic "Secret Area";
    auth_basic_user_file "/home/growthforecast/.htpasswd";

    location / {
      proxy_set_header X-Real-IP $remote_addr;
      proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
      proxy_set_header Host $http_host;
      proxy_pass http://growthforecast;
      proxy_redirect off;
    }
  }
.htpasswdの作成
htpasswd -c /home/growthforecast/.htpasswd username

CPU使用率をロギングしてみる

GrowthForecastはintegral (整数) でなければ受け付けてくれないので、小数点以下を切り捨てた値を取得するワンライナーを書いて、crontabで1分毎に報告させます。

CPU使用率(パーセント)を整数で表示する
# cpu usage (integral)
cat /proc/loadavg | cut -d ' ' -f 1 | cut -d '.' -f 1
CPU使用率をGrowthForecastに報告するcrontab
# send your cpu usage (integral) to GrowthForecast every 5 minutes via crontab
*/1 * * * * curl -F number=`cat /proc/loadavg | cut -d ' ' -f 1 | cut -d '.' -f 1` http://localhost:5125/api/yourmachine/system/cpu 2>&1

gistにも纏めておきました。
centos_cpu_usage_one_liner.sh

メモリ使用率をロギングしてみる

ワンライナーで書けさえすればGrowthForecastに報告できる!と知ってawkを調べながら書いてみました。実際の使用率については、freeコマンドの見方を参考に計算しています。

メモリ使用率(パーセント)を整数で表示する
// actual memory usage (integral)
// http://open-groove.net/linux-command/free/
free | awk 'NR==2' | awk '{print int(($3-($6+$7))/$2*100)}'

こちらもCPU使用率と同じようにnumber=``でくくってcrontabに登録すれば完了です。
メモリ使用量についてもgistに纏めておいてます。

centos_memory_usage_one_liner.sh

グラフ化結果

f:id:letsspeak:20130330222650p:plain

データを取ってみると、CPU使用率が全然低いのとメモリ使用量がおよそ70%と高めなのが気になります。
もしかすると取得方法が間違っているのかもしれないので、これから他のVPSや新しい環境構築時にログをとって比べてみたいと思います。
もし間違いなどお気づきになりましたら、ご指摘頂けると幸いです。

FuelPHPのタスクと連携

FuelPHPのタスクと連携するためにGrowthForecastにPOSTするための簡単なモデルを作成しました。

fuel/app/classes/model/growthforecast.php
<?php

Class Model_GrowthForecast extends Model
{
  static public function post($section_name = null, $graph_name = null, $number = null, $mode = 'gauge')
  {
    // validation
    if (is_null($section_name)) return;
    if (is_null($graph_name)) return;

    if (is_int($number) === false)
    {   
      if (is_numeric($number) === true)
      {   
        $number = (int)$number;
      }   
      else
      {   
        return;
      }   
    }   

    $url = 'http://localhost:5125/api/yourapplication/'.$section_name.'/'.$graph_name.PHP_EOL;
    $data = array(
      'number' => (string)$number,
      'mode' => $mode,
    );  

    $headers = array(
      "Content-Type: application/x-www-form-urlencoded",
      "Content-Length: ".strlen(http_build_query($data)),
    );  

    $options = array('http' => array(
      'method' => 'POST',
      'content' => http_build_query($data),
      'header' => implode("\r\n", $headers),
    )); 

    $contents = file_get_contents($url, false, stream_context_create($options));
  }
}

これを、こんな感じでTaskから呼び出すだけでどんどんグラフ化してくれます。

    \Model_GrowthForecast::post('crawler', 'source_count', count($sources));

f:id:letsspeak:20130330223350p:plain

やっぱり視覚化されるのは純粋に楽しい!わくわくします。

問題点など

今回ポートを閉じてしまいましたが、特定のサーバーから報告をPOSTする場合、IPアドレス指定のオプションをつけて起動して、ポート自体は開けておいたほうが良さそうです。

また今回作成したモデルではyourapplicationの部分を固定化してしまっていますが、開発環境、テスト環境、プロダクション環境に応じて値が変わるようにmodifyする必要がありそうです!

FuelPHPでopauthを使って色んなログインに対応してみた

FuelPHPでopauthを使って色んなログインに対応してみました。
ログインのパターンは

1.通常のusernameとpasswordのログイン
2.Twitterのoauthログイン
3.Facebookのoauthログイン

です。
TwitterFacebookのログインについてはfuel-opauthを使っていますが、こちらの参考ブログ記事にも記載されている通りnamespaceの設定に不具合があるためgithubでforkしたものをsubmodule化しています。

本家 https://github.com/andreoav/fuel-opauth本家
修正版 https://github.com/letsspeak/fuel-opauth

今回作成したり変更したファイルは一通りgistにもアップロードしています。
不具合はいつも通り触りながら修正していこうかと思っていますが、問題点などありましたらお気軽にご連絡ください。

gist
https://gist.github.com/letsspeak/5229245

Twitter/Facebookのアプリケーション登録

参考ブログの通り進める。

fuel-opauthのsubmodule化

git submodule add https://github.com/letsspeak/fuel-opauth.git fuel/app/packages/opauth/
TIPS

githubからhttps://でsubmodule化した場合、内容の変更が発生したときのpush時に下記修正が必要なので注意。

fuel/packages/opauth/.git/config
- url = https://github.com/letsspeak/fuel-opauth
+ url = ssh://git@github.com/letsspeak/fuel-opauth.git

opauth.phpの設定

ソルトとTwitter/Facebookの開発者用のキーを登録する。

fuel/app/config/opauth.php
<?php
namespace Opauth;
return array(
    'path' => '/auth/login/',
    'callback_url'  => '/auth/callback/',
    'security_salt' => 'ランダムな文字列を生成して登録する',
    'Strategy' => array(
      'Facebook' => array(
        'app_id' => 'Facebookで取得したアプリID / APIキー',
        'app_secret' => 'Facebookで取得したアプリのシークレットキー'
      ),

      'Twitter' => array(
        'key' => 'Consumer key',
        'secret' => 'Consumer secret'
      ),  
    ),     
);

config.php

fuel/app/config/config.php
  'always_load' = array(
    'packages' => array(
      'opauth',
    ),
  ),

User/TwitterUser/FacebookUserを作成

oil g model user name:string password:string nickname:string email:string last_login:int
oil g model twitteruser uid:string token:string secret:string user_id:int
oil g model facebookuser uid:string token:string expires:int user_id:int
マイグレーションの変更点

ストレージエンジンは全てInnoDBに変更。
Userで一元管理することを想定してUser->last_login以外はすべて'null'=>trueに設定。
モデルクラス名をModel_TwitterUserに変更したいのでテーブル名をtwitter_usersに変更。
モデルクラス名をModel_FacebookUserに変更したいのでテーブル名をfacebook_usersに変更。

モデルの変更点

モデルクラス名をそれぞれModel_TwitterUser、Model_FacebookUserに変更。
Model_User、Model_TwitterUser、Model_FacebookUserのバリデーション追加。

authコントローラーを作成

通常ログイン時は引数無しで /auth/login/にPOSTする。
Twitterログイン時は/auth/login/twitter/を叩く。
Facebookログイン時は/auth/login/facebook/を叩く。

fuel/app/classes/controller/auth.php
<?php
class Controller_Auth extends Controller
{
    private $_config = null;
    private $_salt_length = null;
    private $_iteration_count = null;

    public function before()
    {
      if(!isset($this->_config))
      {
          $this->_config = Config::load('opauth', 'opauth');
      }

      $this->_salt_length = 32;
      $this->_iteration_count = 10;
    }

    // auth/login/*
    public function action_login($_provider = null, $method = null)
    {
      // 引数無し時は通常ログイン
      if (is_null($_provider)) return $this->normal_login();

      // http://domainname/auth/login/twitter/oauth_callback?denied=signature
      if ($method === 'oauth_callback') {
        if (Input::get('denied')){
          return $this->login_failed();
        }
      }

      if(array_key_exists(Inflector::humanize($_provider), Arr::get($this->_config, 'Strategy')))
      {
        $_oauth = new Opauth($this->_config, true);
      }
      else
      {
        return $this->login_failed();
      }
    }

    // 通常ログイン
    public function normal_login()
    {
      $username = Input::post('username');
      $password = Input::post('password');

     // ユーザー名とパスワード空欄時はログインフォームを表示する
      if (is_null($username) and is_null($password)) {
        return Response::forge(View::forge('auth/form'));
      }
      
      // 認証
      $query = Model_User::query()->where('name', $username);
      if ($query->count() === 0){
        // 認証エラー
        $this->login_failed();
      }

      // パスワードのハッシュ化
      $user = $query->get_one();
      $salt = substr($user->password, 0, $this->_salt_length);
      $enc_password = $salt.$password;
      for ($i = 0; $i < $this->_iteration_count; $i++)
      {
        $enc_password = sha1($enc_password);
      }
      
      if ($user->password === $salt.$enc_password){
        // 認証成功
        return $this->login_succeeded($user->id);
      }else{
        // 認証エラー
        $this->login_failed();
      }
    }

    public function action_signup()
    {
      $username = Input::post('username');
      $password = Input::post('password');

      // ユーザー名とパスワード空欄時はサインアップフォームを表示する
      if (is_null($username) and is_null($password)) {
        return Response::forge(View::forge('auth/signup'));
      }

      // サインアップ処理

      // パスワードのハッシュ化
      $salt = substr(md5(uniqid(rand(), true)), 0, $this->_salt_length);
      $enc_password = $salt.$password;
      for ($i = 0; $i < $this->_iteration_count; $i++)
      {
        $enc_password = sha1($enc_password);
      }

      // バリデーション
      $val = Model_User::validate('create');
      $input = array(
        'name' => $username,
        'password' => $salt.$enc_password,
        'last_login' => \Date::time()->get_timestamp(),
      );
    
      if ($val->run($input))
      {
        // バリデーション成功時
        $user = Model_User::forge($input);
        if ($user and $user->save())
        {
          // サインアップ成功時
          return $this->login_succeeded($user->id);
        }
        else
        {
          // サインアップ失敗時
          return $this->login_failed();
        }
      }
      else
      {
        // バリデーション失敗時
        $data['errors'] = $val->error();
        return Response::forge(View::forge('auth/signup', $data));
      }

    }

    public function action_test1()
    {
      return Response::forge(View::forge('auth/test'));
    }

    public function action_test2()
    {
      return Response::forge(View::forge('auth/test'));
    }

    public function action_test3()
    {
      return Response::forge(View::forge('auth/test'));
    }

    // Twitter / Facebook ログイン成功/失敗時に呼ばれる
    public function action_callback()
    {
      $_opauth = new Opauth($this->_config, false);

      switch($_opauth->env['callback_transport'])
      {
        case 'session':
          session_start();
          $response = $_SESSION['opauth'];
          unset($_SESSION['opauth']);
        break;            
      }

      if (array_key_exists('error', $response))
      {
        return $this->login_failed();
//            echo '<strong style="color: red;">Authentication error: </strong> Opauth returns error auth response.'."<br>\n";
      }
      else
      {
        if (empty($response['auth']) || empty($response['timestamp']) || empty($response['signature']) || empty($response['auth']['provider']) || empty($response['auth']['uid']))
        {
          return $this->login_failed();
//          echo '<strong style="color: red;">Invalid auth response: </strong>Missing key auth response components.'."<br>\n";
        }
        elseif (!$_opauth->validate(sha1(print_r($response['auth'], true)), $response['timestamp'], $response['signature'], $reason))
        {
          return $this->login_failed();
//          echo '<strong style="color: red;">Invalid auth response: </strong>'.$reason.".<br>\n";
        }
        else
        {
          // Twitter / Facebook ログイン成功
          return $this->opauth_login($response);
        }
      }
    }

    public function opauth_login($response = null)
    {
       $provider = $response['auth']['provider'];
       if ($provider === 'Twitter') return $this->twitter_login($response);
       if ($provider === 'Facebook') return $this->facebook_login($response);
    }

   public function twitter_login($response = null)
    {
      $uid = (string) $response['auth']['uid'];
      $query = Model_TwitterUser::query()->where('uid', $uid);
      if ($query->count() == 0)
      {
        // TwitterUser未登録の場合はサインアップ
        return $this->twitter_signup($response);
      }

      // TwitterUser登録済みの場合はログイン
      $twitter_user = $query->get_one();
      return $this->login_succeeded($twitter_user->user_id);
    }

    public function facebook_login($response = null)
    {
      $uid = $response['auth']['uid'];
      $query = Model_FacebookUser::query()->where('uid', $uid);
      if ($query->count() == 0)
      {
        // FacebookUser未登録の場合はサインアップ
        return $this->facebook_signup($response);
      }

      // FacebookUser登録済みの場合はログイン
      $facebook_user = $query->get_one();
      return $this->login_succeeded($facebook_user->user_id);
   }

    public function twitter_signup($response = null)
    {
      // バリデーション
      $val = Model_TwitterUser::validate('create');
      $input = array(
        'uid' => (string) $response['auth']['uid'],
        'token' => $response['auth']['credentials']['token'],
        'secret' => $response['auth']['credentials']['secret'],
      );
    
      if ($val->run($input))
      {
        // バリデーション成功時
        $user = Model_User::forge(array(
          'nickname' => $response['auth']['info']['nickname'],
          'last_login' => \Date::time()->get_timestamp(),
        ));
        $twitter_user = Model_TwitterUser::forge($input);

        if ($user and $twitter_user)
        {
          // ユーザー生成成功
          try
          {
            \DB::start_transaction();
            if ($user->save() === false)
            {
              // User保存失敗
              throw new \Exception('user save failed.');
            }
              
            $twitter_user->user_id = $user->id;
            if ($twitter_user->save() === false)
            {
              // TwitterUser保存失敗
              throw new \Exception('twitter_user save failed.');
            }

            // UserとTwitterUserの保存成功
            \DB::commit_transaction();
            return $this->login_succeeded($user->id);
          }
          catch (\Exception $e)
          {
            \DB::rollback_transaction();
            return $this->login_failed();
          }

        }
        else
        {
          // ユーザー生成失敗
          return $this->login_failed();
        }

      }
      else
      {
        // バリデーション失敗時
        return $this->login_failed();
      }
    }

    public function facebook_signup($response = null)
    {
      // バリデーション
      $val = Model_FacebookUser::validate('create');
      $expires = strtotime($response['auth']['credentials']['expires']);
      $input = array(
        'uid' => (string) $response['auth']['uid'],
        'token' => $response['auth']['credentials']['token'],
        'expires' => $expires,
      );
    
      if ($val->run($input))
      {
        // バリデーション成功時
        $user = Model_User::forge(array(
          'nickname' => $response['auth']['info']['name'],
          'last_login' => \Date::time()->get_timestamp(),
        ));
        $facebook_user = Model_FacebookUser::forge($input);

        if ($user and $facebook_user)
        {
          // ユーザー生成成功
          try
          {
            \DB::start_transaction();
            if ($user->save() === false)
            {
              // User保存失敗
              throw new \Exception('user save failed.');
            }
              
            $facebook_user->user_id = $user->id;
            if ($facebook_user->save() === false)
            {
              // FacebookUser保存失敗
              throw new \Exception('facebook_user save failed.');
            }

            // UserとFacebookUserの保存成功
            \DB::commit_transaction();
            return $this->login_succeeded($user->id);
          }
          catch (\Exception $e)
          {
            \DB::rollback_transaction();
            return $this->login_failed();
          }

        }
        else
        {
          // ユーザー生成失敗
          return $this->login_failed();
        }

      }
      else
      {
        // バリデーション失敗時
        return $this->login_failed();
      }
    }

    public function login_succeeded($user_id)
    {
      Session::set('user_id', $user_id);
      Response::redirect('auth/test1');
    }

    public function login_failed()
    {
      return Response::redirect('auth/test1');
    }
 
    public function action_logout()
    {
      Session::delete('user_id');
      Response::redirect('auth/test1');
    }
}

懸念点など

・とりあえず作っただけなので全然脆弱な気がする。
XSRF対策をしていない。
・ぜんぶlogin_failed()に飛ばしているけどいいの?
・特定のページでログインを経由して特定のページに復帰するようなことは考慮されていない。
・パスワードはいちおうハッシュ化してみた。
・Usersでの一括管理に意味があるのか...