Rails ActionCableでチームチャットを作る

# はじめに
ポートフォリオでActionCableでチームチャット機能を作ったので、アウトプットする。
# 参考記事
以下の記事を参考にしました。ありがとうございました。
・ [Rails 5 + ActionCableで作る！シンプルなチャットアプリ（DHH氏のデモ動画より）](https://qiita.com/jnchito/items/30ab14ebf29b945559f6)
・ [【Rails6.0】ActionCableとDeviseの欲張りセットでリアルタイムチャット作成(改正版)](https://qiita.com/eRy-sk/items/4c4e983e34a44c5ace27)
# コード
参考記事にあるコードに手を加えて、チーム毎にチャットを送るようにする。
今回は、teamコントローラーにchatアクションを作ってチャットを送れるようにする。
Action Cable以外の部分をMVC構造に従って、作成する。

### model
モデルは、チームモデル、ユーザーモデルの多対多の関係を記述する。
また、チャットの中身であるメッセージモデルは、ユーザーとチームの中間テーブルのモデルとして作成する。

```ruby:app/models/team.rb
class Team < ApplicationRecord #チームモデル
  belongs_to :owner, class_name: 'User', foreign_key: :owner_id
  has_many :team_assigns, dependent: :destroy 
  has_many :members, through: :team_assigns, source: :user
  has_many :messages, dependent: :destroy
end
```

```ruby:app/models/user.rb
class User < ApplicationRecord
  # Include default devise modules. Others available are:
  # :confirmable, :lockable, :timeoutable, :trackable and :omniauthable
  has_one_attached :icon #Active Storageでユーザーアイコンをつける
  has_many :team_assigns, dependent: :destroy
  has_many :teams, through: :team_assigns, source: :team
  has_one :owner_team, class_name: 'Team', foreign_key: :owner_id, dependent: :destroy
  has_many :messages, dependent: :destroy
end
```

```ruby:app/models/team_assign.rb 
class TeamAssign < ApplicationRecord
  belongs_to :team
  belongs_to :user
end
```

```ruby:app/models/message.rb
class Message < ApplicationRecord
  belongs_to :user
  belongs_to :team
end
```

### controller
コントローラーは、チーム内にチャットアクションを作成した。
リンクからチームIDを受け取って、Teamクラスから検索、そのチームが持つチャットを取得してviewに渡す。

```ruby:app/controllers/teams_controller.rb
  def chat
    @team = Team.find(params[:id])
    messages = @team.messages
  end
```

### view
bootstrapを適用したので、container、alertなどを使っている。基本的には受け取ったチャットをメッセージパーシャルをレンダリングすることで画面に表示している。また、チャットを送るためのフォームを設置している。

```html:app/views/teams/chat.html.erb
<h1 id="chat" data-team_id="<%= @team.id %>">
  <%= @team.name %>チャットルーム
</h1>

<div class="container">
  <% if @team.members.count == 1 %>
    <div class="alert alert-danger" role="alert">
      チャットするチームメンバーがいません。チーム画面からメンバーを追加して下さい
    </div>
  <% end %>
  <form class="form-group">
    <label>チャット:</label>
    <input type="text" data-behavior="team_speaker", placeholder="内容", class="form-control">
  </form>
  <div id="messages_<%= @team.id %>">
    <ul class="list-group">
      <%= render @messages %>
    </ul>
  </div>
</div>
```

```html:app/views/messages/_message.html.erb
<div class="message">
  <li class="list-group-item">
    <div class="row">
      <div class="col-md-2">
        <% if message.user.icon.attached? %>
          <%= image_tag message.user.icon.variant(resize:'50x50').processed %>
        <% end %>
        <p><%= message.user.name %></p>
      </div>
      <div class="col-md-10">
        <small class="text-muted"><%= l message.created_at, format: :short %></small><br>
        <%= message.content %>
      </div>
    </div>
  </li>
</div>
```

ここから、Action Cableの実装をする。
まず、チームチャンネルを作成する。
チャンネル用にディレクトリとファイルが作成される。

```terminal
$ rails g channel team
```

### coffeescript
まず、ブラウザからサーバーを監視する設定を記述していく。
jQueryを活用して、viewからチームIDを取得して、購読するチャンネルを作成する。

```coffee:app/assets/javascripts/channels/team.coffee
App.team = App.cable.subscriptions.create {
  channel: "TeamChannel",
  team_id: $("#chat").data("team_id")}, 

  connected: ->
    # Called when the subscription is ready for use on the server

  disconnected: ->
    # Called when the subscription has been terminated by the server

  received: (data) ->
    # Channel側のspeakメソッドで受け取ったチャットを受け取る
    $('#messages_'+data['team_id']).prepend data['message']

  speak: (message) ->
    team_id = $('#chat').data('team_id')
    @perform 'speak', message: message, team_id: team_id

$(document).on 'keypress', '[data-behavior~=team_speaker]', (event) ->
  if event.keyCode is 13 # return = send
    App.team.speak event.target.value
    event.target.value = ''
    event.preventDefault()

```

### channel.rb
次に、サーバーからブラウザを監視する設定を記述していく。
ブラウザの@perform speakによって、サーバー側のspeakアクションが呼び出される。
speakアクションは、ブラウザから受け取った情報を元に、メッセージクラスのインスタンスを作成する。

```ruby:app/channels/team_channel.rb
class TeamChannel < ApplicationCable::Channel
  def subscribed
    stream_from "team_channel_#{params['team_id']}" #どのチームを監視しているのかを示す
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def speak(data)
    message = Message.create!(
      content: data['message'],
      team_id: data['team_id'],
      user_id: current_user.id
    )
  end
end
```

チャンネル内でcurrent_userを使っているが、そのためには次のコードを追記する必要がある。

```ruby:app/channels/application_cable/connection.rb
module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    def connect
      self.current_user = find_verified_user
    end

    private

      def find_verified_user
        verified_user = User.find_by(id: env['warden'].user.id)
        return reject_unauthorized_connection unless verified_user
        verified_user
      end
  end
end
```

### Messageモデル
Messageモデルにインスタンスが作成された際(コミット後)にjobが走るようにします。

```ruby:app/models/message.rb
class Message < ApplicationRecord
  belongs_to :user
  belongs_to :team
  after_create_commit { MessageBroadcastJob.perform_later self } #追記
end
```

### job
jobから、作成されたメッセージをブラウザへ送る設定を記述します。
まず、railsコマンドからMessageBroadcastJobを作成します。

```console
$ rails g job MessageBroadcast
```

ActiveStorageを使っている場合、ApplicationController.rendererのhttp_hostを指定しないと、画像の参照先がエラーになる。http://ではなく、https://から始まるURLを本番で使う場合には、https: trueを指定する必要がある。

```ruby:app/jobs/message_broadcast_job.rb
class MessageBroadcastJob < ApplicationJob
  queue_as :default

  def perform(message)
    ActionCable.server.broadcast(
      "team_channel_#{message.team_id}",
      message: render_message(message),
      team_id: message.team_id
    )
  end

  private
    def render_message(message)
      renderer = ApplicationController.renderer.new(
        http_host: 'http_host番号', #ローカルだと、locallhost:3000。本番環境によって変更
　　　　#https: true 　　　　　　　　 https://から始まるURLの場合、このコードを追記する必要あり
      )
      renderer.render(partial: 'messages/message', locals: { message: message })
    end
end
```

このjobによって、coffeescriptのreceiveにメッセージのパーシャルが渡されてリアルタイムで反映されるという仕組みになっている。

# まとめ
ActionCableは複雑だと思ったが、参考文献によってデータの受け渡しを追うことができ、理解することができた。この経験から、複雑に見えても、データの流れを1つずつ追うことが大事で、サーバー側だったらbinding.pry、ブラウザ側だったらdebuggerを活用するのが一番の近道だと改めて感じた。