【Rails】Twitter APIとLINE APIでジャニーズの私物を特定して、すぐに購入できるサービスを作ってみた!!! PF [gem部分]

#はじめに
PFで**ジャニーズの私物を特定して、すぐに購入できるサービス**を作成したのでまとめます。
あまりにも長くなりそうだったので、4回に分けてまとめています。
今回が最後です使用した`gem`についてまとめます。

#サービスの概要

ジャニーズファンは推しと同じものを日常生活やライブで身につけたいという心理があります。
twitte上にファンの人が私物を特定してツイートをするので、そのツイートをキャッチして、LINEで通知・購入できるサービスになっています。

#なぜこのサービスを作成したか
私物の特定のツイートがされるとすぐファンが購入し、売り切れてしまうことがよくあります。
私自身、私物特定のツイートを発見し、サイトを検索してももう売り切れてしまっていたということが何回もあります。
他のファンよりも早く購入できるサービスがあればいいなと思い、このサービスを作成しました。

##完成したサービス
Webサイトは以下になります↓

https://www.findgoodsofstan.jp/

LINEのQRコードになります。
ぜひ動かしてみてください!!
<img width="150" alt="a163e5fa2b2098ef30926fb607faa8ff.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2133440/a14102d3-51c9-2d22-8d96-89396b192b6d.png">

サービスのイメージがしづらいと思いますがこんな流れになります
1.Twitterで私服特定ツイートがされます。
<img width="150" alt="S__37232668.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2133440/53b8694a-7863-28c2-f542-724b2666d617.jpeg">

2.LINEにこのような通知が届きます。
<img width="150" alt="S__37232670.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2133440/655e3495-461c-9893-14ef-fbb72d2fd061.jpeg">

3.`商品検索ボタン`から検索可能です。
<img width="150" alt="S__37232676.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2133440/0cac9a6b-b08e-177b-f1ba-8fc533464d59.jpeg">

4.`To tweetボタン`からソース元に遷移可能です。
<img width="150" alt="S__37232678aaa.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2133440/eb20d86e-88f7-1fb5-41fe-7d4415d933ab.jpeg">

**LINE友達登録後できること**
1.最新の情報を確認できます。
リッチメニュー右上の`最新の情報ボタン`を押す→情報を確認したい`メンバーを選択`する→`最新の情報`が表示される
<img width="150" alt="S__37232679ss.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2133440/8356275b-8543-573c-7977-8c0be57320cc.jpeg">

<img width="150" alt="S__37232680ss.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2133440/5fd3ae36-11f3-2ab2-dee2-a141e96f05eb.jpeg">

2.情報を受け取るメンバーを選択できます。
リッチメニュー左上の`メンバー設定を変更する`を選択する→設定した`いメンバーを選択`する→通知を`受け取る`か、`受け取らない`かを選択する

<img width="150" alt="S__37232682b.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2133440/4ff385cc-e26b-8c41-82b2-d8193d26bca5.jpeg">

<img width="150" alt="S__37232683b.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2133440/bccb87c7-e4d8-4915-8ead-9572dff76465.jpeg">

もちろん、現在の通知設定についてもリッチメニュー左下の`メンバー設定を確認する` から確認可能です。

<img width="150" alt="S__37232681b.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2133440/993e74a7-e425-3671-d2c8-32306ad510af.jpeg">


##使用した機能
Twitter API
LINE Messaging API
dotenv-rails'
config gem
ransack gem
sorcery gem
kaminari gem
enum_help gem
rails-i18n gem
AWS
heroku

##ER図
![3917056f5181a1ce73694684e344b358.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2133440/c94b651d-f7d3-2072-fdc5-fd44e049b93d.png)

https://drive.google.com/file/d/10gO9reC8SdPK9OE3ZZntfC9CflYIkJHv/view?usp=sharing


##画面遷移図

![d657ee3591bde2b85796fbee48567573.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2133440/26fd44d1-3ee1-0043-31c7-63dc3a8e16b1.png)

https://www.figma.com/file/pFrYU4hSwDQFn4da2XSTyh/Untitled?node-id=32%3A2


##他に使用しているgemについて

他にも今回導入しているgemがあるのでまとめていきます。

