陽菜(天気の子)に話しかけると天気を教えてくれるSlackBotをつくったよ

#はじめに
ネタバレは含みません。なぜなら映画見る前に作ったから！笑
#動作イメージ
天気の子(陽奈)に地名を聞くと天気情報を答えてくれます。
<img width="485" alt="hareruyo.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101834/b8301786-9b91-1b5f-733f-d92d5af14694.png">

これの後半側ですね。
前半側の「ねぇ、今から晴れるよ」は別記事に書きました。

[陽菜が「ねぇ、今から晴れるよ？」という瞬間を捉える](https://qiita.com/konya/items/c9b201e16bd7bdb8d9ed)

#構成
<img width="563" alt="陽菜のシステム構成図.PNG" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101834/57e12178-db36-c9d2-a2cd-c2c7a9641a21.png">


#構成の解説

- Slack outgoing&incoming WebHook
    - Slackの投稿内容からトリガーを引き、自動投稿するBOTのトリガー部分
 
[SlackとGoogleAppsScript（GAS）を連携する手順・事例](https://tech.tabilabo.co.jp/598/)


- GAS
    - もろもろの処理を行う基盤。dopostてメソッド作るとweb公開した際に当該urlへPOSTされるとデータを受け取って処理が出来るんです。

- Yahoo日本語形態素解析
    - 日本語文を送ると分解して返してくれます。
    - パラメータで名詞だけ返してくるようにして使ってます。
    - 前に流行ったゲンジンメソッドはコトハ使ってましたが、これでも出来るはず(未実装)
    - 他がことごとくjsonを使うなかxmlオンリーなので、ちょっと面倒です。

[Yahooデベロッパーネットワーク　日本語形態素解析](https://developer.yahoo.co.jp/webapi/jlp/ma/v1/parse.html)

- GoogleジオコーディングAPI
    - 地名から緯度経度を取得するために利用。
    - yahooで揃えたかったが、住所完全指定しての緯度経度を返すやつしかなかった。
    - よってGoogleを採用。APPIDがまた一つ増えた。


- YOLP 気象情報
    - たぶんYahoo天気アプリで見れる降水地図と同じデータが取れるやつ。(予報は一時間後まで)
    - 気象情報というくせに降水情報しか返してこない。
たいていのが緯度経度の順にパラメータ渡すけど、こいつはなぜか経度緯度。毎回間違う。。

- DarkSky
    - 気象情報API。YOLPが日本国内限定なのに対し全世界対応。
    - こちらは降水だけじゃなく気温湿度から風向き、近くの嵐までの距離からオゾン？まですさまじい情報を返してくる。
    - こっちも降水情報を持ってるけどYOLPを信頼しているので利用せず。
  
- GoogleリバースジオコーディングAPI
    - 緯度経度から住所を返してくれる。
 
- SlackInComingWebHook
  - Slackに投稿する用のwebhook。

#コード
あとの細かいことはコードから感じてください。
ここわからんから補足してってオーダーあれば追記します。
いろいろなところから頂いたコードのキメラなんですが、
出典を追えなくなってしまっています。指摘頂ければ出典に追加します。すみません。。

```js:hina.gs

  var SLACK_TOKEN = "";
  var WEBHOOK_TOKEN = "";
  
// doPost関数はpostをキャッチする関数のらしいです(GASのデフォルト関数)
function doPost(e) { // eにはPOSTされた投稿情報が格納されています。
  

  var tokens = 
  {
    "zero":{
        "webhookToken":"zzzzzzzzzzzzzzzzzzzzzzzz",
        "legacyToken":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
      }
  };
  //トークンとか想定されたものか見てるよ、複数Slack対応用にForで回してるよ。
  for (var item in tokens) {
    if(tokens[item]['webhookToken'] == e.parameter.token){
      SLACK_TOKEN   = tokens[item]['legacyToken']
      WEBHOOK_TOKEN = tokens[item]['webhookToken']
    }
  }
  if (e.parameter.user_name != "slackbot"){
    //Slack投稿内容を取得して整形するよ
    var text = e.parameter.text;
    var place = text.replace( '陽菜', '' )
    place = place.replace(/\s+/g, "");
    
    //形態素解析でさらに整形するよ。
    var placeArray = callJapaneseParse(place);
    var placeName = "";
    if(placeArray.length > 1){
      placeArray.forEach(
        function(meishi){
          placeName = placeName + meishi[0]+" ";
        }
      );
    }else{
      placeName = placeArray[0][0];
    }
    //一回応答してあげるよ
    postMessage(e.parameter.token, e.parameter.channel_name, placeName  +"ね、ちょっと待ってて:pray: ");
    //地名Toジオコード問い合わせだよ
    var geo = getPlace2Geo(place);
    if (geo==false){ 
       postMessage(e.parameter.token, e.parameter.channel_name, "うーん、" + placeName  +"がわかんなかった");
    }else{
      //気温・体感気温・湿度をdarkskyに聞くよ
      var message = callDarkSky(geo["lat"],geo["lng"]);
      //リバジオして住所を追加するよ
      var revGeoAddress = callReverseGeo(geo["lat"],geo["lng"]);
      //雨をYOLPに聞くよ
      var ame ="";
      var js = callYOLP(geo["lng"],geo["lat"]);
      js.Feature[0].Property.WeatherList.Weather.forEach(
        function(item){
          //降水量があるならmm数と時刻を追加するよ
          if(item.Rainfall > 0){ 
           ame = ame + item.Date.slice(-4) + "に" + item.Rainfall + "mm/h "  ; 
          }
        }
      );   
      //雨予報があるならポストに追加するよ
      if(ame.length > 1){
          ame = "雨は" + ame  + "降るって\r\n"; 
      }else{
          ame = "雨は降らないみたい！\r\n"
      }
      postMessage(e.parameter.token, e.parameter.channel_name, ame + message + "って感じ:heart: \r\nちなみに"+revGeoAddress+"のことよ！");
    }
  }
}
 
//Post massage(Slackに投稿する関数)
var postMessage = function(webhook_token,channel_name, text){
  // POSTされたwebhook_tokenと、設定したOutgoing Webhooksのwebhook_tokenを照合し、異なる場合はエラーを通知します。
  // GASのendpointを知られてしまい、勝手にGASにPOSTされた場合への対処です。
  if (WEBHOOK_TOKEN != webhook_token) {
    throw new Error("invalid token."); //エラーを通知します
  }
  PropertiesService.getScriptProperties().setProperty("token", SLACK_TOKEN);
  var prop =  PropertiesService.getScriptProperties().getProperties();
  
  // slackAppライブラリを利用してslackに投稿
  var slackApp = SlackApp.create(prop.token);
  slackApp.postMessage("#"+channel_name, text, {
    username : "陽菜", // 
    icon_url : "https://avatars.slack-edge.com/hoge.png"//outGoingWebhookに使ったアイコンのURL
  });
  return null;
}
//地名を緯度経度にして返すよ
function getPlace2Geo(place){
  var url ="https://maps.googleapis.com/maps/api/geocode/json?";  
  var key = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";//グーグルのappid
  var response = UrlFetchApp.fetch(url + "address=" + place + "&key=" + key);
  var js = JSON.parse(response.getContentText());
  if (js["status"] == "OK"){
    var location = js["results"]["0"]["geometry"]["location"];
    return location;
  }
  return false;
}
//ヤフー形態素解析をたたくよ
function callJapaneseParse(text){
  var url="https://jlp.yahooapis.jp/MAService/V1/parse";
  var appId = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";//ヤフーのappid
  var url = url + "?results=ma&filter=9&appid=" + appId + "&sentence=" + text;
  var myXml = UrlFetchApp.fetch(url);
  var myDoc = XmlService.parse(myXml.getContentText());
  var namespace = XmlService.getNamespace("urn:yahoo:jp:jlp");
  var root = myDoc.getRootElement();
  var ma_result = root.getChild("ma_result", namespace);
  var word_list = ma_result.getChild("word_list", namespace);
  var word_array = word_list.getChildren("word", namespace);
  var surface = word_array[0].getChild("surface", namespace);

  var array = [];
  for (var i=0; i < word_array.length; i++) {
    if(word_array[i].getAllContent()[0].asElement().getText() != "天気"){//～の天気教えて！って言われるのを想定した対策
      array[i] = [
        word_array[i].getAllContent()[0].asElement().getText(),
        word_array[i].getAllContent()[1].asElement().getText(),
        word_array[i].getAllContent()[2].asElement().getText()
      ];
    }
  }
  return array;
}
//緯度経度をもらって住所をかえすよ
function callReverseGeo(lat,lng){
  var url ="https://maps.googleapis.com/maps/api/geocode/json?language=ja&";  
  var key = "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb";//グーグルのappid
  url = url + "latlng=" + lat + "," + lng + "&key=" + key;
  var response = UrlFetchApp.fetch(url);
  var js = JSON.parse(response.getContentText());
  if (js["status"] == "OK"){
    var address = js["results"]["0"]["formatted_address"];
    return address;
  }
  return false;
}
//緯度経度をもらってYOLPの降水量API叩いて返すよ
//パラメータを緯度経度にしてるけど、実は逆だよ。idoに経度がはいるよ。
function callYOLP(ido,keido){
  var appId = "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";//ヤフーのappid
  var secret = "yyy"; // 使わなかった  
  var url = "https://map.yahooapis.jp/weather/V1/place?coordinates=" + ido + "," + keido + "&output=json&appid=" + appId;
  var response = UrlFetchApp.fetch(url);
  var js = JSON.parse(response.getContentText());
  return js;
}  


//緯度経度をもらってダークスカイに問い合わせした結果を返すよ
function callDarkSky(ido,keido){
  var url = "https://api.darksky.net/forecast/yyyyyyyyyyyyyyyyyyyyyyyyyyyyyyyy/"+ ido + "," + keido;//yyはdarkskyのappid
  var response = UrlFetchApp.fetch(url);
  var js = JSON.parse(response.getContentText());
  var apparentTemperature = (5/9) * (js["currently"]["apparentTemperature"]-32); // 華氏を摂氏に変換
  var currentTemperture = (5/9) * (js["currently"]["temperature"]-32); // 華氏を摂氏に変換
  var currentHumidity = js["currently"]["humidity"] * 100;
  console.log(currentTemperture)
  console.log(currentHumidity)
  console.log(apparentTemperature)
  var res = "気温" + Math.round(currentTemperture * 100)/100 + "℃ 体感気温"+ Math.round(apparentTemperature * 100)/100 +"℃ 湿度" +  currentHumidity +"%";
  return res;
}
```

微妙にスクリーンショットと発言内容が異なるのは、映画を見てキャラ設定をブラッシュアップしたせいです笑

#あとがき
色々詰まりましたが、特に２つの問題が発生したので紹介。

- お茶の水問題
「陽菜　～の天気教えて」というワードを想定して、「の～」を削除する雑処理したためお茶の水がお茶になる問題
  - Yahoo形態素解析で名詞だけ取得するようにして対応。

- 日本橋問題
ジオコーディングAPIが一発目に東京の日本橋を返すため、大阪の日本橋の天気が分からない問題
  - 形態素解析により適切なワード抽出が出来、「大阪の日本橋」といった指定も可能になった 


あと、実はこのbot少しずつ成長させてきたものなので、成長記書きます。
そちらに各APIの細かいところ書こうかと思います。
おたのしみに！

#あとがき２
大事にそだてたbotには知性がやどります！
![DECED885-FD3E-4E6E-81B3-F4C6E45E71A1.jpeg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/101834/34b96d51-f0cd-a878-51a5-130634375dc4.jpeg)