2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

[Rails/Render]LINE Message APIによる通知機能の実装方法

Posted at

始めに

完全未経験からwebエンジニア転職を目指して、Ruby on Railsを中心に学習している者です。

今回、WEBアプリでLINE Message APIを使用したLINE通知の実装方法を復習も兼ねてまとめていきたいと思います!

環境

  • Mac OS
  • Rails 7.2.3"
  • ruby-3.3.6
  • デプロイ先:Render
    ※ LINE Developersへの登録とプロバイダー設定は済んでいるものとします。
    LINEログイン実装の記事でLINE Developersへの登録など詳しく書いてあるのでよろしければご覧ください↓

LINE Message APIとは

Messaging APIは、あなたのサービスとLINEユーザーの双方向コミュニケーションを可能にする機能です。
あなたのアカウントの友だちになってくれたユーザーにメッセージを送信できます。
また、グループに追加されていれば、グループ内でメッセージを送信することも可能です。

要するに、美容室や飲食店でよくある、公式LINEを作成して色々できるよってことですね!

webアプリの課題である通知機能
メールだと埋もれがちですが、LINEなら開封率も高く、ユーザー体験(UX)の向上に直結します。
さらにLINEログインを実装している場合は相性も抜群です!

⚠️ 無料プランは(月)200通のみです。
詳しいプランはこちらを参照してください。

今回のゴールと実装の流れ

今回のゴール

今回は、ユーザーが設定した日時に、リマインドをLINEに届ける機能を実装します。

こんな感じでユーザーが日時を設定↓

スクリーンショット 2026-02-04 22.41.32.png

LINEでメッセージ通知が届く↓
IMG_2174.jpg

実際の流れ

  • 1, LINE公式アカウント作成(チャネルアクセストークン取得)
  • 2, 必要gem導入&cron設定
  • 3, 通知ロジックの実装(Rakeタスクの作成)
  • 4, Renderでのcron job設定(重要)

になります。

⚠️ターミナルとシェル

実装する前に、一つ注意点があります。
初学者の人が勘違いしやすい点として、ターミナルとシェルの違いです。

ターミナル:Mac本体で動かす、開発環境(ローカル)で動かす場所
シェル:デプロイ先の本番環境
で動かす場所
となっています。

開発環境では動いていたのに、本番環境では動かない、、といった場合はデプロイ先のシェル(今回だとRender)でコマンドを叩きましょう。

スクリーンショット 2026-02-05 13.36.01.png

1. LINE公式アカウント作成(チャネルアクセストークン取得)

まずは通知送信用の公式アカウントを作成していきます。

公式ドキュメントがかなりわかりやすいです↓

ボットが作成できたら、Message API設定からチャネルトークンを取得し、config/credentials.yml.encや.envへ格納します。(今回は通知のみなのでチャネルトークンのみでいいですが、Webhook(返信機能)を使用する場合はシークレットキーも同様に格納してください)

スクリーンショット 2026-02-04 23.22.13.png

line:
  messaging_secret: "xxxxxx" 
  messaging_token: "yyyyyy"

credentials.yml.encの設定はこんな感じ!

LINE側の設定は以上です。

作成した公式LINEのアイコン等の設定は、Line official account managerのビジネスプロフィールページ設定から変更してみてください!

2. 必要gem導入&cron設定

ここからはRails側での設定になります。
gem導入

gem "line-bot-api"

gem "whenever", require: false

この2つをインストールします。
require: falseにしているのは、起動時にwheneverは常に使用する意味がないので、起動時のメモリ消費を抑えるためにつけています。

cron使用の設定
cronを叩く中身の前に、compose.ymlファイルなどの設定を見直していきます。
下記の記述がない場合追記しましょう。

# compose.yml
services:
  ~~略~~
  web:
  ~~略~~
  
  cron:
      build:
        context: .
        dockerfile: Dockerfile.dev
      command: bash -c "bundle install && bundle exec whenever --update-crontab && cron -f"
      volumes:
        - .:/myapp
        - bundle_data:/usr/local/bundle:cached
        - node_modules:/myapp/node_modules
      environment:
        - RAILS_ENV=development
        - TZ=Asia/Tokyo
        - BUNDLE_PATH=/usr/local/bundle
      env_file:
        - .env
      depends_on:
        db:
          condition: service_healthy

# Dockerfile.dev
RUN apt-get update -qq && apt-get install -y build-essential libpq-dev nodejs yarn vim cron

