Railsでチャット機能を実装する方法(実装編)

- チャット機能を実装する場合、WebSocketを使う、Server Sent Event(SSE)を使う、Pusherなどのサービスを活用するなど、いくつかの方法がありますが、私のケースでは、WebSocketを使うのが最もマッチしていたので、WebSocketを利用することを考えます。この辺の選定についてはまた別途まとめたいと思います
- RailsでWebSocketを使う場合、`websocket-rails`を使うのが、手っ取り早くて良いのではないかと思います
- そこで、`websocket-rails`での、チャネル、プライベートチャネル、セキュリティ、認証あたりの関係性が分かりにくかったので、自分なりに整理してみました

## `websocket-rails`の基本的な使い方
### Event Routerへのイベントの追加
- JSクライアントからのリクエストをRailsのコントローラーとマッピングしている

```event.rb
WebsocketRails::EventMap.describe do
  namespace :tasks do
    subscribe :create, :to => TaskController, :with_method => :create
  end
end
```

### JSクライアントからイベントをトリガー

```
var dispatcher = new WebSocketRails('localhost:3000/websocket');
dispatcher.trigger('tasks.create', {name: "hogehoge"});
```

### イベントのハンドリング
- `send_message`で、クライアント側にプッシュできる
  - `send_message`は、このイベントを実行したクライアントにしか送信しない
    - クライアントが接続した際に"接続完了しました"のようなメッセージを表示する場合に使える
- 一方、`broadcast_message`は、接続している全てのクライアントにプッシュできる

```
class TaskController < WebsocketRails::BaseController
  def create
    task = Task.new message
    if task.save
      send_message :create_success, task, :namespace => :tasks
    else
      send_message :create_fail, task, :namespace => :tasks
    end
  end
end
```

### JSクライアントでレスポンスを受け取る
- `dispatcher.bind`で、コールバックを登録しておく

```
dispatcher.bind('tasks.create_success', function(task) {
  console.log('successfully created ' + task.name);
});
```

## チャネルを使用する場合
- チャネルは、WebSocket接続したクライアントに対して任意のタイミングでメッセージを送ることができる
- チャネルは、複数のチャットルームを管理して、特定のクライアントにブロードキャストする必要がある場合に使える
  - チャネルがない場合には、イベントを指定して、そのイベントをトリガーしたクライアントにプッシュする(send_message)か、WebScoketに接続している全クライアントにブロードキャストする(broadcast_message)するかしかない

### Event Routerへのイベントの追加

```
WebsocketRails::EventMap.describe do
  subscribe :new_post, to: ChatController, with_method: :create_post
end
```

### JSクライアントでチャネルをサブスクライブする
- `dispatcher.subscribe`でチャネルをサブスクライブすることができる
  - チャネル名は、乱数などで動的に指定することで、そのチャネル名を知る特定のクライアントだけがアクセスできるチャネルをつくることができる

```
var dispatcher = new WebSocketRails('localhost:3000/websocket');
postsChannel = dispatcher.subscribe('posts');
```

### JSクライアントからイベントをトリガー
- `dispatcher.trigger`でイベントをトリガーできる(ここは、チャネル名ではない点に注意)

```
dispatcher.trigger('new_post', post);
```

### イベントのハンドリング
- `WebsocketRails[:posts].trigger`とチャネル名を指定して、triggerをすることで、このチャネルにメッセージをブロードキャストできる

```
class ChatController < WebsocketRails::BaseController

  def create_post
    post = Post.new(message)
    post.cook

    if post.save
      WebsocketRails[:posts].trigger 'new_post', post
    else
      puts "Failed to saved post"
    end
  end
```

### JSクライアントでレスポンスを受け取る
- dispatcherではなく、`チャネル.bind`でコールバックを登録しておく

```
postsChannel.bind('new_post', function(post) {
  /* イベント受け取り後の処理 */
});
```

