hawk, camphora, avocado and so on..

美術展、博物展、神社、旅行、読書、IT関連などの雑感を書いていきます。

毎朝 天気を通知する LINE Bot を作ってみました。

鷹楠です。今年は「思いついたらとりあえずやってみる」精神でいきたいです。
クオリティはひとまず気にしない。既存のものがあるとかも気にしない。

きっかけ&こんなものをつくりたい

朝、傘を持っていくのを忘れる。
天気予報をみればいいんだけど、それをみるのを忘れる。
毎朝、LINEに通知が来れば嬉しい。特に知りたいのは降水確率。
地域の設定はなるべく手間をかけずにやりたい。
※ちなみに、毎日天気を知らせてくれる、似たようなサービスは既にあるようです(「LINEお天気」など)。

こんなものができた

名付けて、「毎朝お知らせ!天気予報」


f:id:takagusu:20170124194725j:plain


使い方の表示はこんな感じ(友だち追加時と、何か話しかけたときに表示)。


f:id:takagusu:20170124194802j:plain


位置情報を送信すると、地域を設定できる。


f:id:takagusu:20170124194917j:plain


f:id:takagusu:20170124200046j:plain


「天気」と入力すると、その日の天気予報をお知らせ。

そして、「スタート」と入力すると、毎朝7時に天気が通知されるようになる。


f:id:takagusu:20170124200317j:plain


朝、通知が届いている様子。これで傘を忘れることもなくなりそう。


f:id:takagusu:20170124195216j:plain


おおまかなつくりかた

  • LINE Bot 自体は、LINE Business Center に登録すれば無料で作成できる。ためしにということで、Messaging API Developer Trial でアカウントを作成した。返信だけでなく、Bot 側からユーザーにメッセージを送信する Push API が利用できる。ただし、追加可能友だち数は50人という制限がある。ブラウザ上で、Bot のプロフィールなどを設定する。
  • IBM Bluemix にアカウントを作成し、LINE Bot の返信のためのサーバーアプリを作る。Bluemix は Amazon Web Service や Google Cloud Platform のような、開発者向けクラウドサービスで、工夫すれば無料で利用できる。
  • 今回、サーバーアプリは、Bluemix が提供している Ruby + Sinatra のテンプレートをベースにして、LINE が提供している Ruby 用の API を使って実装した。LINE Business Center から辿れる LINE Developers > Channels ページで、作成した LINE Bot の Webhook URL に、サーバーアプリの URL を設定することで、Bot の返信がサーバーアプリに任される。
  • 天気の情報は、JP Weather Forecast という天気予報 API を利用させてもらった。無料で降水確率が取得できる API はこれ以外にほとんどなかった。ありがたい。(ただ、欲を言うともっと細かい地域の情報が知りたい。)日本の主要な地域の天気予報が XML または JSONP 形式で取得できるので、これを Ruby でパースする。
  • どこの天気をお知らせするかという 地域 の設定は、LINE の位置情報を利用できるようにした(これは LINE のかなり売りな機能だと思う)。ユーザーが位置情報を送信すると、サーバーアプリでは 緯度・経度 が取得できるので、天気予報 API から取得できる最も近い観測地点を自動的に選択する。各都道府県ごとに、数個の地点があるようである。詳細な位置情報は、プライバシーの観点から、保存しない。
  • ユーザーごとの ID と地域を保存するために、Bluemix のサービスのひとつ ClearDB (中身は MySQL)を利用した。5 MB までなら無料で使える。
  • 毎朝メッセージを送信するために、Bluemix のサービスのひとつ Workload Scheduler を利用した。Windows のタスクスケジューラのようなことができる(というより、UI そのまんま)。月 50 回までなら無料で使える。

ソースコードの紹介

LINE Bot の受け口と返信

/callback という URL を受け口にすることにします(名前は任意です)。ユーザーからメッセージが来た場合、この URL が呼ばれます。Sinatra のコードだとこんな感じ。

post '/callback' do

  # 処理

  "OK"
end


LINE の Ruby 用の API を利用するために、require に下記を書く(Gemfile にも)。

require 'line/bot'


Ruby 用の API のサンプルコードに倣って、まず client を定義。ここで、LINE_CHANNEL_SECRET と LINE_CHANNEL_TOKEN は、Bluemix に設定した環境変数です。値は LINE Developers で取得・確認できます。

def client
  @client ||= Line::Bot::Client.new { |config|
    config.channel_secret = ENV["LINE_CHANNEL_SECRET"]
    config.channel_token = ENV["LINE_CHANNEL_TOKEN"]
  }
end


client.parse_events_from でリクエスト body をパースして、

  body = request.body.read

  events = client.parse_events_from(body)


