Edited at

[LineBootAwards2018] linebotとclovaを連携させて見守りアプリを作った。


アプリ名

IMAGE ALT TEXT HERE

linebot: ドア君

clovaskill: ドア君


説明

ただいまの言葉をきっかけに親からの伝言を子供へ伝えます。

忙しくて電話もできない、手紙も書く時間がない親に変わってテキスト形式で子供への伝言を預かります。

子供の周りに起きている事件の情報、洗濯物の天敵の雨の情報、よく遊ぶ子供が避けたいインフルエンザの情報を親に届けます。


紹介動画

https://youtu.be/G19BNJIt0co


エントリーページ

https://www.line-community.me/awards/workdetail/5bbd650d851f745ac3022468


 機能概要


  • 伝言板機能


    • ドア君アカウントに対してメッセージを登録

    • clovaでスキルを起動 -> 伝言板を読み上げ・メッセージプッシュが行われる


      • line_user_idにより紐付けを行った。





  • お天気プッシュ機能



  • 不審者プッシュ機能


    • ドア君アカウントに対してメッセージで不審者情報のプッシュ通知をする地区を登録

    • 警視庁ホームページより、情報を取得し、プッシュ通知





  • インフルエンザプッシュ機能


    • GoogleTrendsより、「インフルエンザ」の検索結果により流行を予測




 使用技術


  • 使用言語: Ruby

  • フレームワーク: RubyOnRails

  • デプロイ: heroku

  • linebot

  • clova


Messaging Api

rubyなので、line-bot-apigemを利用

gem 'line-bot-api'

https://developers.line.me/ja/docs/messaging-api/


プッシュ通知

require 'line/bot'

require 'dotenv'

class LineBotClient
# OPTIMIZE このクラスにLineBotへのリクエストをまとめる

# to: userId、groupId、またはroomId
# https://developers.line.me/ja/reference/messaging-api/#anchor-0c00cb0f42b970892f7c3382f92620dca5a110fc
def pushMessage(type='text', text="", to)
message = {
type: type,
text: text
}
client.push_message(to, message)
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

第一引数: プッシュメッセージのタイプ

第二引数: 送信するテキスト情報

第三引数: userId、groupId、またはroomId

line_bot_client = LineBotClient.new

line_bot_client.pushMessage('text', text, to)


clovaスキル

rubyに対応しているライブラリはないので、直接CEK_APIを叩く

https://clova-developers.line.me/guide/#/CEK/References/CEK_API.md


clovaの応答

@shouldEndSessionの値がfalseだとクローバのスキルが終了せずに再度待ち状態となる。

だが、@shouldEndSessionの値がfalseでもvoice_messageが存在しない場合は、セッションが終了する仕様であった。

class ClovaController < ApplicationController

require 'line/bot'
before_action :set_clova, only: [:callback]
before_action :set_shouldEndSession, only: [:callback]

def callback
@voice_message = "おかえりなさい!"

if @shouldEndSession
# ラインボットへ通知を送る
send_push_message

# @voice_messageの設定
set_voice_message
end

render 'clova/callback', formats: 'json', handlers: 'jbuilder'
end

private

def set_shouldEndSession
# スキルの起動時のみfalseとする
slots = params['request']['intent']['slots']
@shouldEndSession = slots.nil? ? false : true
end

def send_push_message
...
end

def set_voice_message
...
end
end

callback.json.jbuilder

json.version "1.0"

json.response do |response|
response.outputSpeech do |outputSpeech|
outputSpeech.type "SimpleSpeech"
outputSpeech.values do |values|
values.type "PlainText"
values.lang "ja"
values.value @voice_message
end
end
response.card {}
response.directives []
response.shouldEndSession @shouldEndSession
end


お天気情報取得

OpenWeatherMapApiを利用。無料プランで、3時間毎のお天気情報が取得できる。

https://openweathermap.org/


サンプル

https://samples.openweathermap.org/data/2.5/forecast?id=524901&appid=b1b15e88fa797225412429c1c50c122a1

require "json"

require "open-uri"

class OpenWeatherMap
BASE_URL = "https://api.openweathermap.org/data/2.5/forecast".freeze
TOKYO_CITY_ID = '1850147'

