Help us understand the problem. What is going on with this article?

Action Cableでリアルタイムチャットアプリの作成方法 (Rails 5.1.4にて)(その2)グループ別チャンネルの作成!!

More than 1 year has passed since last update.

概要

この記事はAction Cableでリアルタイムチャットアプリの作成方法 (Rails 5.1.4にて)(その1)herokuで動かす!!のその2である。
その1で作成したチャットアプリにグループチャット機能を追加していく。
機能の概要としては、今まですべてのユーザーにブロードキャストしていたものグループ別に分け、そのグループをsubscribeしているユーザーだけに送信(append)するというもの。
前提として、ログイン機能が実装されている投稿アプリがあり、session[:user_id]でユーザーのログインを管理している。

つまずいた部分としては、channelやjobからはsessionが使えないためログインしているユーザーのidとそのユーザーがsubscribeしているグループ(チャンネル)の取得する方法にかなりてこずった。最終的には各々の投稿に投稿者がsubscribeしているgroupの情報を登録することで解決した(かなり無理やり)。

環境

Ruby2.2.4
Rails 5.1.4

参考にしたサイト

Rails5のActionCableで簡易チャットの作成 ~モデルに応じたチャンネルを聴講する方法~
このサイトにはDMを実装する方法が書かれているが、作成したい機能が若干異なる(今回はグループ別にチャンネルを作りたい)のと、ブロードキャストするアクションの記載場所が異なるため、この記事を参考にしながら独自に書き替えた。

手順

1. 下準備(groupカラムの追加)

・usersモデルにgroupカラムを追加
・postsモデルにgroupカラムの追加

2. viewにgroupを選択するselectボックスの設置

今回はこんな感じ

app/assets/view/posts/index.html.erb
  <div class="new-post">
    <input name="content" rows="2" cols="65" data-behavior="room_speaker">
    <select id="group" name="group">
      <option value="1">Group1</option>
      <option value="2">Group2</option>
      <option value="3">Group3</option>
    </select>
  </div>
(省略)

3.room.coffeeの書き替え

エンター入力時に手順2で作成したselectボックスの値を取得してspeakメゾットに送信し、それを引数に加えて、room_channel.rbのspeakアクションを呼ぶ。

app/assets/javascripts/channels/room.coffee
  speak: (message, group)->
    @perform 'speak',message: message, group: group


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

4. room_channel.rbの編集

まず、post.createにgroupを追加し、送られてきたdata['group']を登録postに登録する。
また、投稿者のidもcurrent_userから取得しuser_idに登録する。
さらに、userにもgroup情報を登録する

そして、subscribeの購読先にstream_for current_user.groupを追加して、selectボックスで指定したチャンネルを購読できるようにする。

channelの中でcurrent_userはこのままでは使えず、次の手順が必要である。

app/channels/room_channel.rb
class RoomChannel < ApplicationCable::Channel
  def subscribed
    stream_from "room_channel"
    stream_for current_user.group
  end

  def unsubscribed
    # Any cleanup needed when channel is unsubscribed
  end

  def speak(data)
     Post.create!(content: data['message'], user_id: current_user.id, group: data['group'])
     user = User.find_by(id: current_user.id)
     user.group = data['group']
     user.save
  end

Memo:heroku上でテストをしたら上記のコードではうまく動かなかった。
原因は、処理の順番。
postモデル(post.rb)でjobを呼び出して、投稿をブロードキャストしているが、サーバーの処理速度の関係でこの時点ではuserテーブルにgroup情報が登録できておらず、stream_for current_user.groupで購読先を検索できずにストリーミングを受信できないようであった。
そのため、speakメゾットをまず、userのgroup登録を行い、その後に、postを登録、postがトリガーとなりブロードキャストという順番に以下のように書き替えた。

def speak(data)
     user = User.find_by(id: current_user.id)
     user.group = data['group']
     user.save
     Post.create!(content: data['message'], user_id: current_user.id, group: data['group'])

  end

すると、うまく動作するようになった。
サーバーでの実行速度などを考慮して処理の順番を考えたコーディングをすることの重要性を学んだ。

5.channelでcurrent_user(session情報)を使えるようにする。

これは、上記の参考サイトを参考にした。
非常に助かりました。

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

   protected

   def find_verified_user
     if verified_user = User.find_by(id: session['user_id'])
       verified_user
     else
       reject_unauthorized_connection
     end
   end

   def session
     cookies.encrypted[Rails.application.config.session_options[:key]]
   end
 end
  end

6. message_broadcast_job.rbを編集

ログインしているユーザーの購読しているチャンネルにのみブロードキャストするするように変える。
手順4のstream_forで指定したchannelにブロードキャストするにはRoomChannel.broadcast_toを使う。
(全員にはActionCable.server.broadcast)
post.rbから送られてきたmessageはモデルそのものなので、message.groupとすれば、その投稿のgroupカラムに登録されたデータが取れる。

app/jobs/message_broadcast_job.rb
(省略)
  def perform(message)
    # Do something later
    group = message.group
     RoomChannel.broadcast_to(group, message: render_message(message))
  end

以上

感想

userモデルにgroupを追加しても、channel内やjob内で取得できないので、postモデルに追加することで解決したが、すこし変な実装になってしまった。
job内でcurrent_userを使うやり方を調べたがわからなかったため、このような形になってしまった。
もし、job内でcurrent_userを使う方法がわかれば、もう少しきれいに書けるず。

分かる方、コメントで教えてください。
お願いします。

Hijiri-K
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away