LoginSignup
4
4

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-09-11

はじめに

ネタバレは含みません。なぜなら映画見る前に作ったから!笑

動作イメージ

天気の子(陽奈)に地名を聞くと天気情報を答えてくれます。
hareruyo.png

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

陽菜が「ねぇ、今から晴れるよ?」という瞬間を捉える

構成

陽菜のシステム構成図.PNG

構成の解説

  • Slack outgoing&incoming WebHook
    • Slackの投稿内容からトリガーを引き、自動投稿するBOTのトリガー部分

SlackとGoogleAppsScript(GAS)を連携する手順・事例

  • GAS

    • もろもろの処理を行う基盤。dopostてメソッド作るとweb公開した際に当該urlへPOSTされるとデータを受け取って処理が出来るんです。
  • Yahoo日本語形態素解析

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

Yahooデベロッパーネットワーク 日本語形態素解析

  • GoogleジオコーディングAPI

    • 地名から緯度経度を取得するために利用。
    • yahooで揃えたかったが、住所完全指定しての緯度経度を返すやつしかなかった。
    • よってGoogleを採用。APPIDがまた一つ増えた。
  • YOLP 気象情報

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

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

    • 緯度経度から住所を返してくれる。
  • SlackInComingWebHook

    • Slackに投稿する用のwebhook。

コード

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

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;
}

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

あとがき

色々詰まりましたが、特に2つの問題が発生したので紹介。

  • お茶の水問題
    「陽菜 ~の天気教えて」というワードを想定して、「の~」を削除する雑処理したためお茶の水がお茶になる問題

    • Yahoo形態素解析で名詞だけ取得するようにして対応。
  • 日本橋問題
    ジオコーディングAPIが一発目に東京の日本橋を返すため、大阪の日本橋の天気が分からない問題

    • 形態素解析により適切なワード抽出が出来、「大阪の日本橋」といった指定も可能になった

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

あとがき2

大事にそだてたbotには知性がやどります!
DECED885-FD3E-4E6E-81B3-F4C6E45E71A1.jpeg

4
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
4
4