さておき、世は年末年始あたりから空前のVTuberブームが到来しておりますね。
普段あまり動画や生放送を見る習慣がない人間なので、原作(?)を見ることはあまり多くないのですが、それでも日々TLに流れてくる二次創作や実況を見ているとその人気を肌で感じることが多いです。
で、中でも自分が気に入ってしまったVTuberが西荻窪在住の「鳩羽つぐ」ちゃん。キャラデザの可愛さはもちろんのことながら、登場後のファン界隈の動きなど含めた全体から溢れ出るサブカル感が最高ですね。
しかしこのキャラクター、恐ろしく公式からの供給が薄く、2月末のアカウント開設から今日に至るまでアップロードされた動画はたったの5本、うち2本は10秒足らずの短い動画で、大体において声も不鮮明で異様に音量も小さい、という有様。
受け手の妄想を捗らせるという点では非常に良く機能している宣伝戦略で、動画を追いかけるにあたって可処分時間が全く圧迫されないということでもあり、ある意味現代に完璧に適応している活動スタイルであると言えないこともありませんが、好きになったからにはもう少し定期的な供給が欲しいというのも人情。
幸いにも毎日のように様々の二次創作が作られている人気キャラとなっている彼女ではありますが、折角だから自分でも何かやってみたい。最近仕事でjs触ってることだしGASでbotでも作るか。という事でslackbotを作りました。
このように、西荻近辺のGoogleMap画像やストリートビューを無作為に定期的に投げつけてくるだけの謎botとなっております。オリジナルほどでないにせよちょっと余白があって良い感じではないでしょうか(自画自賛)
今回作成したアプリは
SlackAPI + GoogleAppsScript/スプレッドシート + GoogleStaticMapsAPI + StreetViewImageAPI
という感じの構成。GASがタイマーで定期的に起動してGoogleMap系のAPIに対して適当な座標情報を与え、取得した画像をSlackの所定チャンネル(#西荻窪)に投げるという感じの挙動になっております。
最初は一通りSlackBotの作成というあたりから細々説明やろうかと思っていたんですが、残業時間増えたりとか色々あって面倒になったので適当にコードだけ貼ってあとは軽めの説明という感じにします(雑)。
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function randomPost(){ | |
var rand = Math.round(random(0,2)); | |
if(rand == 0){ | |
postMapData(); | |
}else if(rand == 1){ | |
postStreetView(); | |
}else if(rand == 2){ | |
postMessage(); | |
} | |
} | |
function uploadImage(image,option){ | |
var slackApp = SlackApp.create(SLACK_API_TOKEN); | |
var result = slackApp.filesUpload(image, option) | |
Logger.log(result); | |
} | |
function postMessage(){ | |
var sheet = getSpreadSheet("テキスト"); | |
var lastRow = sheet.getLastRow(); | |
var index = random(1,lastRow); | |
var message = sheet.getRange(index, 1).getValue(); | |
var slackApp = SlackApp.create(SLACK_API_TOKEN); | |
// 対象チャンネル | |
var channelId = "#西荻窪"; | |
slackApp.postMessage(channelId, message); | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
function postMapData(){ | |
var image = createMapImage(); | |
var option = createMapOption(); | |
uploadImage(image,option); | |
} | |
function postStreetView(){ | |
var image = createStreetView(); | |
var option = createMapOption(); | |
uploadImage(image,option); | |
} | |
function createMapImage(){ | |
var place = getRandomPos("map"); | |
var header = "https://maps.googleapis.com/maps/api/staticmap"; | |
var mapCenter = "?center=" + place; | |
var mapZoom = "&zoom=" + Math.round(random(15,20)); | |
var imgSize = "&size=" + "600" + "x" + "600"; | |
var mapType = "&maptype=" + "roadmap"; | |
var decoration = "&markers=color:red%7C" + place; | |
var key = "&key=" + MAP_API_KEY; | |
var mapRequest = header + mapCenter + mapZoom + imgSize + mapType + decoration + key; | |
Logger.log(mapRequest); | |
var mapImage = UrlFetchApp.fetch(mapRequest).getBlob(); | |
return mapImage; | |
} | |
function createMapOption() { | |
var option={ | |
"content": "test" , | |
"content_type": "post", | |
"title" : "ここにはいません", | |
"channels": "西荻窪", | |
"filetype": "txt", | |
"mode": "snippet" | |
}; | |
return option; | |
} | |
function createStreetView(){ | |
var place = getRandomPos("streetView"); | |
var header = "https://maps.googleapis.com/maps/api/streetview"; | |
var imgSize = "?size=" + "600" + "x" + "600"; | |
var location = "&location=" + place; | |
var key = "&key=" + MAP_API_KEY; | |
var placeRequest = header + imgSize + location + key; | |
Logger.log(placeRequest); | |
var placeImage = UrlFetchApp.fetch(placeRequest).getBlob(); | |
return placeImage; | |
} | |
// ランダムに座標データを生成して返す | |
function getRandomPos(type){ | |
var sheet = getSpreadSheet("場所"); | |
var pos = "西荻窪"; | |
// ストリートビュー時のみ低確率で軍艦島出す | |
if(type == "streetView"){ | |
var rand = Math.round(random(0,10)); | |
if(rand == 9){ | |
pos = "軍艦島"; | |
}else if(rand == 10){ | |
pos = "軍艦島2"; | |
} | |
} | |
var tgtRow = searchRow(sheet,pos); | |
var topLeft = sheet.getRange(tgtRow, 3).getValue().split(","); | |
var btmRight = sheet.getRange(tgtRow, 4).getValue().split(","); | |
// 緯度経度をランダム設定 | |
var longitude = random(topLeft[0],btmRight[0]); | |
var latitude = random(topLeft[1],btmRight[1]); | |
var position = longitude + "," + latitude; | |
// ログ出力 | |
var time = Utilities.formatDate(new Date(), "Asia/Tokyo", "yyyy/MM/dd HH:mm:ss"); | |
var log = [time,position]; | |
getSpreadSheet("出力地点ログ").appendRow(log); | |
return position; | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// プロパティから設定したMAPのAPIキーを取得 | |
var MAP_API_KEY = PropertiesService.getScriptProperties().getProperty("MAP_API_KEY"); | |
// プロパティからSlackのAPIキーを取得 | |
var SLACK_API_TOKEN = PropertiesService.getScriptProperties().getProperty("SLACK_API_TOKEN"); | |
// プロパティから各種データのシートを取得 | |
var SHEET_ID = PropertiesService.getScriptProperties().getProperty("SHEET_ID"); | |
function random(min,max){ | |
min = Number(min); | |
max = Number(max); | |
return Math.random() * (max - min) + min; | |
} | |
// 指定したシートを取得する | |
function getSpreadSheet(sheetName){ | |
return SpreadsheetApp.openById(SHEET_ID).getSheetByName(sheetName); | |
} | |
// 該当の情報が該当シートの何行目にあるかを返す | |
function searchRow(sheet, key){ | |
var header = sheet.getRange(1, 1, sheet.getLastRow(), 1).getValues(); | |
for(var i = 0; i < header.length; i++){ | |
if(header[i].toString() === key){ | |
// シートの座標は1,1スタートなので | |
return i + 1; | |
} | |
} | |
return -1; | |
} | |
function test(){ | |
var sheet = getSpreadSheet("場所"); | |
var index = searchRow(sheet,"西荻窪"); | |
Logger.log(index); | |
} |
ざっくり言うと、main.gsのrandomPost()がGASのタイマー実行機能で定期的に呼び出されるようになっており、randomPost()は
- postMapData:マップ画像投稿
- postStreetView:ストリートビュー画像投稿
- postMessage:ポエム()テキスト投稿
という3つの関数のいずれかをランダムに呼び出すような仕組みです。
postMapDataとpostStreetViewは、どちらもgetRandomPos関数でランダムな座標データを生成してAPIに渡し、取得した画像をSlackに投稿する感じの仕組み。叩くAPIがStaticMapかStreetViewかの違い程度。
SlackやGoogleAPIなどのトークン類やスプレッドシートのアドレスなどはGASのスクリプトプロパティに格納し、utility.gsでデータへのアクセスなどを行うような感じにしています。
アクセスしているスプレッドシートは以下のような感じ。
で、マップにしてもテキストにしてもGASの機能でスプレッドシートを読みに行って、そのデータを利用するようにしているわけですが、これは実装とデータを分離することでメンテナンス性とか拡張性を上げたいとかそういう感じの理由によるもの。
実際テキストシートについては、思いついた文章を追加すればそれだけでランダムに選択対象に入るような作りとしているので、ここのメリットは感じられています。
GASはその出自の関係もあってGoogleDocとかその他のGoogleサービスとの連携が簡単に実装できるわけですが、特にスプレッドシートを簡易DBみたいに扱えるのはかなり便利なところだなあと今回改めて感じました。
適当なシートにログをバリバリ吐き出させるだけでも色々と捗る……

さて実際作って暫く運用してみた感じとしては、定期的に民家画像を投稿してくる謎botになったし鳩羽つぐ感はほぼねえな、というのが正直なところ。貼ったスクショのように時々ちょっとピンとくる感じのやつがある場合もあるんですが、打率がいかんせん低すぎる……
まあ現実世界における西荻窪は普通の住宅街ですし、ストリートビュー自体も基本的には昼で明るいタイミングに撮影しているわけなので、良い感じにダークな雰囲気を感じられる画像というのは当然ながらあんまり出してくれないんですよね。
テコ入れ的に軍艦島ストリートビューをたまに出すようにしているんですが、こちらは中々良い感じになっているので、Googleはぜひとも他の廃墟ロケーションにもガンガンストリートビューのカメラを入れていって欲しいですね(趣旨が違うのでは)
ちょっと結果としては微妙な感じになってしまいましたが、しかしまあこのまま運用していればいつか偶然例の魚屋(?)にランダム座標が直撃してくれるというワンダーが訪れることもあるかもしれないので、暫くは動かしっぱなしにしておきたいと思います。
なにはともあれ、mapとかstreetviewの画像取ってくるとかslackに投稿するというのがここまで簡単に短いコードで書けるというのは中々驚きでした。Web系のAPI文化の強みというのを感じる気がします。
仕事で触っているのと合わせて、Web系の技術というのもちょっと面白みが分かってきた感があるので、今後もなんか思いつくことがあればちょろっと書いて公開したりしたいなあと思ったところです。趣味で作って公開しときたい、というのはWeb系のプログラムに限らずですけれど。
以上、今回のコードは一応githubにも上げておいたので貼るだけ貼っておきます。
https://github.com/bluebird-kndk/TsuguBot
参考にしたもの・利用したもの
GASのコードをgithubで管理できるようにするやつ
https://techblog.recruitjobs.net/development/maneged_google-apps-script_by_github
Google map API関連(公式ドキュメントが超わかりやすいので他の説明いらない)
https://developers.google.com/maps/documentation/static-maps/intro?hl=ja
https://developers.google.com/maps/documentation/streetview/intro?hl=ja
Slackbot作成全般
http://vaaaaaanquish.hatenablog.com/entry/2017/09/27/184352
https://qiita.com/sublimer/items/2bf030248ab69e32b1f8
おわりでーす