####`whenever gem`
```ruby:Gemfile
gem 'whenever', require: false
```
https://github.com/javan/whenever

`cron`の設定を簡単な文法で書けます。
定時実行の処理を`whenever`を使うと設定できます。
今回は、Twitterの検索をrakeタスクに作成して、定期的に私物に関するツイートがないか検索をかけました。

デプロイには`heroku`を使用しました。`heroku`は`whenever gem`に対応していないため、`scheduler`を使用して定期実行を行っています。

❶lib/tasks以下にcronを使用して、定期的に実行する処理のかたまり(rakeタスク)を作成します。
herokuでは10分に1回しか定期実行できない為、`3.times`や`sleep(150)`を使用して、時間の帳尻を合わせています。

```ruby:lib/tasks/make_goods_find_article.rake
namespace :make_goods_find_article do
  desc '「GoodsFind」のツイートが私物に関するものかを判別し私物に関するものであれば記事を作成する'
  task account_goods_find: :environment do
    3.times{
    #3回行っています。
      article = Article.new
      article.make_GoodsFind_article
      #article.rbで作成したmake_GoodsFind_articleメソッドを実行
      sleep(150)
      #150秒経ったら次の処理へ
    }
  end
end
```

❷`config/suchedule.rb`を作成して、定期実行したいタスク設定(実行の間隔、時間)を書き込んで反映させます。

以下のコマンドをアプリディレクトリ内で実行します。
実行すると、`config/suchedule.rb`が作成されます。

```
$ wheneverize . 
```

ここに定期実行したいタスク設定(実行の間隔、時間)を書き込んで反映させます。

```ruby:config/suchedule.rb
# Rails.rootを使用するために必要
require File.expand_path(File.dirname(__FILE__) + '/environment')
# cronを実行する環境変数
rails_env = ENV['RAILS_ENV'] || :development
# cronを実行する環境変数をセット
set :environment, rails_env
# cronのログの吐き出し場所
set :output, "#{Rails.root}/log/cron.log"
# 10分ごとに実行
every 10.minute do
  rake "make_goods_find_article:account_goods_find"
end
```

❸設定を反映させます。

設定内容にエラーがないか確認

```ruby
$ bundle exec whenever
``` 

cronにデータを反映させるコマンド

```
 $ bundle exec whenever --update-crontab
```



参照(開発中はこの記事を参考にして、定期実行を行いました。)

https://qiita.com/hirotakasasaki/items/3b31966294a809b99c4c

https://boku-boc.hatenablog.com/entry/2021/02/13/152217

`heroku`の`scheduler`を使用し定期実行した方法はこちらの記事にまとめてあります。

https://mmm-st.hatenablog.com/entry/2021/10/05/154927

https://mmm-st.hatenablog.com/entry/2021/11/08/152009

####`ransack gem`

```ruby:Gemfile
# 検索
gem 'ransack'
```
検索部分をこのgemで作成しました。

https://github.com/activerecord-hackery/ransack

このような感じで、`ブランド名`と`メンバー名`から検索できるように作成しました。
![7b856537485694627ffe20df5db41cb3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2133440/d691f70c-7cbb-96da-7d38-22a4e2245ccf.png)

今回は、article一覧ページのヘッダーに検索フォームを作成したいです。

❶まずメソッドを確認します。

`params[:q]` → この後に作成するビューファイルから送られてくるパラメーターです。

`ransack`メソッド → 送られてきたパラメーターを元にテーブルからデータを検索するメソッドです。
(whereメソッドのransack版というイメージです。)

`result`メソッド → ransackメソッドで取得したデータをActiveRecord_Relationのオブジェクトに変換するメソッドです。

https://pikawaka.com/rails/ransack

❸controllerを確認します。今回はヘッダー部分に検索フォームを作成したいので、`application_controller.rb`と`controllers/articles_controller.rb`と`controllers/admins/articles_controller.rb`に作成します。

共通で使用するメソッドを作成します。

