【個人開発】LINEMessagingAPIを使って無差別に飯テロできるアプリを作りました

# はじめに
こんにちは、すずゆーと申します。

突然ですが皆様は飯テロをしたことはありますでしょうか。
私はSNSに美味しいご飯をアップするのが大好きです。
アップした写真に対してフォロワーさんからの反応を楽しむのが大好きです。

しかし、SNSだとせっかくアップしても本当に見てもらいたい人に見てもらえない時があったり...
そんな中、LINEなら確実に相手に届くし見てもらえるじゃん！ということで私の悪戯心を具現化したアプリを開発しました。


## 記事を読む上での注意点
当記事はプログラミング学習を開始して日の浅い業界未経験者が書いたものになりますので、技術的な内容などは誤りを含む可能性があります。予めご了承ください。


# サービス名

### 飯テログ
URL: https://meshitelog.com
GitHub: https://github.com/suzuyu0115/meshitelog
![placeholder.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3493892/fce41a92-ab9f-5a33-90e0-6d39345d4cc1.png)

飯テログは、LINEAPIを通じて好きな飯画像を好きな時間に好きな相手に飯テロできる、
ユーザー投稿型の飯テロサービスです。
LINEベースのアプリケーションのため、スマホからの使用をおすすめしております。


# 開発背景
私は一般男性と比較すると痩せ型体型にあり、もっと太りたいという願望を持っていました。
しかし、なかなか食欲が湧かず、太りたくても太れないという状況にありました。
そこで、食欲を促進してくれるサービスがあればいいなと考え、当アプリの開発に至りました。

飯テロは、「第三者から、不意に食欲を刺激させる」ということが必要になるため、多くの人が頻繁に利用するツールであるLINEで通知が届くようにし、飯テロを避けられないように工夫しました。また、飯テロの投稿が届く時間を指定できたり、送信相手をランダムに設定できる機能なども実装しております。


# サービス概要

## LINEログイン機能
ログイン機能にはLIFF(LINE Front-end Framework)を使用しております。
飯テログ公式アカウントに友達追加をしていただき、あいさつメッセージのURLを踏んでいただくだけでユーザー登録が完結します。

↓トップ画面の友だち追加ボタンから公式アカウントを友だち追加
![topp.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3493892/dd216e18-d1ab-1719-c5e4-4c9608cf4613.png)
↓メッセージのURLを押下することで登録完了！
![aisatu.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3493892/9a979058-076d-d6f5-94e2-960b1a68604c.png)

### LIFFとは
LIFFとは、LINE Front-end Frameworkの頭文字を取ったもので、LINEが提供するウェブアプリのプラットフォームになります。
> LIFFとはLINEアプリ上で動作するブラウザのことです。
LINEのトーク画面でURLを開くと、ChromeやSafariなどの外部ブラウザが開きますが、LIFFアプリのURLを開くと、LINEアプリ内部でブラウザが開きます。
またLIFFアプリは、他のブラウザで使用することもでき、登録しているメールアドレスやユーザーIDなど、LINEの情報を取得できるアプリとなります。

こちらでは細かい解説は省きますが、下記の記事がとても参考になりましのでLIFFに興味を持たれた方は拝見してみてください。

https://qiita.com/mayuyuyuyunn/items/fa82cd1a724c8007f384

## 飯一覧機能
飯一覧では、ユーザーの飯投稿が表示されます。
新着順、人気順のソートの他、あなたへのおすすめでは各ユーザーそれぞれに適したおすすめの飯がレコメンドされます。
![index.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3493892/3f7cd4d0-ecba-f9e8-f7fa-7a647a042c06.png)

### レコメンド機能について
```app/models/user.rb
class User < ApplicationRecord
...

  def similar_users
    # このユーザーがいいねした投稿をいいねしているユーザーを取得
    user_ids = Bookmark.where(post_id: bookmark_posts.ids).pluck(:user_id)
    User.where(id: user_ids)
  end

  def recommended_posts
    # 類似ユーザーがいいねした投稿の中から、このユーザーがまだいいねしていない投稿を取得
    post_ids = Bookmark.where(user_id: similar_users.ids).pluck(:post_id)
    Post.where(id: post_ids).where.not(id: bookmark_posts.ids)
  end
end
```
レコメンド機能は、自分と同じ投稿をいいねしているユーザーのいいね履歴を参照し、その履歴の中から自身がいいねしていない投稿を表示させるロジックを組んでいます。

