pusherの助けを借りたWebSocketを使ったチャットシステム

リアルタイムなchatを作りたい。
そんな欲求誰にでもあるものです。

時間のない主婦の方にも手軽にパパっとチャットシステムを作る方法を紹介しようと思います。

材料は
rails
pusher

作る料理は
チャットシステム(現在の参加者表示機能付き)

の二つです。

変なテンションな書き方はここまで。

今日の記事は全面的にここを参考にさせていだきました。
Yuno

pusherはWebSocketsを利用したAPIです。

Leader in realtime technologies| Pusher

わかりやすい図はっときます。

f:id:gogo  _sakura:20120124124957p:image

例えばブラウザがpostリクエスト投げると、それを受け取ったサーバからpusherにそのデータを加工してパス。
するとブラウザにpushしてくれるというイメージ。

pusher利用にはユーザ登録で得られるAPI Credentialsが必要です。
まずはpusherのサイトでユーザ登録して、

app _id
key
secret

をゲットしてください。

今日の記事は実際に作ったものからコピペで書いていますが。
view部分はhamlで書いてるので、見ずらくてすいません。
erbメインの方は脳内で変換かけてください。

とりあえずGemfileに記述

1
gem 'pusher'

bundle install後、config/initializers内にpusher.rb作成

1
2
3
  Pusher.app_id = ユーザ登録でゲットしたapp_id
  Pusher.key = ユーザ登録でゲットしたkey
  Pusher.secret = ユーザ登録でゲットしたsecret

config/route.rbにauthアクションへのルーティング追加

1
2
3
resources :chats , :only => [:index , :create , :destroy] do
  post :auth , :on => :collection
end

現在の参加者を取得するためには、pusherのPresence Channelsを使用しなければなりません。
Presence Channelsを使用するためには、認証を受けること・channel名のprefixにpresence-つけないといけません。

view

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
= javascript_include_tag "http://js.pusher.com/1.11/pusher.min.js"

:javascript
  $(function(){
    Pusher.channel_auth_endpoint = '/chats/auth';
    var pusher  = new Pusher("#{ユーザ登録でゲットしたkey }");
    var channel = pusher.subscribe("presence-chat");
    channel.bind("chatevent", function(html) {
      $("#chat_body").val('');
      $("ul").prepend(html);
    });

    channel.bind('pusher:subscription_succeeded', function(members){
      members.each(add_member);
    });

    channel.bind('pusher:member_added', function(member){
      add_member(member);
    })

    channel.bind('pusher:member_removed', function(member){
      remove_member(member);
    })
  });

  function add_member(member) {
    var container = $("<span>", {
      "class": "member",
      id: "presence_" + member.id
    });

    $('.members').append(container.html(member.info.name + " "))
  }

  function remove_member(member) {
    $('#presence_' + member.id).remove();
  }

= render "form"

%div
  現在の参加者
  %span.members

%ul#chat_list
  = render partial: "list", locals: { :chats => @chats }

フォームとリストの部分テンプレートは割愛。

認証は

1
Pusher.channel_auth_endpoint = '/chats/auth';

で指定したエンドポイントにコールバックされます。

controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
class ChatsController < ApplicationController
  protect_from_forgery :except => :auth

  def create
    @new_post = Chat.new(params[:chat])
    @new_post.user = @current_user

    if @new_post.save
      Pusher["presence-chat"].trigger!(
        "chatevent",
        render_to_string(
          file:   "chats/_list.html.haml",
          layout: false,
          locals: { :chats => Array.wrap(@new_post) }
        )
      )
    end

    render 'index'
  end

  def auth
    if current_user
      auth = Pusher["presence-chat"].authenticate(params[:socket_id],{
          :user_id => @current_user.id,
          :user_info => {
            :name => @current_user.name
          }
        }
      )
      render :json => auth
    else
      render :text => "Not authorized", :status => '403'
    end
  end

end

endpointで設定したauthアクションでは、コールバックされた際に渡されるsocket _idを引数authenticateを実行しています。
ちなみに自分はこの認証ではまりました。

1
protect_from_forgery :except => :auth

でコールバックで呼び出された際に、CSRFを無効にしてあげる必要があったんですねぇ。

以上で多分動くはずです。


同一の操作から呼ばれたかオブザーバで判定する方法

railsのobserverでユーザ情報とプロフィール情報を監視していて、
例えば1つのフォームにユーザ情報とプロフィール情報の編集項目があった場合。

after saveでフックした際には
after
saveは2回。それぞれのモデルで呼ばれる。

んでその二つ呼ばれたafter _saveが同じ処理で呼ばれたのかってわかんないと思っていた。

先日入れたuserstampの処理がどうなっているのだろうかと見たら、
ActionControllerのbefore _filterでThread.currentにuserのIDを押し込んでいた。

実際のソースは以下のとおり。

1
2
3
4
5
6
7
8
def stamper=(object)
  object_stamper = if object.is_a?(ActiveRecord::Base)
    object.send("#{object.class.primary_key}".to_sym)
  else
    object
  end
  Thread.current["#{self.to_s.downcase}_#{self.object_id}_stamper"] = object_stamper
end

キーにはobject id(デフォルトではUserのobject id)使ってる。
controller側で毎回Thread.currentに操作者のidを保存して、それをmodelで取り出している。

んでobserverで同一の処理で呼ばれたのか判定する方法は。

同じようにThread.currentに設定してるキーと値を任意の値にすれば良さそう。
ActionControllerのbefore filterでキーはuserstampと同じようにobject idを使って、適当にkey _とかprefixつけてみる。
んで値はユニークな値を設定する。

