ActionCableにおいてのcurrent_userについて読み解く｜Rails

#action cableのcurrent_userについて読み解く
##本記事投稿の経緯
Railsで簡単なチャットアプリを作成中、ActionCable内での`current_user`の取り扱いにつまづいたのでメモ。

##環境
Rails 5.2.3(最新じゃないンゴオオオォ)
Ruby 2.4.0(最新じゃないンゴオオオォ)

##connection.rbで定義されるcurrent_user

```ruby:connection.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
    end

    protected
    def find_verified_user
    remember_token = User.encrypt(cookies[:user_remember_token][:value]) if cookies[:user_remember_token]
      if current_user = User.find_by(remember_token: remember_token)
        current_user
      else
        reject_unauthorized_connection
      end
    end
  end
end
```
上記は公式ドキュメントのサンプルコードであるが、ActionCableはWebSocketで通信する際、まずここで認証を行える。その際、`current_user`を定義しているのだが、こいつの仕組みがイマイチ把握できなかったのでソースコードを読み解いていくことにした。

##identified_by
まずは`identified_by`を見ていく。(コードは一部です。)

```action_cable/connection/identification.rb
included do
  class_attribute :identifiers, default: Set.new
end

module ClassMethods
  # Mark a key as being a connection identifier index that can then be used to find the specific connection again later.
  # Common identifiers are current_user and current_account, but could be anything, really.
  #
  # Note that anything marked as an identifier will automatically create a delegate by the same name on any
  # channel instances created off the connection.
  def identified_by(*identifiers)
    Array(identifiers).each { |identifier| attr_accessor identifier }
    self.identifiers += identifiers
  end
end

```
コメントに大体が書いてあるのだが、`identifier_by`は後で特定のconnectionを見つけられるようにキーを作っている。今回であれば`current_user`をキーとしてconnectionを識別できるようにしてくれているのだ。
更にはそれを子であるchannelに自動で同じ名前で`delegate`するという。
>
Connections form the foundation of the client-server relationship. For every WebSocket accepted by the server, a connection object is instantiated. This object becomes the parent of all the channel subscriptions that are created from there on. 

そういえばrails guideにもconnectionはそこから作成されるchannnelの全ての親となると書いてあった。
(これややこしいが継承のことではないっぽい？。、`delegate`してるし...それはまた今度、気が向けば記事にします。)

つまり、WebSocketの接続の識別として`current_user`は使われており、更には子であるchannelでも利用可能であるはず。

てわけで使ってみる。

##実際にcurrent_userを使ってみる
###流れ
⓪チャットルームモデルとユーザーモデルは多対多の関係。（ここは省略。）
①connection接続時にユーザーがログイン済みユーザーか認証し、`current_user`に接続したユーザーを入れる
②room_channelにサブスクライブする時、先の`current_user`にそのチャンネルに接続する権限があるか認証する。

connection.rbは上記のサンプルコードと同じにします。
room_channel.rbのサブスクライブ部分は下記のとおり。

```room_channel.rb
class RoomChannel < ApplicationCable::Channel

  def subscribed
    if !params.empty? && current_user.is_member?(params['room_id'])
      stream_from "room_channel_#{params['room_id']}"
    else
      reject
    end
  end

end
```

クライアントからチャットルームのidを送ってもらい、そのルームにログイン中のユーザーが属しているか確認している。
`is_member?`メソッドは下記のような感じで。

```user.rb
  def is_member?(room_id)
    members = Room.find(room_id).users
    members.any?{|member| member.id == self.id }
  end
```

こんな感じで`current_user`を使って各チャンネルでも認証をできる。

###is_member?メソッドに指摘が入ったので修正（20190927）
下記は`exists?`を使ったver。
ここ、色々書き方あるが、どれが一番リソースに優しいか考えてたら、記事にできそうだったので今度書きます。（SQL力が足りない...）

```user.rb
  def is_member?(room_id)
    Room.find(room_id).users.exists?(self.id)
  end
```

##まとめ
connection時、ユーザーがログイン済みか認証し、ログインしていればそれを`current_user`にいれ、子のチャンネルで利用可能にする。
`current_user`を使えばチャンネル内でも認証可能。

##あとがき
実は今回、`current_user`の取り扱いでつまづいたのはrspecでテスト中のことでして、
「`current_user`はチャンネル内で使えるって聞いたんだけど！！」
とか叫いたりして、
「本当に使えんのか？.....調べてやる！」
と思い執筆しました。

結局、testコードの書き方が間違っていただけでしたンゴ！！めでたしめでたし！！

まあためにはなったかな。。。

##参考記事
[【ActionCable】チャンネル接続／購読時にユーザ認証を行う](https://qiita.com/NaokiIshimura/items/a63cdcbec5b64c5366b6)
[これはMUST！ActiveSupport の Class#class_attribute を使おう！](https://qiita.com/cuzic/items/ffd115f1e17458020b1b)
[Rubyのdelegateについて整理する](https://scrapbox.io/10nin/Ruby%E3%81%AEdelegate%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6%E6%95%B4%E7%90%86%E3%81%99%E3%82%8B)
[Rails Guide](https://guides.rubyonrails.org/action_cable_overview.html)