# tokyo: key='id', val='1850147'
# akashi: key='q', val='Akashi'
def self.get(key = 'id', val = TOKYO_CITY_ID)
url = BASE_URL + "?#{key}=#{val}&APPID=#{ENV['Open_Weather_Map_API_KEY']}"

puts "Request method:GET, URL:#{url}"
json_response = open(url)
JSON.parse(json_response.read)
end
end

tokyo_city_id = '1850147'

tokyo_weather_infos = OpenWeatherMap.get('id', tokyo_city_id)


不審者情報

Mechanizeでのスクレイプ。

1日毎に情報が更新されるため、1日に一回クーロンで実行。

namespace :patrol do

desc "東京都の不審者情報取得"
task :tokyo_police_page => :environment do
SLEEP_TIME = 1.second.freeze

puts "Start scrape..."
agent = Mechanize.new
top_page = agent.get("http://www.keishicho.metro.tokyo.jp/kurashi/higai/kodomo/fushin/index.html")
sleep(SLEEP_TIME)
detail_urls = top_page.search('.norcor').search('a').map{|e| e.get_attribute(:href) }
detail_urls.each_with_index do |detail_url, i|
sleep(SLEEP_TIME)
# only scrape newest day
break if i > 0

# move to detail page
detail_page = top_page.link_with(href: detail_url).click
city_infos = detail_page.search('tr')
city_infos.each do |city_info|
# input suspicious person infomation
city_name = city_info.children.search('th > p').children.first.text
suspicious_person_info_text = city_info.children.search('td > p').first.children.map{ |a| a.text }.reject(&:empty?).join('\n')
tx = city_info.children.search('ul > li').first&.children&.map{ |a| a.text }&.reject(&:empty?)&.join('\n')
suspicious_person_info_text += "\n" + tx if tx

if city = City.find_by(name: city_name)
puts "find #{city_name}"
city.suspicious_person_infos.find_or_create_by(
text: suspicious_person_info_text,
source_url: detail_url,
published_at: Time.now.beginning_of_day)
else
puts "Error: Not found city name #{city_name}"
end
end
end
puts "Finish scrape.\n"

puts "Start send message..."
send_alart_push_message()
puts "Finish send message."
end

def send_alart_push_message()
SuspiciousPersonInfo.tell_infos.each do |line_user_id, cities|
text = create_message_text(cities).chomp
response = send_message_text(text, line_user_id.to_s)
puts "Sent message. line_user_id:#{line_user_id} response:#{response}"
end
end

def send_message_text(text, to)
@line_bot_client ||= LineBotClient.new
@line_bot_client.pushMessage('text', text, to)
end

def create_message_text(cities)
# TODO 不審者情報が見やすいようなページを作成
# tokyo_police_page = "http://www.keishicho.metro.tokyo.jp/kurashi/higai/kodomo/fushin/index.html"
<<-EOS
<不審者情報>
#{cities.map(&:name).join(', ')} にて不審者が確認されました。
EOS
end
end


インフルエンザ情報

取得したCSVをManagementページで追加すると、CSVからインフルエンザのトレンド情報を取得する。そのトレンドからインフルエンザを予測。

現状は、自動でCSVを取得する部分が未完成。

require 'csv'

class GoogleTrendsManager
def initialize(a = nil, b = nil)
@csv_file = nil

if a.class == ActionDispatch::Http::UploadedFile && b == nil
import_csv_by_file(a)
elsif a.class == String && b.class == String
import_csv_by_crowl(a, b)
else
raise "Unexpected params."
end
end

def get_trends
return unless @csv_file
trends = {}

CSV.foreach(@csv_file.path, headers: false).with_index do |row, i|
next if i < 3

time, score = row
trends[time] = score
end

trends
end

private

def import_csv_by_file(file)
@csv_file = file
end

def import_csv_by_crowl(q='', geo='JP')
# TODO Seleniumでのクローリング -> CSVファイル取得
...
end
end


参考

https://developers.line.me/ja/docs/

https://openweathermap.org/

http://www.keishicho.metro.tokyo.jp/

https://qiita.com/4geru/items/7fdd418ce8c1059001c6