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

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

ということで、外部サービスいいのないかなーと思って調べたところ、[Pusher](https://pusher.com/)にゆきつきました。

昔からあるサービスです。しかし、[公式でRubyの事例がある](https://pusher.com/tutorials?tag=Ruby)にもかかわらず、ネット上でRailsの事例をみません。private channelを使った事例は特に。

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

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

# 使用イメージ
![Kapture 2021-03-21 at 18.12.13.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124445/405718a7-57df-7ce3-0f38-aac0f1e7a595.gif)

## 使用手順
PUSHERのアカウントを作成

https://pusher.com/

PUSHERの App keysで情報を入寮

`.env_sample.env` ファイルを `.env` ファイルにコピーして、PUSHERのApp keys の情報を入力する。

インストール

```console
rails db:migrate
rails db:seed
rails s
```

以下にアクセスする
http://localhost:3000

ログインする。ブラウザを2つ開いて、それぞれ違うユーザーでログインして下さい。

ついになるようにチャットの相手を選んで下さい。（上述の動画参照のこと）

# 環境
* Rubyは 3.0.0
* dbは sqlite3
* Rails は 6.1


# コードはこちら
https://github.com/junara/sandbox_pusher_private_channel

Pusherを使うために追加したコードに集中して読みたい場合は以下のPRを参照下さい。

https://github.com/junara/sandbox_pusher_private_channel/pull/1


# 説明
* 擬似ログインを作る
* 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です。とくに説明ないです。リンク貼っておきます。

https://github.com/bkeepers/dotenv#rails


## Pusherにsign inして環境変数をセットする

[Pusher](https://pusher.com/)です。アカウントを持っていない人はつくってください。 sign upしてください。

App keysのメニューに進んで、下図の情報を .envに転記して下さい。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/124445/7bd628f1-a120-6e36-15c8-c2b34a628a3c.png)

（下記は例です・）

```.env
PUSHER_APP_ID = 1234567
PUSHER_KEY = 77777777777777777777
PUSHER_SECRET = 77777777777777777777
PUSHER_CLUSTER = ap3

```

## Pusherのprivateチャネルをsubscribeする

Pusher の [Private channels](https://pusher.com/docs/channels/using_channels/private-channels)to[Authenticating users](https://pusher.com/docs/channels/server_api/authenticating-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 %>

```

上記のposｔを下記の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の有料プランををつかってもいいのかなあーと思いました。

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