概要
Railsでチャットを作るには、ActionCableが第1候補です。しかし、スケールしたときに、ActionCableを自前のサーバーで面倒見るのはちょっと心配。
ということで、外部サービスいいのないかなーと思って調べたところ、Pusherにゆきつきました。
昔からあるサービスです。しかし、公式でRubyの事例があるにもかかわらず、ネット上でRailsの事例をみません。private channelを使った事例は特に。
なお、Laravelでは事例をよく見ますし、すでに語り尽くされたのかも知れません。
が、僕は初めてだったので、改めてRailsで作りにはどうすればいいかといことで、やったのでその記録をのせます。
使用イメージ
使用手順
PUSHERのアカウントを作成
PUSHERの App keysで情報を入寮
.env_sample.env
ファイルを .env
ファイルにコピーして、PUSHERのApp keys の情報を入力する。
インストール
rails db:migrate
rails db:seed
rails s
以下にアクセスする
http://localhost:3000
ログインする。ブラウザを2つ開いて、それぞれ違うユーザーでログインして下さい。
ついになるようにチャットの相手を選んで下さい。(上述の動画参照のこと)
環境
- Rubyは 3.0.0
- dbは sqlite3
- Rails は 6.1
コードはこちら
Pusherを使うために追加したコードに集中して読みたい場合は以下のPRを参照下さい。
説明
- 擬似ログインを作る
- dot-envで設定を管理できるようにする
- Pusherにsign inして環境変数をセットする
- Pusherのprivateチャネルをsubscribeする
- チャットをできるようにする
擬似ログインを作る
本質では無いですが、ログインできた方がイメージが作りやすいように、擬似ログインを作りました。
特定のユーザーのuser_idをsessionに login_user_idとして保存します。
それにより、 current_user
authentificate_user!
のおなじみのメソッドを定義して参照できるようにしました。
なので、deviseを使っているユーザであればとくやらないでもよいかと。
なお、作ってみるとこれ、以外と使い出があって個人的には満足しています。いろんなところで応用ききそうなので。
それっぽい、endpointを定義。
get 'sign_in', to: 'sessions#new'
post 'sign_in', to: 'sessions#create'
get 'sign_out', to: 'sessions#destroy'
それっぽいsessions_controllerを定義。
class SessionsController < ApplicationController
def new
@users = User.all
end
def create
@user = User.find(user_params[:id])
session[:login_user_id] = @user.id
redirect_to root_path
end
def destroy
session[:login_user_id] = nil
redirect_to root_path
end
private
def user_params
params.require(:user).permit(:id)
end
end
dot-envで設定を管理できるようにする
いつものあれです。dot-envです。とくに説明ないです。リンク貼っておきます。
Pusherにsign inして環境変数をセットする
Pusherです。アカウントを持っていない人はつくってください。 sign upしてください。
App keysのメニューに進んで、下図の情報を .envに転記して下さい。
(下記は例です・)
PUSHER_APP_ID = 1234567
PUSHER_KEY = 77777777777777777777
PUSHER_SECRET = 77777777777777777777
PUSHER_CLUSTER = ap3
Pusherのprivateチャネルをsubscribeする
Pusher の Private channelstoAuthenticating usersが、非常に参考になります。これがあればOK。
該当のコードを下記にはります。
# frozen_string_literal: true
class PusherController < ApplicationController
before_action :authenticate_user!
def auth
if current_user
response = Pusher.authenticate(private_channel_name(current_user.id), params[:socket_id], {
user_id: current_user.id # => required
})
render json: response
else
render text: 'Forbidden', status: '403'
end
end
end
特に重要なのは、 private_channel_name(current_user.id)
private channel。
ここでユーザー毎のprivateチャネルを用意します。
ここで用意したチャネルはprivateになります。他のユーザーはsubscribeできません。
def private_channel_name(user_id)
"private-channel_user_id_#{user_id}"
end
privateチャネルの subscribeは下記です。 740c9cda57f8f6c66b27'
ここには各自の app_key を入力して下さい。(secret_keyではないので間違えないように!)
'X-CSRF-Token': "<%= form_authenticity_token %>"
はCSRF tokenが入ります。
let pusher = new Pusher('<%= ENV['PUSHER_KEY'] %>', {
authEndpoint: '/pusher/auth,
cluster: 'ap3',
encrypted: true,
auth: {
headers: {
'X-CSRF-Token': "<%= form_authenticity_token %>"
}
}
});
let channel = pusher.subscribe("<%= @private_channel_name %>");
チャットをできるようにする
チャットの送信は下記です。普通にpostのフォームです。 local: false, remote: true
が特徴です。
<%= form_with model: @chat, url: chats_path(with_user_id: @received_user.id), local: false, remote: true, id: 'chat-form' do |f| %>
<%= f.text_field :message, autocomplete: 'off' %>
<% end %>
上記のpostを下記のcreateで処理します。
def create
@user = current_user
@received_user = User.find(params[:with_user_id])
@chat = Chat.new(user: @user, received_user: @received_user, message: chat_params[:message])
if @chat.save!
data = {
id: @chat.id,
message: @chat.message,
user_name: @chat.user.name,
user_id: @chat.user.id,
created_at: @chat.created_at.to_s
}
Pusher.trigger_batch(
[
{ channel: private_channel_name(@user.id), name: event_name(@received_user.id), data: data },
{ channel: private_channel_name(@received_user.id), name: event_name(@user.id), data: data }
]
)
else
# なにかエラー処理
end
end
重要なのは下記です。ここで、チャット送信者と受信者の両方のprivate チャネルへ通知をしています。
Pusher.trigger_batch(
[
{ channel: private_channel_name(@user.id), name: event_name(@received_user.id), data: data },
{ channel: private_channel_name(@received_user.id), name: event_name(@user.id), data: data }
]
)
上記を通知を下記で受信して、画面の描画を更新します。
event_nameの箇所には、やりとりしているユーザー独自のイベント名が入ります。
channel.bind("<%= @event_name %>", function (data) {
let chat_message = data.message;
let chat_created_at = data.created_at;
let chat_user_name = data.user_name;
let chat_user_id = data.user_id;
let new_content = document.createElement('div');
new_content.innerHTML = `
<div>
<span>「送信者」id=${chat_user_id} の ${chat_user_name}</span>
<span>「送信日時」${chat_created_at}</span>
<span>「メッセージ」${chat_message}</span>
</div>
`
let post_section_div = document.getElementById('post_section');
post_section_div.appendChild(new_content);
let post_input = document.getElementById('chat_message');
post_input.value = '';
});
event_nameはこんな感じ。user_idにはやりとりしている相手のuser_idがはいります。
def event_name(user_id)
"new_post_with_user_id_#{user_id}"
end
チャット末尾に追加する。
let post_section_div = document.getElementById('post_section');
post_section_div.appendChild(new_content);
所感
使いやすいです。かなり簡単です。
ActionCableを使う時は Redisとセットだったりすることを考えると、Pusherの有料プランををつかってもいいのかなあーと思いました。
サーバーの心配せずに夜ぐっすり眠れそうなので。