cronを使用できる環境を整えます。

通知ロジックの実装(Rakeタスクの作成)

通知機能の核となるRakeタスクの作成に移ります。

notification_setting.rbテーブル

スクリーンショット 2026-02-05 0.50.01.png

主なカラム

  • enabled
    • 通知のON/OFF切り替え
  • send_time
    • 時間設定
  • mon~sun
    • 曜日設定

作成ファイル一覧

※notification_settingという通知設定テーブルを想定しています。
ご自身の命名に変えて作成してください。

  • app/models/notification_setting.rb
    • バリデーション設定
  • app/controllers/notification_settings_controller.rb
    • リダイレクトやCRUD設定
  • app/views/notification_settings/edit.html.erb
    • 通知機能画面の表示
  • app/services/line_bot_service.rb
    • LINE APIを叩く処理設定
  • lib/tasks/notification.rake(重要)
    • Cron Jobから呼び出されるコマンド設定。ここで通知の処理やメッセージを設定
  • config/schedule.rb
    • whenever用のファイル。ローカルでの確認用で、cronの読み込み頻度を設定。本番環境(Render)では使用しません。

以上が作成ファイルになります。
その中身を下記に記述します。

app/models/notification_setting.rb

class NotificationSetting < ApplicationRecord
  belongs_to :user
  validates :send_time, presence: true, if: :enabled?
end

バリデーション設定と他テーブルとの関係性を記述しています。

app/controllers/notifications_controller.rb

class NotificationSettingsController < ApplicationController
  before_action :require_login

  def edit
    @setting = current_user.notification_setting || current_user.create_notification_setting
  end


  def update
    @setting = current_user.notification_setting || current_user.build_notification_setting

    p_hash = notification_params

    if p_hash[:send_time].present?
      hour, min = p_hash[:send_time].split(":")
      @setting.send_time = Time.zone.now.change(hour: hour.to_i, min: min.to_i)
    end

    @setting.assign_attributes(p_hash.except(:send_time))

    if @setting.save
      redirect_to profile_path, notice: "通知設定を保存しました"
    else
      Rails.logger.error "保存失敗: #{@setting.errors.full_messages}"
      flash.now[:alert] = "保存に失敗しました。入力内容を確認してください。"
      render :edit, status: :unprocessable_entity
    end
  end

  private

  def notification_params
    params.require(:notification_setting).permit(
      :enabled, :send_time,
      :mon, :tue, :wed, :thu, :fri, :sat, :sun
    )
  end
end

editの編集ページのみの設計になっています。
詳細ページなど必要な場合は追加してください。

app/views/notification_settings/edit.html.erb

