1. rhiroe

    No comment

    rhiroe
Changes in body
Source | HTML | Preview
@@ -1,447 +1,446 @@
#編集履歴
-'18.6.27 追記
-`cookies.encrypted[Rails.application.config.session_options[:key]][‘warden.user.user.key’][0][0]`にログインしているユーザーのID情報を発見。記事を修正しました。
-
'18.10.12 修正
-`find_verified_user`メソッド内のif文が必ず`false`になってしまっていた部分を修正
+`find_verified_user`メソッド内のif文が必ず`false`になってしまっていたので修正
+'18.6.27 追記
+`cookies.encrypted[Rails.application.config.session_options[:key]][‘warden.user.user.key’][0][0]`にログインしているユーザーのID情報を発見。記事を修正しました。
#前置き
今回この記事を書こうと思った理由は3つ。
1つ目は、ActionCableを使ったリアルタイムチャット作成の記事が少ないこと、
2つ目は、既存する記事のRailsや諸々のバージョンが古いこと、
3つ目は、その多くがユーザー認証にDeviseを使うことを前提としていなかったこと。
などと偉そうなことを言っている私は、実はまだ未経験で入社して2ヶ月目のひよっこですので、もしおかしなところや説明不足なところがございましたら教えてください🙏
#本題
今回作成するアプリはユーザー認証にDevise、リアルタイムチャットを実現するためにActionCableを使用します。順序としては全てのユーザーが接続できるオープンなチャットアプリを作成した後、グループを作成してそこに参加するメンバーのみでチャットができるよう改良します。
##開発環境
Ruby 2.5
Rails 5.2
##参考にした記事
・[Action Cableでリアルタイムチャットアプリの作成方法 (Rails 5.1.4にて)(その1) herokuで動かす!](https://qiita.com/Hijiri-K/items/c3774c72a2cb68e1a720)
・[Rails 5 Action Cable メッセージとルームを紐付ける。](https://qiita.com/kohei1228/items/7aed5aad9c63e834c0e1)
##完成形イメージ
下のような同じルーム内でのみリアルタイムで投稿内容が表示されるシンプルなアプリです。
![名称未設定.mov.gif](https://qiita-image-store.s3.amazonaws.com/0/262029/f8963451-bba3-493c-8e1e-2a1d0942b7e5.gif)
##ソースコード
必要ないかもしれませんがGitHubのURLを貼っておきます。
https://github.com/eRy-sk/chat_app
##アプリの作成
今回はchat_appという名前でアプリケーションを作成します。
```:bash
$ rails new chat_app
```
必要なgemをインストールします。
```:Gemfile
gem 'devise'
gem 'jquery-rails'
```
```:bash
$ bundle install
```
jQueryを読み込む
```js:app/assets/javascripts/application.js
//= require jquery
//= require rails-ujs
```
##ユーザー認証
続いてDeviseでサクッとユーザー認証を実現します。
```:bash
$ rails g devise:install
$ rails g devise user
$ rails db:migrate
```
サーバーを起動して[http://localhost:3000/users/sign_in](http://localhost:3000/users/sign_in)にアクセスし、実装されたか確認します。
##チャット機能の作成
###ControllerとModelを作成
```:bash
$ rails g controller rooms show
$ rails g model message content:text
```
###showアクションの追加
```rb:app/controllers/rooms_controller.rb
def show
@messages = Message.all
end
```
###message一覧を表示させるviewを作成
```html:app/views/rooms/show.html.erb
<h1>Chat room</h1>
<div id="messages">
<%= render @messages %>
</div>
```
###message1つ1つを表示するパーシャルを追加
```html:app/views/messages/_message.html.erb
<div class="messages">
<p><%= message.content %></p>
</div>
```
###投稿データの登録
```:bash
$ rails c
```
```rb:console
Message.create! content: "Hello"
```
[http://localhost:3000/rooms/show](http://localhost:3000/rooms/show)にアクセスし、投稿したデータが表示されるか確認
###Roomチャンネルの作成
```:bash
$ rails g channel room speak
```
ブラウザ側のコンソールを開いて確認(デベロッパー ツール)
```rb:console
App.room.speak()
=>true
```
speakアクションを定義する
```coffee:app/assets/javascripts/channels/room.coffee
speak: (message) ->
@perform 'speak', message: message
```
subscribedアクションのコメントアウトを外し、room_channelをstreamする
```rb:app/channel/room_channel.rb
class RoomChannel < ApplicationCable::Channel
def subscribed
stream_from "room_channel"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def speak(data) #サーバーサイドのspeakアクションの定義
ActionCable.server.broadcast 'room_channel', message: data['message']
end
end
```
データを受け取った際のアクション(アラート)を定義して動作確認
```coffeescript:app/assets/javascripts/channels/room.coffee
received: (data) ->
alert data['message']
```
サーバー起動、ブラウザ側コンソールを開いて確認
```rb:console
App.room.speak("test")
#アラートが表示されればOK
```
###フォームの作成
```html:app/views/rooms/show.html.erb
<form>
<label>Say something:</label><br>
<input type="text" data-behavior="room_speaker">
</form>
```
エンターキーを押した時の動作を定義
```coffee:app/assets/javascripts/channels/room.coffee
$(document).on 'keypress', '[data-behavior~=room_speaker]', (event) ->
if event.keyCode is 13 # return = send
App.room.speak event.target.value
event.target.value = ''
event.preventDefault()
```
サーバー起動、テキストボックスに文字を入力してエンターを押してアラートが出ればOK
###入力したものが保存されるように改善
speakアクションの書き換え
```rb:app/channels/room_channel.rb
def speak(data)
Message.create! content: data['message']
end
```
###Broadcastのjobを作成
```:bash
$ rails g job MessageBroadcast
```
作成されたjobファイルを編集し、ブロードキャスト処理を追加
```rb:app/jobs/message_broadcast_job.rb
def perform(message)
ActionCable.server.broadcast 'room_channel', message:
render_message(message)
end
private
def render_message(message)
ApplicationController.renderer.render(partial: 'messages/message', locals: {message: message})
end
```
###messageモデルを編集する
```rb:app/models/message.rb
class Message < ApplicationRecord
validates :content, presence: true #これにより無記入投稿とエンター長押しの連続投稿の2つが同時に防げる
after_create_commit {MessageBroadcastJob.perform_later self}
end
```
サーバー起動、文字入力後エンターを押し、入力した文字入りアラートが表示されればOK
###非同期で画面に入力された文字を表示させる
```coffee:app/assets/javascripts/channels/room.coffee
received: (data) ->
$('#messages').append data['message']
```
実際にメッセージを送信して確認
これでチャット機能は実装されました。
##複数のルームとメッセージの紐付け
ここからは先ほど作ったアプリを改良して、複数ルームのそれぞれのルームの接続者のみにチャットが見えるようにします。
###Roomモデルの作成
```:bash
$ rails g model room
```
###モデルの紐付け
Messageモデルに紐付けのためのカラムを追加します。
```:bash
$ rails g migration AddColumnsToMessages user:references room:referencese
$ rails db:migrate
```
UserモデルとRoomモデルに紐付けのためのhas_manyを追加
```rb:app/models/user.rb
class Room < ActiveRecord::Base
has_many :messages
end
```
```rb:app/models/room.rb
class Room < ActiveRecord::Base
has_many :messages
end
```
##接続ルームのメッセージのみが表示されるようControllerとViewを変更
###Controller
```rb:app/controllers/rooms_controller.rb
class RoomsController < ApplicationController
def show
@room = Room.find(params[:id])
@messages = @room.messages
end
end
```
###View
接続ルームに紐づいたメッセージを全て表示させるためのview
```html:app/views/rooms/show.html.erb
<div id="messages", data-room_id="<%= @room.id %>">
<%= render @messages %>
</div>
<div class="input">
<form>
<label>post</label>
<input type="text" data-behavior="room_speaker">
</form>
</div>
```
接続ルームに紐づいたメッセージ1つ1つを表示するパーシャル
```html:app/views/messages/_message.html.erb
<div class="message">
<p><%= "#{message.user.email}: #{message.content}" %></p>
</div>
```
##Room.idを受け取り、監視する場所を分ける
```coffee:app/assets/javascripts/channels/room.coffee
document.addEventListener 'turbolinks:load', ->
App.room = App.cable.subscriptions.create { channel: "RoomChannel", room_id: $('#messages').data('room_id') },
connected: ->
disconnected: ->
received: (data) ->
$('#messages').append data['message']
speak: (message)->
@perform 'speak', message: message
$(document).on 'keypress', '[data-behavior~=room_speaker]', (event) ->
if event.keyCode is 13 # return = send
App.room.speak event.target.value
event.target.value = ''
event.preventDefault()
```
```rb:app/channels/room_channel.rb
class RoomChannel < ApplicationCable::Channel
def subscribed
stream_from "room_channel_#{params['room_id']}"
end
def unsubscribed
# Any cleanup needed when channel is unsubscribed
end
def speak(data)
Message.create!(content: data['message'], user_id: current_user.id, room_id: params['room_id'])
end
end
```
##current_user使えない問題
Websocket側はモデルのインスタンスを参照できないからcurrent_userが使えない(だったかな)。
そこでcookieの`cookies.signed["user.id"]`からログイン中のユーザーのIDを抜き出してcurrent_userを作ろうって話。
###connection.rbに色々書いて設定
```rb: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: cookies.signed["user.id"])
return verified_user if verified_user && cookies.signed['user.expires_at'] > Time.now
reject_unauthorized_connection
end
end
end
```
##それでもcurrent_userが使えない問題
Deviseを使ってると`cookies.signed["user.id"]`にログイン中のユーザーのIDが格納されないらしい。詳しくはWardenを学ぼう。
---
[追記 '18.6.27]
どこにあるかというと
`cookies.encrypted[Rails.application.config.session_options[:key]][‘warden.user.user.key’][0][0]`
にある。よって、先ほどのファイルは
```rb: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
session_key = cookies.encrypted[Rails.application.config.session_options[:key]]
verified_id = session_key['warden.user.user.key'][0][0]
verified_user = User.find_by(id: verified_id)
return reject_unauthorized_connection unless verified_user
verified_user
end
end
end
```
に変更することでログインユーザーのID情報をもとにcurrent_userを定義できます。
もしくは↓でも可能。
---
仕方ないので`cookies.signed["user.id"]`にログイン中のユーザーのIDが格納されるようinitializers/warden_hooks.rbを追加して設定。
```rb:config/initializers/warden_hooks.rb
Warden::Manager.after_set_user do |user,auth,opts|
scope = opts[:scope]
auth.cookies.signed["#{scope}.id"] = user.id
auth.cookies.signed["#{scope}.expires_at"] = 30.minutes.from_now
end
Warden::Manager.before_logout do |user, auth, opts|
scope = opts[:scope]
auth.cookies.signed["#{scope}.id"] = nil
auth.cookies.signed["#{scope}.expires_at"] = nil
end
```
##完成
やったね。
あとは必要に応じてルームを作成するアクションやviewを作ってルーティング設定して、実際に動かして確認してみてね。
この記事はアプリ作ってしばらくして、思い出しながら書いたので間違いがあったらごめんなさい。
うまくいかない場合報告していただければ…。
おわり。