【Ruby】毎日更新の天文写真が送信されるLINEbotを作ってみた

# はじめに
Messaging APIを使ったLINEbotでNASAの毎日更新される天文写真を送ってくれるものを作ってみました。さらに、飽き足らずに日付を選んでその日の天文写真も見られるようにしました。

#### 完成形
![ezgif.com-gif-maker (1).gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2549439/3093f4b0-3aab-5f20-f40c-2a941b472811.gif)



## 前提、環境など
[こちらの記事](https://qiita.com/Yuzu_Ginger/items/18edc58bd039fc19ad53)のサンプルコードが動かせることが前提です。
また、[こちらの記事](https://qiita.com/Yuzu_Ginger/items/675cca0f56a8ed8b4a23)の手順1でNASA APIキーを生成してください。

https://qiita.com/Yuzu_Ginger/items/18edc58bd039fc19ad53

https://qiita.com/Yuzu_Ginger/items/675cca0f56a8ed8b4a23


- Windows10
- Ruby `3.1.0`
- Sinatra
- Heroku
- Message API
- NASA apod API

# 手順
## 0. コードを理解する
以下は[こちらの記事](https://qiita.com/Yuzu_Ginger/items/18edc58bd039fc19ad53)の最後にあるサンプルコードを少し変えたものです。
```ruby:sample.rb
require 'bundler/setup'
require 'sinatra'
require 'line/bot'
require 'nasa-api'
require 'net/http'
require 'json'
require 'date'

get '/' do
  'hello world!'
end

def client
    @client ||= Line::Bot::Client.new { |config|
      config.channel_secret = ENV["LINE_CHANNEL_SECRET"]
      config.channel_token = ENV["LINE_CHANNEL_TOKEN"]
    }
end
  
post '/callback' do
    body = request.body.read
  
    signature = request.env['HTTP_X_LINE_SIGNATURE']
    unless client.validate_signature(body, signature)
      halt 400, {'Content-Type' => 'text/plain'}, 'Bad Request'
    end
  
    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
                # 1. ユーザからメッセージが送られた時の処理
            end
        when Line::Bot::Event::Postback
            # 2. ポストバックアクションが返ってきた時の処理
        end
    end
  
    "OK"
end
```
ポイントは、`when Line::Bot::Event::MessageType::Text` と `when Line::Bot::Event::Postback`です。インテンドに注意してください。
前者はユーザがメッセージを送信したときの処理を書きます。後者にはポストバックイベントが返ってきたときの処理を書きます。
ポストバックイベントは、テンプレートメッセージのボタンテンプレート、確認テンプレート、カルーセルテンプレートや、日付選択アクション、クイックリプライなどの機能を使ったときに返ってくるアクションです。

## 1. HerokuにNASAのAPIキーの環境変数を追記する
Herokuのこのアプリのsettingを開き、`Reveal Config Vars`をクリックします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2549439/276b4109-70a1-a6ac-67fe-94e544891218.png)
すでに、`LINE_CANNEL_SECRET`と `LINE_CHANNEL_TOKEN`の環境変数は設定されているはずなので(まだの場合は、[こちらの記事](https://qiita.com/Yuzu_Ginger/items/18edc58bd039fc19ad53)の手順を踏んでください)、`NASA_API_KEY`という環境変数も設定します。valueは自身のNASAのAPIキーです。生成していない場合は[こちらの記事](https://qiita.com/Yuzu_Ginger/items/675cca0f56a8ed8b4a23)の手順1を踏んでください。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2549439/25f88ac9-aefe-4ef4-7b2f-c995d7c5ab09.png)

## 2. とりあえずコードを書いてみる
sample.rb に以下を追記していきます。
```ruby
require 'bundler/setup'
require 'sinatra'
require 'line/bot'
require 'nasa_apod'
require 'net/http'
require 'json'
require 'date'

get '/' do
  'hello world!'
end

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

 # ここから追記----
def nasa(date)
    api_key = ENV["NASA_API_KEY"]
    if date == 0
        uri = URI.parse("https://api.nasa.gov/planetary/apod?api_key=#{api_key}")
    else
        uri = URI.parse("https://api.nasa.gov/planetary/apod?api_key=#{api_key}&date=#{date}")
    end
    json = Net::HTTP.get(uri)
    data = JSON.parse(json)
    return data["url"], data["date"], data["title"]
end
 # ここまで追記----

post '/callback' do
    body = request.body.read
  
    signature = request.env['HTTP_X_LINE_SIGNATURE']
    unless client.validate_signature(body, signature)
      halt 400, {'Content-Type' => 'text/plain'}, 'Bad Request'
    end
  
    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
                # ここから追記----
                if event.message['text'] == "今日の天文写真は？"
                    data = nasa(0)
                    url = data[0]
                    date = data[1]
                    title = data[2]
                    reply_image(event, url, date, title)
                elsif event.message['text'] == "あの日の天文写真は？"
                    today = Date.today
                    client.reply_message(event['replyToken'], select_date(today))
                end
                # ここまで追記----
            end
        when Line::Bot::Event::Postback
            # ここから追記----
            user_date = event['postback']['params']['date']
            data = nasa(user_date)
            url = data[0]
            date = data[1]
            title = data[2]
            reply_image(event, url, date, title)
            # ここまで追記----
        end
    end
  
    "OK"
end

 # 以下を追記----
def select_date(today)
    {
    "type": "template",
        "altText": "this is a buttons template",
        "template": {
            "type": "buttons",
            "title": "Please select a date",
            "text": "いつの天文写真を見ますか？",
            "actions": [
                {
                  "type": "datetimepicker",
                  "label": "select date",
                  "mode": "date",
                  "data": "action=datetemp&selectId=1",
                  "max": today,
                  "min": "1995-06-20"
                }
            ]
        }
    }
end

def reply_image(event, url, date, title)
    if url =~ /jpg/
        space_image = {
            type: 'image',
            originalContentUrl: url,
            previewImageUrl: url
        }
    else
        space_image = {
            type: 'text',
            text: url
        }
    end
    message = {
        type: 'text',
        text: "#{date}\n#{title}"
    }
    client.reply_message(event['replyToken'], [space_image, message])
end
```
最後にHerokuへのデプロイを忘れずに。
```bash
$ git add .
$ git commit -m "xxx"
$ git push heroku master
```
これで、LINE画面から「今日の天文写真は？」と送ると、今日の天文写真とタイトルが返ってきます！

### 解説
#### 場合を分けて考えてみる
大きく分けて、場合分けは以下の2通りです。
1. ユーザから「今日の天文写真は？」と送られてくる
2. ユーザから「あの日の天文写真は？」と送られてくる

1 は今日の天文写真を返します。2 は日付を選んでその日の天文写真を返します。
では、1つずつ見ていきましょう。

##### 1. ユーザから「今日の天文写真は？」と送られてくる
まず、`if event.message['text'] == "今日の天文写真は？"`の処理に入ります。nasaメソッドで今日の天文写真のurl、date、titleが返ってきます。その後、reply_image メソッドで応答メッセージとして、天文写真、日付、タイトルがトーク画面に表示されます。
`client.reply_message(event['replyToken'], [space_image, message])`と応答メッセージを配列にすることで、吹き出しが2個になります。

##### 2. ユーザから「あの日の天文写真は？」と送られてくる
まず、`elsif event.message['text'] == "あの日の天文写真は？"`の処理に入ります。select_date メソッドは日付選択アクションです。日付選択アクションは日付を選択できます。時間まで指定することもできます。詳しくは[こちら](https://developers.line.biz/ja/reference/messaging-api/#datetime-picker-action)。
こちらはiOS版での表示です。Androidでは1か月ごとのカレンダーが表示されるようです。
<img width="300" alt="20220522_052415000_iOS.jpg" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2549439/0682d643-6f89-63d0-a4c3-42b5fe5b9798.jpeg">

日付が選択されると、ポストバックイベントが返ってきます。`user_date = event['postback']['params']['date']`には選択された日付のデータが入っています。JSON形式で以下のようになっています。詳しくは[こちら](https://developers.line.biz/ja/reference/messaging-api/#postback-event)です。
あとは 1 と同様です。
```json
{
    "destination": "xxxxxxxxxx",
    "events": [
        {
            "replyToken": "b60d432864f44d079f6d8efe86cf404b",
            "type": "postback",
            "mode": "active",
            "source": {
                "userId": "U91eeaf62d...",
                "type": "user"
            },
            "timestamp": 1513669370317,
            "webhookEventId": "01FZ74A0TDDPYRVKNK77XKC3ZR",
            "deliveryContext": {
                "isRedelivery": false
            },
            "postback": {
                "data": "storeId=12345",
                "params": {
                    "date": "2017-12-25"
                }
            }
        }
    ]
}
```
[Messaging APIリファレンス-日付選択アクション](https://developers.line.biz/ja/reference/messaging-api/#datetime-picker-action)
[Messaging APIリファレンス-ポストバックイベント](https://developers.line.biz/ja/reference/messaging-api/#postback-event)

<br>

##### <font color=red>2022/05/23 追記 RubyにおけるMessaging APIのポストバックイベントの返り値</font>
postbackイベントのcase whenの記述の中で、`p event`を追記し、event には何が入っているのか確かめてみました。結果は以下になります。改行はありませんでしたが、見やすいように適宜改行を入れています。また、`...`となっているところは文字が続きます。
```ruby:p event
 #<Line::Bot::Event::Postback:0x0000561e.....
@src = {
        "type"=>"postback",
        "postback"=>{"data"=>"action=datetemp&selectId=1","params"=>{"date"=>"2020-02-23"}},
        "webhookEventId"=>"01G3RJ9KE9.....",
        "deliveryContext"=>{"isRedelivery"=>false},
        "timestamp"=>165331.....,
        "source"=>{"type"=>"user", "userId"=>"U53ff33106....."},
        "replyToken"=>"9ef9a......",
        "mode"=>"active"
      }
>
```
こうして見ると、項目は公式リファレンスにあったJSONの内容と同じで、Rubyではハッシュを使っていることがわかりました。(しかもハッシュの中にハッシュ...?)
`event['postback']['params']['date']`の記述がありましたが、これはハッシュのキーで値を取得しているということのようです。。ハッシュの中のハッシュ(?)なので、配列の二次元配列のようにキーを並べて値を取得しているということでしょうか。
さらに、応答メッセージを送る際、必ず`client.reply_message(event['replyToken'],...`という記述が必要なのですが、これもハッシュの中の`replyToken`というキーから値を取得しているということがわかりました。
今までMessaging APIの[RubyのGithub](https://github.com/line/line-bot-sdk-ruby)のサンプルコードから試行錯誤して動けば正しい精神で書いていたので、ポストバックイベントの返り値を確かめたことで少しすっきりしました。

LINEbotの作成にあたって、Rubyの出力`p`や`puts`を書いてもLINEのトーク画面に表示されることがないことに注意してください。それらの出力は、ターミナルで以下のコマンドを実行すると見ることができます。
```bash
$ heroku logs --tail
```
`--tail`のあとにHerokuで設定したアプリ名(`heroku create`のときに決めたもの。忘れたらHerokuの自分のページに行くとアプリ一覧が見られます)を追記することで、アプリを指定して見ることもできます。ない場合は現在の階層にあるプログラムになります。
このコマンドはリアルタイムでログを確認するものです。`p`や`puts`の出力結果が見られるほか、エラーがあればエラー文も表示されます。LINEbotが動かなかったり、変数の中身を確認したいときなどに使います。

## リッチメニューを設定する
LINE Official Account Manager でリッチメニューを設定します。リッチメニューの設定の仕方は[こちら](https://www.linebiz.com/jp/manual/OfficialAccountManager/rich-menus/)を参考にしてください。
ここで2つのメニューを作り、タップすると「今日の天文写真は？」と「あの日の天文写真は？」というテキストが送信されるようにしました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2549439/34fd6600-bc37-77ab-87a2-833c4fbd7d21.png)

https://www.linebiz.com/jp/manual/OfficialAccountManager/rich-menus/

# 最後に
ちなみに、Herokuの設定で地域がUSになっているため、時間がUSの標準時間になっています。しかし、NASAの天文写真もUSの時間で更新されるので丁度良かったかなと思っています。
また、今回のNASAのAPIから情報を取得するとき、1時間あたりの制限は1,000リクエストです。[こちらの記事](https://qiita.com/Yuzu_Ginger/items/675cca0f56a8ed8b4a23)と取得方法が違うので制限回数が違います。

というわけで、天文写真が送信されるLINEbotを作ることができました。毎日ワンタップするだけで綺麗な天文写真が送られてきます。自分の誕生日などの天文写真を調べてみるのもおすすめです！(私の誕生日は美しい星空などではなく、よくわからない写真でした(笑))