#はじめに
ネタバレは含みません。なぜなら映画見る前に作ったから!笑
#動作イメージ
天気の子(陽奈)に地名を聞くと天気情報を答えてくれます。
これの後半側ですね。
前半側の「ねぇ、今から晴れるよ」は別記事に書きました。
#構成の解説
- Slack outgoing&incoming WebHook
- Slackの投稿内容からトリガーを引き、自動投稿するBOTのトリガー部分
SlackとGoogleAppsScript(GAS)を連携する手順・事例
-
GAS
- もろもろの処理を行う基盤。dopostてメソッド作るとweb公開した際に当該urlへPOSTされるとデータを受け取って処理が出来るんです。
-
Yahoo日本語形態素解析
- 日本語文を送ると分解して返してくれます。
- パラメータで名詞だけ返してくるようにして使ってます。
- 前に流行ったゲンジンメソッドはコトハ使ってましたが、これでも出来るはず(未実装)
- 他がことごとくjsonを使うなかxmlオンリーなので、ちょっと面倒です。
-
GoogleジオコーディングAPI
- 地名から緯度経度を取得するために利用。
- yahooで揃えたかったが、住所完全指定しての緯度経度を返すやつしかなかった。
- よってGoogleを採用。APPIDがまた一つ増えた。
-
YOLP 気象情報
- たぶんYahoo天気アプリで見れる降水地図と同じデータが取れるやつ。(予報は一時間後まで)
- 気象情報というくせに降水情報しか返してこない。
たいていのが緯度経度の順にパラメータ渡すけど、こいつはなぜか経度緯度。毎回間違う。。
-
DarkSky
- 気象情報API。YOLPが日本国内限定なのに対し全世界対応。
- こちらは降水だけじゃなく気温湿度から風向き、近くの嵐までの距離からオゾン?まですさまじい情報を返してくる。
- こっちも降水情報を持ってるけどYOLPを信頼しているので利用せず。
-
GoogleリバースジオコーディングAPI
- 緯度経度から住所を返してくれる。
-
SlackInComingWebHook
- Slackに投稿する用のwebhook。
#コード
あとの細かいことはコードから感じてください。
ここわからんから補足してってオーダーあれば追記します。
いろいろなところから頂いたコードのキメラなんですが、
出典を追えなくなってしまっています。指摘頂ければ出典に追加します。すみません。。
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の細かいところ書こうかと思います。
おたのしみに!