うちのいぬ Tech Blog

Tech Blog of Uchinoinu/My dog

うちのいぬ - うみちゃんと話せるbotを作りました 3 - LINE bot を Heroku と Rails を使って作ってみた

これまでFacebook botを作った経緯や作り方を書きました。 http://satomi.hatenablog.jp/entry/2016/06/26/093734satomi.hatenablog.jp

http://satomi.hatenablog.jp/entry/2016/06/25/213801satomi.hatenablog.jp

今回は

LINE bot の作り方を書きます

https://business.line.me/services/products/4/introduction

出来たもの

f:id:susanne:20160625213554p:plain

まだオウム返しです。メッセージ処理は別ドメインなので、別に作ります。

今回はbotを使うところまでを書きます。

基本的にこれを読んでおけばOK

ググればいっぱい出てくるのでググッてください。

もしHerokuを利用していなければ(固定IPをもっていたら) LINE Developers - BOT API Trial (Deprecated) - Getting started を使うのが一番手っ取り早いです。

ハマった点

Herokuだとめんどい

LINE botではIPアドレスでのWhite Listを必要とします。 つまり、botのサーバーの固定IPアドレスが必要になるわけです。 Herokuではデフォルトでそれが提供されていないので、FixieというAddonを使ったりして、対応する必要があります。

Fixie - Add-ons - Heroku Elements

qiita.com

LINE提供のSDKがそのままでは使えない

LINE Developers - BOT API Trial (Deprecated) - Getting started

SDKはいくつか提供されていて、はっきり言ってこれを使えば一番楽です。

でもHerokuでFixieを使う場合、proxyを設定する必要があって、そのままでは使えなかったです。

また、wrapperは出ていますが、

line-bot | RubyGems.org | your community gem host

その他のバージョンの違いの問題で、使えなかったです。

Railsじゃなかったら、すんなり使えたかもしれないです。

Callbackの疎通テストに惑わされる

Callbackの疎通テストボタンがあります。 そこでcompleteになっても、 サーバーログではbad requestが出たりして、なんかエラってるって怖くなります。

ですが、基本的にはそれでOKなようです。

疎通テストをパスしたら、LINEアプリでの会話テストに移るといいです。

そっちで会話したら、エラーがなく、普通にやりとりできました。

コードの例

      class LineController < ApplicationController
        CHANNEL_ID = ENV['CHANNEL_ID']
        CHANNEL_SECRET = ENV['CHANNEL_SECRET']
        CHANNEL_MID = ENV['CHANNEL_MID']
        OUTBOUND_PROXY = ENV['OUTBOUD_PROXY']

        def callback
          unless is_validate_signature
            render :nothing => true, status: 470
          end
          result = params[:result][0]

          if result['content']['opType'].present?

            mid = result['content']['params'][0]

            client = LineService.new(CHANNEL_ID, CHANNEL_SECRET, CHANNEL_MID, OUTBOUND_PROXY)
            res = client.profile(mid)

          else
            text_message = result['content']['text']
            from_mid = result['content']['from']
            arr = [from_mid]
            message = NlpService.make_response_text(text_message)

            client = LineService.new(CHANNEL_ID, CHANNEL_SECRET, CHANNEL_MID, OUTBOUND_PROXY)
            res = client.send(arr, message)
          end
          render :nothing => true, status: :ok
        end

        private
        def is_validate_signature
          signature = request.headers["X-LINE-ChannelSignature"]
          http_request_body = request.raw_post
          hash = OpenSSL::HMAC::digest(OpenSSL::Digest::SHA256.new, CHANNEL_SECRET, http_request_body)
          signature_answer = Base64.strict_encode64(hash)
          signature == signature_answer
        end
      end
require "faraday"
require "faraday_middleware"
require "json"
require "pp"

class LineService
  module ContentType
    TEXT = 1
    IMAGE = 2
    VIDEO = 3
    AUDIO = 4
    LOCATION = 7
    STICKER = 8
    CONTACT = 10
  end
  module ToType
    USER = 1
  end

  END_POINT = "https://trialbot-api.line.me"
  TO_CHANNEL = "1383378250" # Fixed value
  EVENT_TYPE = "138311608800106203" # Fixed value

  def initialize(channel_id, channel_secret, channel_mid, proxy = nil)
    @channel_id = channel_id
    @channel_secret = channel_secret
    @channel_mid = channel_mid
    @proxy = proxy
  end

  def post(path, data)
    client = Faraday.new(:url => END_POINT) do |conn|
      conn.request :json
      conn.response :json, :content_type => /\bjson$/
      conn.adapter Faraday.default_adapter
      conn.proxy @proxy
    end

    res = client.post do |request|
      request.url path
      request.headers = {
          'Content-type': 'application/json; charset=UTF-8',
          'X-Line-ChannelID': @channel_id,
          'X-Line-ChannelSecret': @channel_secret,
          'X-Line-Trusted-User-With-ACL': @channel_mid
      }
      request.body = data
    end
    res
  end

  def send(line_ids, message)
    post('/v1/events', {
        to: line_ids,
        content: {
            contentType: ContentType::TEXT,
            toType: ToType::USER,
            text: message
        },
        toChannel: TO_CHANNEL,
        eventType: EVENT_TYPE
    })
  end

end