```ruby:application_controller.rb
class ApplicationController < ActionController::Base

  def set_item_search
    @q = Article.ransack(params[:q])
  #ransackメソッドを使用しparams[:q]でviewから送られてくるパラメーターを元にテーブルからデータを検索する。
    @set_items = @q.result
#ransackメソッドで取得したデータをActiveRecord_Relationのオブジェクトに変換する
  end
end
```
```ruby:controllers/articles_controller.rb
def index
  @articles = @set_items.includes(:members).published.order(created_at: :desc).page(params[:page]).per(6)
end
```

```ruby:controllers/admins/articles_controller.rb
def index
  @articles = @set_items.includes(:members).order(created_at: :desc).page(params[:page])
end
```

❸検索フォームのviewを確認します。

共通で使用する検索フォームはパーシャルにしました。

```ruby:views/layouts/_search.html.erb
<div class = "search mx-auto py-4">
  <%= search_form_for @q do |f| %> 
    <%= f.search_field :brand_cont, class: " search_btn btn-outline-dark", placeholder: t("activerecord.attributes.article.brand") %> 
    <%= f.search_field :members_name_cont, class: " search_btn btn-outline-dark", placeholder: Member.model_name.human %> 
    <%= f.submit 'search', class: "search_btn  btn-outline-dark"%>
  <% end %>
</div>
```

ヘッダーでそのときのpathに合わせて表示する形にしました。

```ruby:views/layouts/_header.html.erb
<% if current_page?(articles_path) %>
  <%= render partial: 'layouts/search', url: articles_path %>
<% elsif current_page?(admins_articles_path) %>
  <%= render partial: 'layouts/search', url: admins_articles_path %>
<% end %> 
```



####`sorcery gem`

```ruby:Gemfile
# ログイン
gem 'sorcery'
```
認証のための最小限のロジックを積んだgemで、ユーザーの必要に応じてメソッドを増やしたり拡張をしていくことを目的としています。

https://github.com/Sorcery/sorcery

以下の点を考えて今回は`sorcery`を使用しました。
使いたいのはログイン機能のみです。

>deviseに比べて重くない
カスタマイズしやすい。

https://www.for-engineer.life/entry/rails-sorcery/

❶コマンドを実行し必要なファイルを生成します。

```
$ rails generate sorcery:install
```
↓のファイルが生成されます。

```
create  app/models/user.rb
create  db/migrate/XXXXXXXXX_sorcery_core.rb
```
(1)db/migrate/XXXXXXXXX_sorcery_core.rbについて

```ruby:db/migrate/XXXXXXXXX_sorcery_core.rb
class SorceryCore < ActiveRecord::Migration[5.2]
  def change
    create_table :users do |t|
      t.string :email, null: false
      t.string :crypted_password
      t.string :salt
      t.integer :role, default: 0

      t.timestamps
    end
  end
end
```
`salt`について

>標準の暗号化では、「ソルト」を使用して、パスワードハッシュの安全性を高めます。Sorceryでは、パスワードの末尾にランダムな文字列を結合し、その文字列をソルトフィールドに記憶することでこれを行います。

https://stackoverflow.com/questions/52419857/what-salt-is-used-for-in-sorcery-gem/52420917#52420917

`$ rails db:migrate`で反映させます。

今回は、ユーザー作成ページは作成しませんでした。
管理者のみ必要なので、一般ユーザーはログインせずに使用できる流れになります。

なので、ログイン用のviewやcontrollerのみ作成しています。

####ログイン用のviewやcontroller