1
2
3
def set_key
  Thread.current["key_#{User.object_id}"] = Time.now.strftime("%Y%m%d%H%M%S") #ミリ秒まで含めた方がいいと思われ。
end

observerでは

1
2
3
4
5
6
7
8
9
10
observe :user,:user_profile

def after_save(model)
  case model
  when User
    p Thread.current["key_#{User.object_id}"]
  when UserProfile
    p Thread.current["key_#{User.object_id}"]
  end
end

で同じ値が取得できた場合には同一の操作から呼ばれたものと考えられる。

この方法は副作用とかどうなんだろう。
そしてもっとスマートな方法はあるのだろうか・・・


jQuery Mobile 勝手に不正なページへ遷移してしまう時の対応

去年スマフォサイト作りました。

railsつかってます。
jQuery Mobileも使ってます。

mobileinitイベント内で

1
$.mobile.ajaxEnabled = false;

でAjaxオフにしてます。

でもたまにページロード完了後に、
勝手にローディング画像が出てきて、全然自分が見たいページとは違うページに飛ばされることがあります。

調べた結果Android端末の沢山とiPhone4以外(というか3GS?)で起きる現状でした。

上記現象が起こる操作は

Aページ- >[Bページへのリンクをクリック]- >Bページ- >[端末のブラウザバック]- >Aページ- >[Cページをクリック]- >Cページ読み込み完了後にBページに飛ばされる。

対策としては

1
$.mobile.pushStateEnabled = false;

でpushStateオフにしたら直りましたっていうお話。


Nヶ月前の月を求める

またまたRubyネタ。

Rails抜きでNヶ月前の月から今日までの月を求める必要があったので考えてみました。

すっごい遠回りして考えてるかもしれません。

rubyの配列はインデックスにマイナスを指定することができます。
その場合、参照する先は後ろから数えた値となります。

今日のコードはirbだけで試せます。
REPL最高。

1
2
3
a = [1,2,3]
a[0] # => 1
a[-1] # => 3

とりあえずNは3として、今月を含めて3ヶ月前を求めるには

1
2
month = [1,2,3,4,5,6,7,8,9,10,11,12]
month[Time.now.month-3]

1月ならば11月が帰ってきます。
2月ならば12月。3月なら1月となります。

インデックスは0始まりなのでそのあたりを考慮しないとあかんですね。

以上です。と書こうとしたけど、3ヶ月前から今日までの月でしたね。

1
3.times{ |i| p month[Time.now.month-i-1] }

先ほど述べたようにインデックスが0始まりなので-1してます。
配列で欲しいなら

1
3.times.map{ |i| p month[Time.now.month-i-1] }

かなぁ?


rubyはブロックを引数に取れる

わかりやすく書けるだろうか。

railsのviewでログの出力を出し分けする以下のメソッドがあります。

user profile log
use favorite log

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
def genarate_link_path(log_type,log)
  case log_type
  when :warm
    "http://" + config[:host] + "/info/" + "warm"
  when :error
    "http://" + config[:host] + "/info/" + "error"
  end
end

def user_profile_log(log)
  disp_path = genarate_link_path(:warn,log)

  case log.operation_type
  when "insert"
    change_value = log.inspect + "ログを新規登録しました。"
  when "update"
    change_value = log.inspect + "ログを更新しました。"
  else
    "解析に失敗しました"
  end

  return link_to( change_value , disp_path )
end

def use_favorite_log(log)
  disp_path = genarate_link_path(:warn,log)

  case log.operation_type
  when "insert"
    change_value = log.name + "をお気に入りに追加しました。"
  when "update"
    change_value = log.name + "を更新しました。"
  else
    "解析に失敗しました"
  end

  return link_to( change_value , disp_path )
end

DRYじゃないですよね。
それぞれのメソッドでは
disp path = genarate link path(:warn,log)
return link
to( change value , disp path )
が二つのメソッドでかぶっております。

これをブロックで書き直します。

まずはブロックを引数に取るメソッド

1
2
3
4
5
def output(log,log_type,&block)
  disp_path = genarate_link_path(log_type,log)
  change_value = block_given? ? block.call : "ブロックもらえてないんで表示できません"
  link_to( change_value , disp_path )
end

&blockでブロックを引数に取れます。
受け取ったブロックはblock.callで実行できます。
ブロックがもらえたかどうかはblock _given?で判定できます。

んでブロックを引数にouput呼び出す方も修正。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
def user_profile_log(log)

  output(log,:warm) do
    case log.operation_type
    when "insert"
      change_value = log.inspect + "ログを新規登録しました。"
    when "update"
      change_value = log.inspect + "ログを更新しました。"
    else
      "解析に失敗しました"
    end
  end

end

def use_favorite_log(log)

  output(log,:error) do
    case log.operation_type
    when "insert"
      change_value = log.name + "をお気に入りに追加しました。"
    when "update"
      change_value = log.name + "を更新しました。"
    else
      "解析に失敗しました"
    end
  end
end

っていう。

説明できたか以前にソース汚いという。
あと思ったより再利用性の高いコードが書けなかったという。

ちなみに&blockしなくてもブロックは受け取れてその場合は

1
2
3
4
5
def output(log,log_type)
  disp_path = genarate_link_path(log_type,log)
  change_value = yield
  link_to( change_value , disp_path )
end

となります。
ブロックが渡されることが確実に保証されているならばこっちの方がいいのかな