[Working with Channels · websocket-rails/websocket-rails Wiki · GitHub](https://github.com/websocket-rails/websocket-rails/wiki/Working-with-Channels)

### プライベートチャネル
- プライベートチャネルは、1クライアントからの接続を保証するもの
  - 接続した際に、他のクライアントが接続していれば、全部追い出して、そのクライアントだけを接続するようにする
  - チャットのような2人のクライアントが接続するようなアプリケーションには向いていない
    - このようなアプリケーションでプライベートチャネルを使いたい場合は、2つのプライベートチャネルを作って、それぞれにメッセージを送り合うようなことをする必要がある
    - もしくは、チャネル名をユニークにすれば、そのユニークなチャネルにアクセスした限定されたクライアントだけがアクセスできる状況を作ることができる
- セキュアとは異なる概念
  - 当たり前だけどプライベートとは、他の誰かが接続していないというだけで、その接続がセキュアというわけではない
  - また、チャネル名をユニークにしても、そのチャネル名がわかれば接続できてしまう
- プライベートチャネルはパブリックから、プライベートに変更することができる
  - デフォルトでは、パブリックチャネルに接続していたクライアントは、そのチャネルから追い出されてしまうが、以下の設定をすることでプライベートチャネルでも複数クライアントからの接続を維持することができる

```
config.keep_subscribers_when_private = true
```

[Using Private Channels · websocket-rails/websocket-rails Wiki](https://github.com/websocket-rails/websocket-rails/wiki/Using-Private-Channels)

[How to broadcast data to specific subcsriber? · Issue #104 · websocket-rails/websocket-rails](https://github.com/websocket-rails/websocket-rails/issues/104)

[How to prevent spam when using websocket-rails gem? - Stack Overflow](http://stackoverflow.com/questions/15237801/how-to-prevent-spam-when-using-websocket-rails-gem)

### Security
- `window.location.protocol`が`https`であれば、セキュアなプロトコル`wss`で通信してくれる

[WebSocket Security | Heroku Dev Center](https://devcenter.heroku.com/articles/websocket-security)

[websocket-rails: standaloneモードのサーバでSSL通信 - Qiita](http://qiita.com/utwang/items/be06f1eb6e7075fc6447)

### UserManager
- UserMangerを使えば、特定のユーザーにメッセージを送ることができる

```event.rb
subscribe :client_connected to: WebsocketController, with_method: :client_connected
```

- `wss://localhost:3000/websocket?client_id=some_unique_id`でリクエストすれば、client_connectedがコールされて、client_idをもつuserが登録される

```
class WebsocketController < WebsocketRails::BaseController

  def client_connected
    WebsocketRails.users[params[:client_id]] = connection
  end

end
```

[Access one specific client from anywhere (regular controller / delayed task) · Issue #164 · websocket-rails/websocket-rails](https://github.com/websocket-rails/websocket-rails/issues/164)

[Websocket Railsを使って特定のユーザーにメッセージを送る - 亀の速度で走る](http://takuyanz.hatenablog.com/entry/2015/01/30/224938)

#### current_user
- `ApplicationController`で、`current_user`というメソッドが定義されていて、ユーザーがサインインし、かつWebSocketコネクションが確立されている場合、そのコネクションは、UserManagerに格納される
  - 逆に、current_userが定義されていない、もしくはサインインしていない場合は、UserManagerにコネクションは格納されない
- つまり、Deviseを使用していれば、自動的にUserManager.usersが手に入る

##### current_user以外
- ただし、DeviseでUser以外のモデルを使用している場合(例えばAdmin)、current_userではなく、current_adminとなる
  - current_adminを使用したい場合は、user_classを変更することで対応できるとあるが、自分の場合は、`User`以外うまく動かなかった

```
config.user_identifier = :id
config.user_class = Admin
```

  - このプルリクにあるように、Standaloneモードで`NameError: uninitialized constant`が表示される場合は、procを使うと良いとある
    - でも、これをやってもUserManager.usersは空のまま。。。

  [Recommend overriding user_class with a proc · alexdunae/websocket-rails@3612137](https://github.com/alexdunae/websocket-rails/commit/3612137207edc0bca52cd8a57213e391a47eee8e)

### 認証/認可
- WebSocketのプロトコル自体は、認証や認可の仕組みは提供していないので、アプリケーションで実装する必要がある
- `WebsocketRails::BaseController`を継承したコントローラー内で、ApplicationControllerのメソッドは普通に使えるので、Deviseを使っている場合は、`current_user`が使える
- プライベートチャネルと組み合わせて、サーバー側で認証する方法が紹介されている

[Using Private Channels · websocket-rails/websocket-rails Wiki](https://github.com/websocket-rails/websocket-rails/wiki/Using-Private-Channels)

[Solves Warden compatibility and gives the possibility to register an user after connection established. by phlegx · Pull Request #217 · websocket-rails/websocket-rails](https://github.com/websocket-rails/websocket-rails/pull/217)

[current_user is not compatible with Warden (Devise) · Issue #216 · websocket-rails/websocket-rails](https://github.com/websocket-rails/websocket-rails/issues/216)

[Can't Access Helpers in Websocket Controller · Issue #3 · websocket-rails/websocket-rails](https://github.com/websocket-rails/websocket-rails/issues/3)

[Register and Authenticate User · Issue #184 · websocket-rails/websocket-rails](https://github.com/websocket-rails/websocket-rails/issues/184)

[websocket-rails gem and authentication - Stack Overflow](http://stackoverflow.com/questions/15049262/websocket-rails-gem-and-authentication)

[Filtering Channel Events · websocket-rails/websocket-rails Wiki](https://github.com/websocket-rails/websocket-rails/wiki/Filtering-Channel-Events)

### Standalone Mode
- スタンドアロンモードでは、Redisを起動して、イベント処理を任せることができる

```
WebsocketRails.setup do |config|
  config.standalone = true
end
```

- 以下のコマンドで、スタンドアロンサーバーを起動、停止できる
  - Redisを起動した上で、実行する

```
$rake websocket_rails:start_server
$rake websocket_rails:stop_server
```

[Standalone Server Mode · websocket-rails/websocket-rails Wiki · GitHub](https://github.com/websocket-rails/websocket-rails/wiki/Standalone-Server-Mode)

### Onlineの検出

[How do I tell if a user is online? | Ryan Epp](http://www.ryanepp.com/blog/how-do-i-tell-if-a-user-is-online)

[ActionCable Devise Authentication](http://www.rubytutorial.io/actioncable-devise-authentication/)

[Unable to reach channel subscribers from SideKiq workers · Issue #173 · websocket-rails/websocket-rails](https://github.com/websocket-rails/websocket-rails/issues/173)

### Synchronization

[Multiple Servers and Background Jobs · websocket-rails/websocket-rails Wiki · GitHub](https://github.com/websocket-rails/websocket-rails/wiki/Multiple-Servers-and-Background-Jobs)