ログイン・ログアウト機能の実装についてです。sorceryで提供しているメソッドについては、[公式のこちら](https://github.com/Sorcery/sorcery#api-summary)のに記述があります。

loginメソッドは、email、password、remenber_me(デフォルト値はfalse)の３つの引数を取ることができると分かります。
今回はemail、passwordを使用してログインさせます。

```ruby:controllers/admins/sessions_controller.rb
class Admins::SessionsController < ApplicationController
  skip_before_action :require_login, only: %i[create new]

  def new;end

  def create
    @user = login(params[:email], params[:password])
    if @user
      redirect_back_or_to admins_articles_path, success: t('flash.login')
    else
      flash.now[:danger] = t('flash.not_login')
      render :new
    end
  end

  def destroy
    logout
    redirect_back_or_to admins_login_path, success: t('flash.logout')
  end
end
```

ここの`require_login`について

```ruby:controllers/admins/sessions_controller.rb
skip_before_action :require_login, only: %i[create new]
```
`require_login`メソッドはSorceryで提供されているメソッドです。
ログインしていないユーザーをアクション単位で弾き、`not_authenticated`メソッドを発火します。

`not_authenticated`はSorceryで提供されているメソッドです。
デフォルトではredirect_to root_pathが定義されています。
自分で変更したい際には、`application_controller.rb`で記述します。



```ruby:controllers/application_controller.rb
class ApplicationController < ActionController::Base
  add_flash_types :success, :info, :warning, :danger
  #add_flash_typesメソッドで指定したキーがredirect時のflashで使えるようになる。
  #通常はnoticeとalertだけだが、任意のキーを指定できるようになる。
  before_action :require_login

  private

  def not_authenticated
    redirect_to admins_login_path, danger: t('flash.before_login')
  end
end
```

それぞれまとめているページはこちらになります。

https://mmm-st.hatenablog.com/entry/2021/11/05/113932?_ga=2.76800085.1343198345.1638342236-1111946945.1605581552

https://mmm-st.hatenablog.com/entry/2021/11/08/140503

####`rails-i18n gem`

```ruby:Gemfile
# ja.yml
gem 'rails-i18n'
```
デフォルトの言語を日本語に設定します。
`config/locales/ja.yml`を作成し、日本語に対応させます。

https://github.com/svenfuchs/rails-i18n

❶`config/application.rb`の設定を変更します。

```ruby:config/application.rb
config.i18n.default_locale = :ja 
#デフォルトの言語を日本語(ja)にします。
config.i18n.load_path += Dir[Rails.root.join('config', 'locales', '**', '*.{rb,yml}').to_s]
#i18nの複数ロケールファイルが読み込まれるようpathを通します。
```

❷`config/locales`以下にロケールファイルを配置し、日本語を設定します。

```ruby:config/locales/activerecord/ja.yml
ja:
  activerecord:
    models:
      member: 'Member'
      report: 'REPORT'
    attributes:
        article:
          brand: 'Brand'
          member: 'メンバー'
          twitter_url: 'TwitterへのURL'
        member:
          email: 'メールアドレス'
          password: 'パスワード'
```
https://qiita.com/shimadama/items/7e5c3d75c9a9f51abdd5

####`enum_help gem`

```ruby:Gemfile
# enum
gem 'enum_help'
```
enumで定義した値をi18n化させることができます。
i18n対応セレクトボックスを作成することも可能です。

https://github.com/zmbacker/enum_help

#####❶enumで定義した値をi18n化はこのように設定し、使用します。

```ruby:config/locales/activerecord/ja.yml
ja:
  enums:
    user:
      role:
        other: '一般'
        admin: '管理者'
```

#####❷今回はi18n対応セレクトボックスも作成、使用しています。
以下の形で、編集ページで`公開`か`非公開`かを選択できるようにしています。

![af56edcf1afe32f5624b971ce14e0c83.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2133440/f4994fec-6945-709f-e4e6-021a6d8f84fd.png)

```ruby:views/admins/articles/edit.html.erb
<%= form_with model: @article, url: admins_article_path(@article.id), local: true do |f| %>
#省略
<%= f.select :status, Article.statuses_i18n.invert %>
#省略
<% end %>
```

ポイントはセレクトボックスの値です。

```
select(オブジェクト名, メソッド名, 要素(配列 or ハッシュ) [, オプション or HTML属性 or イベント属性])
```
https://railsdoc.com/page/select

とすればいいので、以下のようにすれば動きます。

```ruby
<%= f.select :status,[["公開中", "published"], ["非公開", "draft"]] %>
# f.select :プロパティ名,[["表示される文字", "渡される値"], ["表示される文字", "渡される値"]]
```
ですが、`enum_help`を使用するともっと簡単に書けますし、`enum`に変更があったとしてもコードを変更する必要はありません。
↓このように書くことができます。

```ruby
<%= f.select :status, Article.statuses_i18n.invert %>
```

上で見た以下のコードは以下のような構成になっていたので、

```
f.select :プロパティ名,[["表示される文字", "渡される値"], ["表示される文字", "渡される値"]]
```

`enum_help`を使用して`[[表示される選択肢, 渡される値], [表示される選択肢, 渡される値]]`という形にして渡すには...

```ruby:ターミナル
irb(main):012:0> Article.statuses
=> {"published"=>0, "draft"=>1}

irb(main):013:0> Article.statuses_i18n
=> {"published"=>"公開中", "draft"=>"非公開"}

irb(main):014:0> Article.statuses_i18n.map { |k, v| [v, k] }
=> [["公開中", "published"], ["非公開", "draft"]]

irb(main):015:0>Article.statuses_i18n.invert 
#値からキーへのハッシュを作成
=> {"公開中"=>"published", "非公開"=>"draft"}
```

`invert`メソッド
>値からキーへのハッシュを作成して返します。

https://docs.ruby-lang.org/ja/latest/method/Hash/i/invert.html

```
select(オブジェクト名, メソッド名, 要素(配列 or ハッシュ) [, オプション or HTML属性 or イベント属性])
```
の通り、**配列 or ハッシュ**を渡せばいいので、以下の形で同じように動きます!もし並列にしたければ`to_a`メソッドを最後に使用すれば配列になりますが、必要ないので使用しません。

```ruby
Article.statuses_i18n.invert 
#値からキーへのハッシュを作成
=> {"公開中"=>"published", "非公開"=>"draft"}
```

https://qiita.com/aiandrox/items/3fedb826dd870929c3f7

https://qiita.com/kikikikimorimori/items/353f69e31b42e85b9c29

####`kaminari gem`

```ruby:Gemfile
# ページネーション
gem 'kaminari'
```

1つのページに表示する記事の数を指定し、複数ページに分けて表示させるようにするgemです。
今回は1ページに8つの記事を表示させています。
8個以上の記事がある場合は以下を表示し、別ページに表示されています。

![91b35ef89d846c4abdaf01a9e88b6a1d.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2133440/f9f7eb6f-86b6-f2fc-3b45-0a8b6d530d32.png)

❶設定ファイルを作成し、デフォルトの表示数を設定します。

```
$ rails g kaminari:config
```
`config/initializers/kaminari_config.rb`が作成されます。

ここで、表示数を指定します。デフォルトは25が設定されています。
今回は8で設定します。

```ruby:config/initializers/kaminari_config.rb
Kaminari.configure do |config|
  config.default_per_page = 8
#省略
end
```

❷controllerで表示数などを指定します。

デフォルトで8を指定したので、8こ表示したい場合は記載は必要ないですが、この一覧部分は6個を指定したかったので、`.per(6)`を使用して数を指定しています。

```ruby:controllers/articles_controller.rb
def index
  @articles = @set_items.includes(:members).published.order(created_at: :desc).page(params[:page]).per(6)
end
```

❸view部分を指定します。
❷のコントローラー、アクションでレンダリングされるページを例にすると...
以下を記載するだけで表示されます。

```ruby:views/articles/index.html.erb
<%= paginate @articles %>
```

❹デザインを変更します。bootstrap4を導入し、自分で後から少し変更しています。

```
$ rails g kaminari:views bootstrap4
```

https://github.com/kaminari/kaminari

https://qiita.com/ryota21/items/29fa282745afb1474059

https://qiita.com/residenti/items/1ae1e5ceb59c0729c0b9

https://qiita.com/chimeiwang/items/3735336d51e6012cc265


#####`aws-sdk-s3 gem`

```ruby:Gemfile
gem "aws-sdk-s3", require: false
```
https://github.com/aws/aws-sdk-ruby

Heroku には `“一時的” ハードドライブ`があります。これは、ファイルをディスクに書き込むことはできますが、アプリケーションを再起動するとそれらのファイルは失われてしまいます。

activestorageを使用すると、しばらくは問題ありませんが、添付ファイルは表示動作に支障をきたすようになり、最終的には消えてしまいます。

それが起きないように、アップロードしたファイルをディスクに保存する代わりに、クラウドファイルストレージサービスを使用します。

それに使用するのがこの`aws-sdk-s3 gem`です

https://devcenter.heroku.com/ja/articles/active-storage-on-heroku


#####`config gem`

https://github.com/rubyconfig/config

定数管理を行います。
今回は、Twitterのアカウントidなどをここで管理しました。