## 飯投稿機能
飯名、コメント、飯画像を添えて飯投稿をすることができます。
投稿フォームにはモデルバリデーションの他にJSを用いたフロントのバリデーションを施し、飯名、コメントを記載しないと投稿ボタンが押下できない仕様にしています。
[![Image from Gyazo](https://i.gyazo.com/962125324b42cd17762e54b30f6c6019.gif)](https://gyazo.com/962125324b42cd17762e54b30f6c6019)

## 飯テロ機能
飯テロを送りたいユーザーを選択することで、そのユーザーのLINEに向けて飯テロをすることができます。
送り相手は任意に複数人選択できるほか、全ユーザー、ランダムなユーザー10人に向けて飯テロすることができます。
また、投稿日時を指定することで相手に届く時間を指定できるため、お腹の空いている深夜帯などの時間目掛けての飯テロも可能です。
[![Image from Gyazo](https://i.gyazo.com/b3f2efb2a19d898094d520062aad7667.png)](https://gyazo.com/b3f2efb2a19d898094d520062aad7667)

**送り相手にはFlexMwssage形式に整形されて飯テロが送信されます。**
![deliveries.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3493892/d26924bd-c122-674d-7dd9-47db10c7c3a4.jpeg)

### FlexMessageとは
LINEMessagingAPIのフォーマットの一つで、送信メッセージのレイアウトを自由にカスタマイズすることができる機能です。
> Flex Messageは、通常のLINEメッセージに比べ、より豊かでインタラクティブなレイアウトが可能なメッセージです。通常のLINEメッセージでは、テキスト、画像、動画など1種類のソースしか配信されません。しかし、Flex Messageでは、CSS Flexible Box（CSS Flexbox） (opens new window)の仕様に基づいて、レイアウトを自由にカスタマイズできます。

以下のリンクからFlexMessageのシミュレーションができます。(LINE Developersコンソールへのログインが必要です)
https://developers.line.biz/flex-simulator/

## 検索機能
飯名とタグ名から投稿の検索ができます。
検索のユーザビリティ向上のため、飯名検索にはインクリメンタルサーチ、タグ名検索フォームはオートコンプリートを搭載しました。
[![Image from Gyazo](https://i.gyazo.com/aef380d73195b3fbeb9a3e7739f0eab7.gif)](https://gyazo.com/aef380d73195b3fbeb9a3e7739f0eab7)

## X(旧Twitter)シェア機能
投稿されている飯は、Xシェアボタンからシェアが可能です。
OGP画像は動的に生成されるようにしているため、投稿の飯画像がOGP画像としてそのまま表示されます。
そのため、飯テログに登録していない不特定多数に向けて視覚的な飯テロを行うこともできます。
![shareのコピー.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3493892/908abc53-a039-ed70-2950-3afca450032b.jpeg)

# 苦労した点
## 送信相手の選択とLINEへの紐付け
送信相手を指定しての投稿、且つその送信相手へLINE通知として送信するロジックの実装が個人的には苦労したポイントでしたので、備忘録を兼ねて簡単に記載します。

### 1.中間テーブルの作成
まずは、UsersテーブルとPostsテーブルの中間テーブル`deliveries`テーブルを作成し、多対多のアソシエーションを敷きました。
こちらを追加することで投稿を`Deliveries`を通じて複数のユーザーに向けて送信できるようにしました。
```db/migrate/20230712145931_create_deliveries.rb
class CreateDeliveries < ActiveRecord::Migration[7.0]
  def change
    create_table :deliveries do |t|
      t.references :user, null: false, foreign_key: true
      t.references :post, null: false, foreign_key: true

      t.timestamps
    end
  end
end
```
```
$ bundle exec rails db:migrate
```

### 2.LINE Developersへの登録
LINEのAPIを使用するためには、LINEDevelopersへの登録しました。
参考記事が多く、冗長になってしまうため具体的な登録方法等は割愛します。
https://developers.line.biz/ja/

### 3.FlexMessageの構築
```app/models/post.rb
  # nicknameがあればnicknameを、なければnameを返す
  def sender_name
    user.nickname || user.name
  end

  # 各投稿詳細ページのURLを返す
  def post_url
    "#{ROOT_URL}/posts/#{id}"
  end

  def notify_line
    # 予約投稿か否かを判定
    return unless published?

    line_client = LineClient.new
    flex_contents = {
      type: "bubble",
      header: {
        type: 'box',
        layout: 'vertical',
        contents: [
          {
            type: 'text',
            text: "飯が届きました！",
            weight: "bold",
            size: "xl",
            wrap: true
          }
        ]
      },
      hero: {
        type: "image",
        url: food_image.url,
        size: "full",
        aspectRatio: "20:13",
        aspectMode: "cover",
        action: {
          type: "uri",
          uri: post_url
        }
      },
      body: {
        type: "box",
        layout: "vertical",
        contents: [
          {
            type: "text",
            text: title,
            weight: "bold",
            size: "xl",
            wrap: true
          },
          {
            type: "box",
            layout: "vertical",
            margin: "lg",
            spacing: "sm",
            contents: [
              {
                type: "box",
                layout: "baseline",
                spacing: "sm",
                contents: [
                  {
                    type: "text",
                    text: "#{sender_name} さんより",
                    wrap: true,
                    color: "#666666",
                    size: "sm",
                    flex: 5
                  }
                ]
              },
              {
                type: "box",
                layout: "baseline",
                spacing: "sm",
                contents: [
                  {
                    type: "text",
                    text: content,
                    wrap: true,
                    color: "#666666",
                    size: "md",
                    flex: 5
                  }
                ]
              }
            ]
          }
        ]
      },
      footer: {
        type: "box",
        layout: "vertical",
        spacing: "sm",
        contents: [
          {
            type: "button",
            style: "link",
            height: "sm",
            action: {
              type: "uri",
              label: "詳細を見る",
              uri: post_url
            }
          },
          {
            type: "box",
            layout: "vertical",
            contents: [],
            margin: "sm"
          }
        ],
        flex: 0
      }
    }

    deliveries.each do |delivery|
      line_client.push_flex_message(delivery.user.line_user_id, "飯が届きました！： #{title}", flex_contents)
    end
  end
```
`notify_line`メソッドは、投稿が公開された際にLINEユーザーに通知を送信する役割を担っています。
このメソッド内の`flex_contents`ハッシュ内でFlexMessageの中身を構築しています。
そして、`deliveries`に格納されているLINEユーザーに対して、`push_flex_message`メソッドを使用してFlexメッセージを送信します。`push_flex_message`については下記に後述します。

### 4.FlexMessageを送信する
```line_client.rb
require 'line/bot'

class LineClient
  def initialize
    @client = Line::Bot::Client.new do |config|
      config.channel_secret = ENV.fetch("LINE_MESSAGING_CHANNEL_SECRET", nil)
      config.channel_token = ENV.fetch("LINE_MESSAGING_CHANNEL_TOKEN", nil)
    end
  end

# フレックスメッセージ送信
  def push_flex_message(user_id, alt_text, contents)
    message = {
      type: 'flex',
      altText: alt_text,
      contents:
    }

    @client.push_message(user_id, message)
  end
end
```
こちらではLINEMessagingAPIを使用するためのコードを記載しています。
`LINE_MESSAGING_CHANNEL_SECRET`と`LINE_MESSAGING_CHANNEL_TOKEN`はMessagingAPIにアクセスするための重要な環境変数になるので、使用する際は`dotenv-rails`などを使って厳密に保管することをおすすめします。

`push_flex_message`メソッドは、`notify_line`メソッドから`user_id（LINEユーザーのID）`、`alt_text（代替テキスト）`、および `contents（Flexメッセージの内容）`の引数を受け取り、
`@client.push_message`メソッドを使用して指定されたユーザーに対してFlexメッセージを送信します。

要約すると、`notify_line`メソッド内で具体的なFlexメッセージの内容を構築したものを、`push_flex_message`メソッドで送信するロジックを構築しています。


# 主な使用技術

### バックエンド
- Ruby(3.2.2)
- Rails(7.0.5)
- Redis(4.0)
- Sidekiq

### フロントエンド
- JavaScript
- jQuery
- Bootstrap(5.3.0)
- Hotwire
- LINE Front-end Framework（LIFF）

### インフラ
- Heroku
- mkcert

### データベース
- PostgreSQL

### API
- LINEMessagingAPI

# ER図
テーブル設計は以下のようになっています。
![er.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3493892/829f3227-6936-2026-4cf1-e15b72ec218e.png)

# 最後に
初めてのWebアプリ作成でしたが、とても学びになることが多かったです。
記事に起こすのも初めてでしたが、頭で理解できていることを読み手に理解しやすいように文字にするのがとても難しく、書きながらアウトプットの大変さを痛感しています。

この記事を読んで「自分も飯テロしてみたい！」と思っていただけましたら、ぜひ私のアプリを触っていただけると大変嬉しいです✨

拙い文章でしたが、最後までお読みいただき誠にありがとうございました！