初めに
初学者です。
個人開発でRailsアプリにlinebotを導入した際の記録です。
食事内容と排便の状態を記録することで、自身の胃腸と食事の相性を判定するツールで、ブラウザで行う操作をlinebotから手軽に行えるようにするために導入しました。
この記事ではLINEBotの初期設定から開始し、アプリに必要な応答パターンを作成するところまでを記録しています。
この記事で行なっている作業はこちらの記事で構築した環境で実施しています。
https://qiita.com/tkhero555/items/a1811369c59021077d62
アプリ「Puri.log」はこちら(2024/5/2時点でMVPリリース状態)
https://purilog-2a85943c9f18.herokuapp.com/
技術構成とバージョン
カテゴリ | 技術 |
---|---|
フロントエンド | javascript/Hotwire/bootstrap |
バックエンド | ruby3.3.0/rails7.1.3.2 |
データベース | PostgreSQL16.2 |
認証 | devise |
環境構築 | Docker / docker-compose |
CI/CD | Github Actions |
インフラ | heroku |
LINEBOTの初期設定
アカウント登録〜取り敢えずトーク画面で返信設定をするところまでは下記リンクの記事を参考に実施済み
https://qiita.com/kohki_takatama/items/e19960e479a712d63408
line-bot-apiのインストール〜ngrok設定
Gemfileに追記
gem 'line-bot-api'
docker composeで環境作ってるのでコマンドをつけながら、gemのインストール、イメージのビルド、linebot用のコントローラー作成まで
docker compose run web bundle install
docker compose build
docker compose run web bundle exec rails g controller LineBot
これで成功
[+] Creating 1/0
✔ Container runteq_graduation_exam-db-1 Running 0.0s
create app/controllers/line_bot_controller.rb
invoke erb
create app/views/line_bot
invoke test_unit
create test/controllers/line_bot_controller_test.rb
invoke helper
create app/helpers/line_bot_helper.rb
invoke test_unit
ルーティングを設定する
# config/routes.rb
Rails.application.routes.draw do
post 'callback' => 'line_bot#callback'
end
作成されたコントローラーにcallbackアクションを用意する。
class LineBotController < ApplicationController
def callback
end
end
LINEのチャネルとRailsの連携
LINE developersのチャネル管理画面で3つの項目を確認する。
- channel secret
- assertion signing key
- channel access token
railsアプリにdotenv-rails gemを入れて環境変数の定義ができるようにする。
gem 'dotenv-rails'
バンドルインストールとイメージビルド
docker compose run web bundle install
docker compose build
.envファイルをルートに作成して、先ほどLINE developersで確認した項目を設定する。
LINE_CHANNEL_SECRET='xxxxxxxx'
LINE_CHANNEL_TOKEN='xxxxxxxx'
line-bot-apiに用意されているクラスLine::Bot:clientインスタンスを作っておく。
class LineBotController < ApplicationController
def callback
end
private
# clientメソッド
def client
# インスタンス変数がからであればLine::Bot::Clientのインスタンスを代入する
# 環境変数を利用して、チャンネルシークレットとトークンをインスタンスに渡している。
@client ||= Line::Bot::Client.new { |config|
config.channel_secret = ENV["LINE_CHANNEL_SECRET"]
config.channel_token = ENV["LINE_CHANNEL_TOKEN"]
}
end
end
ローカルで開発している際にlinebotの動作を確認するためにngrokを使用する。
サイトにアクセスしてサインアップしてログインを済ませる。
https://ngrok.com/
ローカルでhomebrewを使ってngrokをインストールする。
brew install ngrok
ngrokのサイトにアクセスし、メニューからyour authtokenを確認する。
PCとngrokアカウントを連携する。
ngrok authtoken 確認したトークン
ローカルサーバーを起動する。
docker compose up
ポート番号を指定してngrokを起動する。
ngrok http 3000
表示されるngrokの画面でForwardingの部分がアクセス先のurl
もう一点、railsのセキュリティ設定を変更してやる必要がある。
Rails.application.configure do
# 省略
config.hosts.clear
end
ローカルサーバーで起動したアプリにngrokを通して、外部からアクセスできるようになった。
linebotでおうむ返しができるところまで
LINE developersにアクセスして、messaging APIのwebhookURLに、ngrokで生成したURL + /callbackを設定する。
コントローラーに確認用コードを追加する。
ユーザーがbotに送ったメッセージを表示するコードをcallbackアクションに追記
class LineBotController < ApplicationController
def callback
puts "======="
puts body = request.body.read
puts "======="
end
private
def client
@client ||= Line::Bot::Client.new { |config|
config.channel_secret = ENV["LINE_CHANNEL_SECRET"]
config.channel_token = ENV["LINE_CHANNEL_TOKEN"]
}
end
end
linebotにメッセージを送ってみるとエラー
ActionController::InvalidAuthenticityToken (Can't verify CSRF token authenticity.):
line_bot_controllerに下記を追加して解決
skip_before_action :verify_authenticity_token
再度メッセージを送って見ると、サーバーログでパラメーターが確認できる。
=======
# メッセージから取得したパラメータ
=======
callbackアクションをおうむ返しするものにしていく
def callback
# LINEからのイベントデータを取得する
body = request.body.read
# gemのメソッドであるparse_events_from(body)でevents配列を取得する。
events = client.parse_events_from(body)
# 取得した配列を繰り返しで処理する
events.each do |event|
# caseを使用して、eventの種類の条件をwhenで指定して、種類ごとに処理を行う。
case event
# eventがMessageかどうかをチェック
when Line::Bot::Event::Message
# ネストしてcaseを使用、eventのさらにタイプで分類
case event.type
# MessageType::Textかどうかをチェック
when Line::Bot::Event::MessageType::Text
# messageに受け取ったメッセージをそのまま代入する。
message = {
type: "text",
text: event.message["text"]
}
# これがユーザーにメッセージを配信する記述2つ目の引数にあるmessageに先ほど代入した内容が入っている。
client.reply_message(event['replyToken'], message)
end
end
end
end
送ったメッセージをそのまま返してくるようになってれば成功
必要なパターンを整理する。
必要なやり取りのパターン
- ユーザー「食事の記録」 bot「食事名を送ってください。例:〜」
- ユーザー「食事名」 bot「日時に食事名を食べたことが記録されました。」 (railsに入力データを引き渡す)
- ユーザー「便の記録」 bot「便の状態を選んでください。1:良い, 2:普通, 3:悪い」 ユーザー 選択肢クリック bot「日時に状態の便が記録されました。」
- ユーザー「おすすめの食事」 bot「あなたにおすすめの食事は〜です。」
- ユーザー「避けるべき食事」 bot「あなたが避けるべき食事は〜です。」
メッセージイベントのテキストであるかを既存の分岐で判断した上で、メッセージのテキストの内容を変数に格納し、格納したテキストの内容との一致で分岐を作って処理をする。
パターン一つずつ機能を作っていき、条件分岐を加えていく。
データベースへの記録を担うコードは、別途実装する予定。
パターンごとに実装
メッセージかどうかの判別
body = request.body.read
events = client.parse_events_from(body)
events.each do |event|
case event
when Line::Bot::Event::Message
case event.type
when Line::Bot::Event::MessageType::Text
case event.message["text"]
when "食事の記録"
when "便の記録"
when "おすすめの食事"
when "避けるべき食事"
else
end
ユーザー「食事の記録」 bot「食事名を送ってください。例:〜」
リッチメニューをクリックして、ユーザーに「食事の記録」とメッセージを送らせる想定なので、 == ’食事の記録’ を条件式とする。
when "食事の記録"
message = {
type: "text",
text: "メッセージで食べたもののメニュー名を送ると記録されます。\n送った際の時刻に食べたものとして記録されます。\n時間指定をして記録したい場合は、サイトから実行してください。"
}
client.reply_message(event['replyToken'], message)
ユーザー「食事名」 bot「日時に食事名を食べたことが記録されました。」
これが唯一入力形式を絞り込めない項目になるので、他の条件に合致しない場合にこの動作をするように設定する。
else
meal_log = event.message["text"]
=begin
データベースへの保管を行うコードを実装予定
=end
message = {
type: "text",
text: "食事の記録が完了しました。"
}
client.reply_message(event['replyToken'], message)
ユーザー「便の記録」 bot「便の状態を選んでください。1:良い, 2:普通, 3:悪い」
判別は”便の記録”
選択肢つきのメッセージを作成して、ユーザーに返信する。
選択肢をクリックすると、数字の1~3をユーザーが送る。
app/services/line_bot/messages/フォルダを用意して、unko_message.rbを作成し、ボタンテキストのコードを記述することにする。
module LineBot
module Messages
class UnkoMessage
def button_message
{
"type": "template",
"altText": "This is a buttons select stools condition",
"template": {
"type": "buttons",
"title": "排便の記録",
"text": "選択肢から便の状態を選択してタップしてください。",
"actions": [
{
"type": "message",
"label": "1:良い",
"text": "1"
},
{
"type": "message",
"label": "2:普通",
"text": "2"
},
{
"type": "message",
"label": "3:悪い",
"text": "3"
}
]
}
}
end
end
end
end
when "便の記録"
message = LineBot::Messages::UnkoMessage.new.button_message
client.reply_message(event['replyToken'], message)
ユーザー 選択肢クリック bot「排便が記録されました。」 (railsに入力データを引き渡す)
条件式は、1 or 2 or 3
when "1", "2", "3"
puts "便の記録完了確認用"
stool_log = event.message["text"]
=begin
# 便の記録と、状態に応じた評価値の変動
=end
message = {
type: "text",
text: "排便の記録が完了しました。"
}
client.reply_message(event['replyToken'], message)
ユーザー「おすすめの食事」 bot「あなたにおすすめの食事は〜です。」
判別は”おすすめの食事”
データベースからデータと取って動的にテキストを構築してユーザーに送る。
見つからない場合は、”おすすめの食事はありません”と表示する。
データベースを使用するコードは別途実装するので、この時点では"おすすめの食事機能は未実装です。"と返信するようにしておく。
when "おすすめの食事"
recommend_meal = "おすすめの食事機能は未実装です。"
message = {
type: "text",
text: recommend_meal
}
client.reply_message(event['replyToken'], message)
ユーザー「避けるべき食事」 bot「あなたが避けるべき食事は〜です。」
おすすめの食事と同じ手法で実装する。
when "避けるべき食事"
avert_meal = "避けるべき食事機能は未実装です。"
message = {
type: "text",
text: avert_meal
}
client.reply_message(event['replyToken'], message)
これで必要な応答パターンをlinebotに実装できた。
コードを整理してline_bot_controllerを完成させる。
class LineBotController < ApplicationController
def callback
events.each do |event|
client.reply_message(event['replyToken'], message(event))
end
end
private
config.channel_token = ENV["LINE_CHANNEL_TOKEN"]
}
end
def request_body
request_body = request.body.read
end
def events
# gemのメソッドであるparse_events_from(body)でevents配列を取得する。
events = client.parse_events_from(request_body)
end
def message(event)
# caseを使用して、eventの種類の条件をwhenで指定して、種類ごとに処理を行う。
case event
# eventがMessageかどうかをチェック
when Line::Bot::Event::Message
# ネストしてcaseを使用、eventのさらにタイプで分類
case event.type
# MessageType::Textかどうかをチェック
when Line::Bot::Event::MessageType::Text
case event.message["text"]
when "食事の記録"
message = {
type: "text",
text: "メッセージで食べたもののメニュー名を送ると記録されます。" +
"送った際の時刻に食べたものとして記録されます。" +
"時間指定をして記録したい場合は、サイトから実行してください。"
}
when "便の記録"
message = LineBot::Messages::UnkoMessage.new.button_message
when "1", "2", "3"
puts "便の記録完了確認用"
stool_log = event.message["text"]
=begin
便の記録と、状態に応じた評価値の変動
=end
message = {
type: "text",
text: "排便の記録が完了しました。"
}
when "おすすめの食事"
puts "おすすめの食事動作確認用"
=begin
データベース操作のコード
=end
recommend_meal = "おすすめの食事機能は未実装です。"
message = {
type: "text",
text: recommend_meal
}
when "避けるべき食事"
puts "避けるべき食事動作確認用"
=begin
データベース操作のコード
=end
avert_meal = "避けるべき食事機能は未実装です。"
message = {
type: "text",
text: avert_meal
}
else
meal_log = event.message["text"]
=begin
データベースへの保管を行うコード
=end
message = {
type: "text",
text: "食事の記録が完了しました。"
}
end
end
end
end
end
# 参考記事など
https://qiita.com/kohki_takatama/items/e19960e479a712d63408
https://qiita.com/KNR109/items/e1b5ebd94393441fff74
https://qiita.com/nishina555/items/4ffaf5cc57a384b66230
https://qiita.com/4geru/items/0b8a19165e5b694f5446
https://qiita.com/Yuzu_Ginger/items/5e5fbe6d61e1abe2b8ab
https://developers.line.biz/ja/reference/messaging-api/#action-objects
https://qiita.com/4geru/items/0b8a19165e5b694f5446