<div class="max-w-md mx-auto p-6">
  <% if current_user.line_user_id.present? %>
    
    <h2 class="text-xl font-black mb-6 flex items-center gap-2">
      <span class="w-2 h-6 bg-[#06C755] rounded-full"></span>
      LINE通知設定
    </h2>
    
    <%= form_with model: @setting, 
                  url: notification_setting_path, 
                  method: @setting.persisted? ? :patch : :post, 
                  data: { turbo: false } do |f| %>
                    
      <div class="mb-6 p-4 bg-stone-50 rounded-2xl flex justify-between items-center border border-stone-100">
        <span class="font-bold text-stone-700">通知を有効にする</span>
        <%= f.check_box :enabled, class: "w-6 h-6 rounded border-stone-300 text-[#06C755] focus:ring-[#06C755]" %>
      </div>

      <div class="mb-6">
        <p class="text-sm font-bold text-stone-400 mb-3 uppercase tracking-wider">通知日時</p>
        <div class="grid grid-cols-7 gap-2">
          <% [:mon, :tue, :wed, :thu, :fri, :sat, :sun].each do |day| %>
            <label class="cursor-pointer">
              <%= f.check_box day, class: "hidden peer" %>
              <div class="py-3 text-center border-2 border-stone-100 rounded-xl font-bold text-stone-400 transition-all peer-checked:border-[#06C755] peer-checked:bg-[#06C755]/5 peer-checked:text-[#06C755]">
                <%= t("date.abbr_day_names")[[:sun, :mon, :tue, :wed, :thu, :fri, :sat].index(day)] %>
              </div>
            </label>
          <% end %>
        </div>
      </div>

      <div class="mb-8">
        <p class="text-sm font-bold text-stone-400 mb-3 uppercase tracking-wider">配信時間</p>
        <%= f.time_field :send_time, class: "w-full p-4 bg-stone-50 border-2 border-stone-100 rounded-2xl font-bold text-stone-700 focus:border-orange-200 outline-none transition-all" %>
      </div>

      <%= f.submit "設定を保存する", class: "w-full py-4 bg-gradient-to-r from-orange-400 to-orange-500 text-white font-black rounded-2xl shadow-lg shadow-orange-100 hover:scale-[1.02] transition-all active:scale-[0.98] cursor-pointer" %>
    <% end %>

  <% else %>
    <div class="text-center py-10 bg-white border-2 border-dashed border-stone-200 rounded-[2.5rem] px-6">
      <div class="bg-[#06C755]/10 w-20 h-20 rounded-full flex items-center justify-center mx-auto mb-6">
        <svg class="w-10 h-10 text-[#06C755]" fill="currentColor" viewBox="0 0 24 24">
          <path d="M24 10.304c0-5.369-5.383-9.738-12-9.738-6.616 0-12 4.369-12 9.738 0 4.814 4.269 8.846 10.036 9.608.391.084.922.258 1.057.592.121.303.079.778.039 1.085l-.171 1.027c-.053.303-.242 1.185 1.039.646 1.281-.54 6.913-4.073 9.448-6.972 1.747-2.008 2.551-3.654 2.551-5.286z"/>
        </svg>
      </div>
      <h2 class="text-xl font-black text-stone-800 mb-3">LINE通知を使いませんか?</h2>
      <p class="text-sm text-stone-500 mb-8 leading-relaxed">
        LINEと連携すると、設定した時間に<br>
        <span class="font-bold text-stone-700">「今日の献立どうする?」</span><br>
        とお知らせが届くようになります。
      </p>
      
      <%= button_to "/auth/line", 
          method: :post, 
          data: { turbo: false },
          class: "w-full py-4 bg-[#06C755] text-white font-black rounded-2xl shadow-lg shadow-green-100 hover:scale-[1.02] transition-all active:scale-[0.98] flex items-center justify-center gap-2" do %>
        <span>LINEと連携して通知を受け取る</span>
      <% end %>
    </div>
  <% end %>
</div>

ここはご自身の通知メッセージやレイアウトに変えてみてください!

app/services/line_bot_service.rb

require "line/bot"

class LineBotService
  def initialize
    @client ||= ::Line::Bot::Client.new { |config|
      config.channel_secret = Rails.application.credentials.line[:messaging_secret]
      config.channel_token = Rails.application.credentials.line[:messaging_token]
    }
  end

  def send_message(line_user_id, message)
    return if line_user_id.blank?

    response = @client.push_message(line_user_id, message)
    response.code
  end
end


require "line/bot"で読み込みをしています。
config/credentials.yml.encでのキーの受け渡しですが、ENVを使用している場合は下記のように変えてください。

config.channel_secret = ENV["LINE_CHANNEL_SECRET"]
config.channel_token = ENV["LINE_CHANNEL_TOKEN"]

lib/tasks/notification.rake

require "line/bot"
require "line/bot/v2/messaging_api/api/messaging_api_client"

namespace :notification do
  desc "LINEのリマインド通知を送信します"
  task send_reminders: :environment do
    now = Time.current.in_time_zone("Asia/Tokyo")
    day_of_week = now.strftime("%a").downcase

    puts "--- [Task Start] JST Time: #{now.strftime('%H:%M:%S')}, Day: #{day_of_week} ---"

    all_configs = NotificationSetting.includes(:user).where(enabled: true).to_a
    puts "DEBUG: Fetched #{all_configs.count} enabled settings from DB."

    targets = all_configs.select do |setting|
      next unless setting.send(day_of_week) == true

      config_time = setting.send_time.in_time_zone("Asia/Tokyo").strftime("%H:%M")
      current_time = now.strftime("%H:%M")

      is_match = (config_time == current_time)
      puts "  SettingID:#{setting.id} | User:#{setting.user_id} | Config:#{config_time} | Now:#{current_time} -> Match: #{is_match}"
      is_match
    end

    if targets.empty?
      puts "--- [Finished] No matching targets for this minute. ---"
      next
    end

    line_creds = Rails.application.credentials.line
    if line_creds&.[](:messaging_token).nil?
      puts "ERROR: Line messaging_token is missing!"
      next
    end

    client = Line::Bot::V2::MessagingApi::ApiClient.new(channel_access_token: line_creds[:messaging_token])

    targets.each do |setting|
      line_user_id = setting.user&.line_user_id
      next if line_user_id.blank?

      action = Line::Bot::V2::MessagingApi::URIAction.new(label: "レシピを探す", uri: "https://jisui-save.onrender.com")
      template = Line::Bot::V2::MessagingApi::ButtonsTemplate.new(text: "自炊の時間です!🍳", actions: [ action ])
      message = Line::Bot::V2::MessagingApi::TemplateMessage.new(type: "template", alt_text: "自炊の時間です🍳", template: template)
      push_request = Line::Bot::V2::MessagingApi::PushMessageRequest.new(to: line_user_id, messages: [ message ])

      begin
        client.push_message(push_message_request: push_request)
        puts "  SUCCESS: Sent to User ID #{setting.user_id}"
      rescue => e
        puts "  FAILED: User ID #{setting.user_id} - #{e.message}"
      end
    end
    puts "--- [Task End] ---"
  end