events.each で回して、イベントの種類ごとに処理を書きます。
文字列が入力された場合は event.type の値が Line::Bot::Event::MessageType::Text に、位置情報が入力された場合は Line::Bot::Event::MessageType::Location になるようです。
メッセージを返信するには、まず message オブジェクトを作成し、client.reply_message(event['replyToken'], message) を実行します(下記のコード参照)。一定時間(記事作成時点で30秒)以内に返事を返さないといけないようです。

  events.each { |event|
    case event
    when Line::Bot::Event::Message
      case event.type

      when Line::Bot::Event::MessageType::Text
        # 文字列が入力された場合

        case event.message['text']
        when 'スタート'
          # 「スタート」と入力されたときの処理
        when 'ストップ'
          # 「ストップ」と入力されたときの処理
        when /.*天気.*/
          # 「天気」を含む文字列が入力されたときの処理

        end

      when Line::Bot::Event::MessageType::Location
        # 位置情報が入力された場合
        latitude = event.message['latitude'] # 緯度
        longitude = event.message['longitude'] # 経度

        # 経度・経度を使った処理
      end

      message = { type: 'text', text: "デフォルトの返信メッセージ"}
      client.reply_message(event['replyToken'], message)
    end
  }

天気の取得

必要なモジュール(Ruby標準です)

require 'net/http'
require 'uri'
require 'rexml/document'


JP Weather Forecast という天気予報 API で、天気を XML 形式で取得し、パースします。Ruby だと、とても簡単に書けます。これは東京都の天気を取得する例です。

    uri = URI.parse('http://www.drk7.jp/weather/xml/13.xml')
    xml = Net::HTTP.get(uri)
    doc = REXML::Document.new(xml)


各情報を取得します。doc.elements[<xpath>].text で値を取得、doc.elements[<xpath>].attributes[<属性名>] で属性値を取得できます。この簡便さ。RubyXML パーサは神ですね。上記のコードに引き続き、東京都 東京地方 の天気を取得する例です。

    xpath = 'weatherforecast/pref/area[4]'

    weather = doc.elements[xpath + '/info/weather'].text # 天気(例:「晴れ」)
    max = doc.elements[xpath + '/info/temperature/range[1]'].text # 最高気温
    min = doc.elements[xpath + '/info/temperature/range[2]'].text # 最低気温
    per00to06 = doc.elements[xpath + '/info/rainfallchance/period[1]'].text # 0-6時の降水確率
    per06to12 = doc.elements[xpath + '/info/rainfallchance/period[2]'].text # 6-12時の降水確率
    per12to18 = doc.elements[xpath + '/info/rainfallchance/period[3]'].text # 12-18時の降水確率
    per18to24 = doc.elements[xpath + '/info/rainfallchance/period[4]'].text # 18-24時の降水確率

位置情報から地域の選定

LINE の位置情報を利用して、天気を取得できる地域のうち、最も近いものを選定します。
天気を取得できる地域の緯度・軽度はあらかじめ天気予報 API から取得し、データベースに格納しておきます。

先ほど、LINE でユーザーから位置情報が送信された場合に 緯度・経度 を取得するコードを紹介しましたので、その情報を使って、下記の SQL を実行します。area_info は、地域の緯度・経度を格納しているテーブル名です。

select * from area_info order by abs(latitude - 緯度) + abs(longitude - 経度) asc

Bot 起点でユーザーにメッセージを送る

返信ではなく、Bot 起点でユーザーにメッセージを送るには、下記のように書きます。Push API というらしいです。
こちらは、Developer Trial だと無料で利用できます(ただし先ほども書いたように、追加可能友だち数は 50 人まで)。フリー版だと Push API は利用できません。

message = { type: 'text', text: '送信したいメッセージ' }
client.push_message(row['user_id'], message)

今回、毎朝この処理を実行したいのですが、Bluemix の無料枠では、Sinatra 上で定期実行するのは難しそう…(できるのかな?)。Sinatra 上でこのようなことをする方法をどなたか知ってれば教えてほしいです。

ということで、処理を実行するための URL をひとつ用意することにして、Bluemix の Workload Scheduler から HTTP リクエストを投げることで実現しました。
サーバー側の受け口は下記のような感じ。/send という URL にしました。

get '/send' do
  protected! # basic auth

  # メッセージ送信処理

  "OK"
end

さすがにセキュリティが何もないとまずいと思うので、Basic 認証を入れました。上記、protected! を呼んでいるところの定義は下記です。こちらは、Sinatra の Q&A に載っていたものをそのまま使いました(まあ、Basic 認証もセキュリティ上よくないですが…)。BASIC_AUTH_USERNAME と BASIC_AUTH_PASSWORD は Bluemix の環境変数で、ユーザー名とパスワードです。

helpers do
  def protected!
    return if authorized?
    headers['WWW-Authenticate'] = 'Basic realm="Restricted Area"'
    halt 401, "Not authorized\n"
  end

  def authorized?
    @auth ||=  Rack::Auth::Basic::Request.new(request.env)
    @auth.provided? and @auth.basic? and @auth.credentials and @auth.credentials == [ENV['BASIC_AUTH_USERNAME'], ENV['BASIC_AUTH_PASSWORD']]
  end
end

所感

LINE Bot は、いろんなAPIと組み合わせることができるとても良いプラットフォームだなと思います。しくみは単純で、それゆえに奥が深い。LINE BOT AWARD なるものもあり、APIを提供しているいろんな企業が協賛になってるみたいです。他のプラットフォームやAPIも活用して、何か作ってみたいですね。

では。