山の天気を教えてくれるLinebotを作る on Heroku(Freeプラン)

登山仲間と登山に行くときに天気をググってURLをLineグループで共有することがよくあるので、少しでも手順を簡略化するために山の天気を教えてくれるLinebotを作ることにしました。

※注意
この記事ではHerokuやLINE Developersを使っています。
HerokuやLINE Developersは次々に仕様が変わっていくので料金など重要な情報は公式情報を確認するようにしてください。

---
2022/08/26追記
この記事で使っていたHerokuの無料プランは2022年11月に廃止されることが決定しました。

https://blog.heroku.com/next-chapter

---


# 仕様
Lineで「○○山　天気」とコメントすると、Linebotがその山の天気予報ページをコメントしてくれる。
botがコメントする天気ページは下記の「てんきとくらす」を使っています。山の天気がわかるので大変お世話になっているサイトです。
サイトポリシーに『・本サイトへのリンクは原則自由であり、事前連絡も必要ありません。』と書いてあるので堂々と使って大丈夫でしょう。
https://tenkura.n-kishou.co.jp/tk/policy.html

# システム構成

システム構成は下記の通りです。
<img width="809" alt="スクリーンショット 2019-07-25 14.45.51.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83424/77ddd04b-6e8c-06dd-9085-11e75707ea66.png">


## インフラ
Lineメッセージを解析して該当の天気URLを返却するAPIサーバーは、１アプリを動かすだけであれば料金がかからないherokuのFreeプランを使うことにしました。
DBも10,000行までであれば無料で使えます。
(※2019年7月時点)
https://jp.heroku.com/pricing

## Line Developers
Linebotを作るにはLine Developersに登録する必要があります。
https://developers.line.biz/