end

エラーが多かったためデバックコードがありますが、不必要なら削除してください。

config_time = setting.send_time.in_time_zone("Asia/Tokyo").strftime("%H:%M")
current_time = now.strftime("%H:%M")

ここで、ローカルとデプロイ先での時間のずれを東京時間に統一しています。

config/schedule.rb

set :output, "/log/cron.log"
set :environment, :development

every 1.minute do
  rake "notification:send_reminders"
end

本番環境ではRenderのcron jobを使用するため、最低限の記述になります。

every 1.minute do
  rake "notification:send_reminders"

で1分単位でcronを動かしています。

ファイル作成したら、ローカルで通知が来るか試してみましょう!

通知が来ない場合は、、

ログでエラーの確認をしましょう。
下記のコマンドで確認

tail -n 10 log/development.log

通知自体設定されているかは、docker compose exec web rails cでRailsコンソールに入り、下記のコマンドを叩いてください。

User.all.order(:id).map do |u|
s = u.notification_setting
status = s.present? ? "✅ 設定あり(ID:#{s.id})" : "❌ 設定なし"
time = s&.send_time ? s.send_time.strftime('%H:%M') : "--:--"
 "UserID:#{u.id.to_s.ljust(3)} | #{status.ljust(12)} | 時間:#{time}"
end.each { |line| puts line }; nil
UserID:1   | ✅ 設定あり(ID:6) | 時間:02:57

のようなIDと時間が入っていれば登録できています。

4. Renderでのcron job設定(重要)

RenderのDashboardの右上の+NEWからCron jobを選択。

スクリーンショット 2026-02-05 1.26.57.png

SettingsのSchedule欄で* * * * *と設定。
Docker Commandにbundle exec rake notification:send_remindersと設定。
これで1分間隔でcronが起動して、通知設定があるか判断してくれます。
bundle exec rake notification:send_remindersの部分はRakeタスクで設定している命名と合わせてください。

# lib/tasks/notification.rake
namespace :notification do
  desc "LINEのリマインド通知を送信します"
  task send_reminders: :environment do

スクリーンショット 2026-02-05 1.29.44.png

環境変数にDATABASE_URLRAILS_MASTER_KEYを追加します。

⚠️ENVを使用している場合は、.envに格納してあるチャネルトークンも追加してください!
Web Service側の環境変数にトークンを入れていても、Cron Job側にも同じ設定を入れないと、通知タスクは動きません。

スクリーンショット 2026-02-05 1.32.06.png

全て設定できたら、一番下のデプロイボタンを押して完了です!

もしCron jobが失敗したら、、

Renderで「Status 1」が出たら
RenderのCron Jobが失敗すると Status 1 と表示されます。これは「何らかのエラーで止まった」という合図です。焦らずに 「Logs」 タブを確認しましょう。環境変数の設定漏れや、DB接続エラーなど、原因が必ずログに書き出されています。

最後に

今回は、LINE Message APIを使用したLINE通知機能を実装しました。
私自身、初めての実装だったので「ローカルでは動くのに本番環境では動かない、、」と悩むことがありました。
ただ、ログを見ると「Status 1(エラー)」とエラーがでて上に原因が載っているので、冷静に対応すれば必ず解決できると思います。
エラーで思考ロックすると地獄なのでこまめに休むことをお勧めします笑

ここまで読んでいただきありがとうございます!

2
0
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
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?