LoginSignup
3
1

More than 3 years have passed since last update.

PusherとRailsでprivate channelの通知を使ってチャットを作ってみた

Last updated at Posted at 2021-03-21

概要

Railsでチャットを作るには、ActionCableが第1候補です。しかし、スケールしたときに、ActionCableを自前のサーバーで面倒見るのはちょっと心配。

ということで、外部サービスいいのないかなーと思って調べたところ、Pusherにゆきつきました。

昔からあるサービスです。しかし、公式でRubyの事例があるにもかかわらず、ネット上でRailsの事例をみません。private channelを使った事例は特に。

なお、Laravelでは事例をよく見ますし、すでに語り尽くされたのかも知れません。

が、僕は初めてだったので、改めてRailsで作りにはどうすればいいかといことで、やったのでその記録をのせます。

使用イメージ

Kapture 2021-03-21 at 18.12.13.gif

使用手順

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を定義。

routes.rb
  get 'sign_in', to: 'sessions#new'
  post 'sign_in', to: 'sessions#create'
  get 'sign_out', to: 'sessions#destroy'

それっぽいsessions_controllerを定義。

app/controllers/sessions_controller.rb
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に転記して下さい。

image.png

(下記は例です・)

PUSHER_APP_ID = 1234567
PUSHER_KEY = 77777777777777777777
PUSHER_SECRET = 77777777777777777777
PUSHER_CLUSTER = ap3

Pusherのprivateチャネルをsubscribeする

Pusher の Private channelstoAuthenticating usersが、非常に参考になります。これがあればOK。

該当のコードを下記にはります。

app/controllers/pusher_controller.rb
# 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できません。

app/controllers/application_controller.rb
  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が入ります。

app/views/chats/index.html.erb
    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 が特徴です。

app/views/chats/index.html.erb
  <%= 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で処理します。

app/controllers/chats_controller.rb
  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 チャネルへ通知をしています。

app/controllers/chats_controller.rb
 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の箇所には、やりとりしているユーザー独自のイベント名が入ります。

app/views/chats/index.html.erb
    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がはいります。

app/controllers/application_controller.rb
  def event_name(user_id)
    "new_post_with_user_id_#{user_id}"
  end

チャット末尾に追加する。

app/views/chats/index.html.erb
        let post_section_div = document.getElementById('post_section');
        post_section_div.appendChild(new_content);

所感

使いやすいです。かなり簡単です。

ActionCableを使う時は Redisとセットだったりすることを考えると、Pusherの有料プランををつかってもいいのかなあーと思いました。

サーバーの心配せずに夜ぐっすり眠れそうなので。

3
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
1