登録方法は下記記事と同じなのでこの記事では省略します。
[GCPを使って日次のLine push通知を作ってみた](https://qiita.com/ham0215/items/11bf99ba89c97b4ff155#line-developers)

上記の他にWebhookの設定が必要ですが、それはherokuの環境構築ができた後に設定するので後述します。

## 開発環境
Ruby on Railsで開発しました。
DBはherokuにあわせてpostgres。
ソース管理はheroku内のgithubを使うこともできるが、自分のgithubで管理したかったので自分のgithub経由でｈｅｒｏｋｕへデプロイするようにしました。
開発環境ではdockerを使っています。

# 開発

## 開発環境構築
Dockerを使って開発環境を構築します。
最初にDockerfile, docker-compose.yml, Gemfile, Gemfile.lockを作成します。

```Dockerfile:Dockerfile
FROM ruby:2.6.3

RUN curl -sL https://deb.nodesource.com/setup_10.x | bash -
RUN curl -sS https://dl.yarnpkg.com/debian/pubkey.gpg | apt-key add -
RUN echo "deb https://dl.yarnpkg.com/debian/ stable main" | tee /etc/apt/sources.list.d/yarn.list
RUN apt-get update -qq && apt-get install -y build-essential nodejs yarn postgresql-client

RUN mkdir -p /app
WORKDIR /app

COPY Gemfile Gemfile.lock ./

RUN gem update bundler
RUN bundle install
COPY . .

CMD ["rails", "server", "-b", "0.0.0.0"]
```

```yaml:docker-compose.yml
version: '3'
services:
  web:
    build: .
    command: bundle exec rails s -p 3000 -b '0.0.0.0'
    environment:
      DATABASE_HOST: db
      DATABASE_PORT: 5432
      DATABASE_USER: postgres
      DATABASE_PASSWORD:
    links:
      - db
    volumes:
      - .:/app
    ports:
      - 3000:3000
    tty: true
    stdin_open: true
  db:
    image: postgres:11.3
    ports:
      - "5432"
    volumes:
      - db-volume:/var/lib/postgresvolumes
volumes:
  db-volume:
```

Railsのバージョンはなんでもよかったので現時点で最新の6にしてみた。

```ruby:Gemfile
source 'https://rubygems.org'

gem 'rails', '6.0.0.rc1'
```
Gemfile.lockは空

ファイルが準備できたらdocker上にrails環境を構築します。
まずは環境をビルドします。

```sh
# build
docker-compose build
```
続いてdocker上にrails環境を生成します。

```sh
# rails new
docker-compose run web bundle exec rails new . --database=postgresql --skip-test
```
データベースの設定を行います。
development, test環境のアカウントは適当にpostgres/passwordにしました。(環境変数で変更もできます)
productionはherokuで発行されるurlを環境変数`DATABASE_URL`で指定できるようにしています。

```yaml:config/database.yml
default: &default
  adapter: postgresql
  encoding: unicode
development:
  <<: *default
  database: db_development
  host: <%= ENV.fetch('DATABASE_HOST') { 'localhost' } %>
  port: <%= ENV.fetch('DATABASE_PORT') { 5432 } %>
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: <%= ENV.fetch('DATABASE_USER') { 'postgres' } %>
  password: <%= ENV.fetch('DATABASE_PASSWORD') { 'password' } %>
test:
  <<: *default
  database: db_test
  host: <%= ENV.fetch('DATABASE_HOST') { 'localhost' } %>
  port: <%= ENV.fetch('DATABASE_PORT') { 5432 } %>
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: <%= ENV.fetch('DATABASE_USER') { 'postgres' } %>
  password: <%= ENV.fetch('DATABASE_PASSWORD') { 'password' } %>
production:
  <<: *default
  url: <%= ENV['DATABASE_URL'] %>
  database: db_production
```

rails環境の準備ができたのでサーバーを起動します。

```sh
# サーバー起動(docker-composeのcommandをバックグランドで起動)
docker-compose up --build -d
```

http://localhost:3000/ で接続確認。
下記が表示されれば疎通OKです。
<img width="952" alt="スクリーンショット 2019-07-25 14.59.37.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83424/3b00604e-1762-2bc8-5fac-0eaa0411e497.png">

## 実装
line-botを実装します。
rubyで実装する場合、'line-bot-api'というgemを使うと良いです。
githubにLinebotのサンプルもあるので参考にして作りました。sinatraを使っているのでRailsで作る場合は少し読み替える必要はあります。
https://github.com/line/line-bot-sdk-ruby

私は下記のように実装しました。基本、サンプルと同じように作っています。

```ruby:config/routes.rb
  post '/callback' => 'linebot#callback'
```
```ruby:app/models/yama.rb
#
# yamasテーブルに山の名称(name)、URLを作るためのcode、typeを保持しています。
#
class Yama < ApplicationRecord
  self.inheritance_column = :_type_disabled

  class << self
    def find_by_message(message)
      # あんまりイケてないけど全部の山の名称を１件ずつメッセージに含まれているか確認して
      # 含まれていたらその山情報を返却する。
      # 本当は全文検索とかした方が良いと思うけどデータ数少なくてこれでも困っていないのでとりあえずこれで;;
      all.find_each do |yama|
        return yama if message =~ /#{yama.name}/
      end
    end
  end

  def url
    "https://tenkura.n-kishou.co.jp/tk/kanko/kad.html?code=#{code}&type=#{type}&ba=hk"
  end
end
```
```ruby:app/controllers/linebot_controller.rb
class LinebotController < ApplicationController
  require 'line/bot'

  protect_from_forgery except: [:callback]

  def callback
    body = request.body.read

    signature = request.env['HTTP_X_LINE_SIGNATURE']
    unless client.validate_signature(body, signature)
      head :bad_request
      return
    end

    events = client.parse_events_from(body)

    events.each { |event|
      case event
      when Line::Bot::Event::Message
        case event.type
        when Line::Bot::Event::MessageType::Text
          # メッセージに"天気"という文字が入っているか確認。入っていなければSKIP
          next unless event.message['text'] =~ /天気/

          if yama = Yama.find_by_message(event.message['text'])
            # 山が見つかればメッセージを生成してreplyする
            reply_text = "#{yama.name}の天気\n#{yama.url}"
            message = {
              type: 'text',
              text: reply_text
            }
            client.reply_message(event['replyToken'], message)
          end
        end
      end
    }

    head :ok
  end

  private

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

実装が終わったら自身のgithubへPushしましょう。

# heroku

## 環境構築
### app新規作成
herokuのdashboardを開き、"Create new app"を選択します。
<img width="320" alt="スクリーンショット 2019-08-01 14.43.41.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83424/b7a23df3-158d-f599-dc04-95b21ce30605.png">

適当な名前をつけてappを作成します。
<img width="749" alt="スクリーンショット 2019-08-01 14.43.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83424/52e52931-a183-039e-5c7a-01392dacd7fa.png">

### Deployタブ
Deployment methodで"GitHub"を選択して、Connect to GitHubで先ほどpushしたリポジトリを選択します。
<img width="1260" alt="スクリーンショット 2019-08-01 14.45.46.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83424/afa3d5a5-d9d2-37b3-cc3e-a4758fa3b169.png">

### Resourcesタブ
Add-onesで"Heroku Postgres"を検索して選択します。
<img width="934" alt="スクリーンショット 2019-08-01 14.52.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83424/e22bae00-afcc-f16b-06fd-9402f33154cd.png">
Hobby Dev-Freeを選択して、Provision
<img width="453" alt="スクリーンショット 2019-08-01 14.53.16.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83424/fd7e5ef2-9b14-a3bb-7083-764be98ab0c8.png">
これでpostgresが利用できます。
念のため、同ページに記載されているEstimated Monthly Costが＄0.00（費用がかかっていない）ことも確認しておきましょう。
<img width="920" alt="スクリーンショット 2019-08-01 14.55.38.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83424/b74fa85e-77e5-7abb-190a-b535e8a71306.png">

### Postgres
先ほど作成したHeroku PostgresがリンクになっているのでそこをクリックしてPostgresの情報を確認します。
リンク先のSettingsタブにDatabase Credentialsという項目があるのでView Credentials...をクリックします。
その中に表示される"URI"をDB接続で利用します。
<img width="927" alt="スクリーンショット 2019-08-01 14.58.45.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83424/234bd4e6-c63f-78d6-ad47-4151875a4326.png">

### Settingsタブ
SettingsタブのConfig Varsに必要な環境変数を足します。
DATABASE_URLはすでに設定されていると思いますが、もしなければ先ほど確認したpostgresのURIを設定します。
LINE_CHANNEL_SECRET、LINE_CHANNEL_TOKENはLineDevelopersで作成したチャンネルページに記載されているものを設定します。（LINE_CHANNEL_TOKENはLINE Developersのアクセストークンのことです）
あとはRailsで必要な諸々の環境変数を設定します。
<img width="1224" alt="スクリーンショット 2019-08-01 15.04.20.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83424/a7ddb90d-8fa1-1b47-455e-9b90af817db6.png">

同じタブに"Domains and certificates"にURLが記載されているので、それに"/callback"をつけたものがWebhookURLです。

### Deploy
Deployタグの下部にある"Manual deploy"をクリックしてデプロイします。

## データを入れる
Postgresを使っているので必要に応じてデータを入れてください。
Rails consoleが使えるのでHerokuのドキュメントを参考にしてください。
https://devcenter.heroku.com/articles/getting-started-with-rails5#run-the-rails-console

# Webhookの設定
HerokuのSettingsタブで確認したWebhookURLをLINEDevelopersのWebhook URLに設定します。
<img width="1142" alt="スクリーンショット 2019-08-01 15.17.05.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/83424/a708a8d4-7860-815e-c38e-a3731bd00f59.png">

個別で利用するだけならこれだけでOKですが、もしLINEグループにこのチャンネルを友達追加したい場合はすぐ下にある"Botのグループトーク参加"を利用するにする必要があります。

# 最後に
ここまできたらあとはLine上で話しかけるだけです。Let's talking!!

実際に作成した時とこの記事を書くまでに時差があり、手順が間違っていたり抜けていたりするかもしれないのでおかしな点があればご指摘いただけるとありがたいです。
herokuのログは`heroku logs`で確認できるのでrailsがうまく動かない場合は参照してみてください。
https://devcenter.heroku.com/articles/getting-started-with-rails5#view-logs