LINE Messaging APIとGASで作るポケモンBoT

## ポケモンBoTとは
LINEでポケモンの名前を言うと、博士が教えてくれます。
その他にも、1文字だけ言うとその文字で始まるポケモンを教えてくれたり、ハッシュタグ風に言うとポケモンしりとりをしてくれます。

<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2527615/5d19d83f-49ec-85ae-dc92-d167c00332bf.png">

<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2527615/1c3fd132-0371-753f-0b42-fa0f31438aa4.png">

<img width="200" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2527615/8e0e29a4-5953-603b-d314-9285ff8e7862.png">


## システム概要
LINE Messaging APIとGASを使ってます。
![pokemon-bot (1).jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2527615/bf135d72-dad9-2ac3-b9fa-66e4f96512a4.jpeg)

## GASでやること
GASではLINE Messaging APIから情報を受け取り、ポケモンデータを生成してレスポンスします。
ポケモンのマスタデータは [pokemon_data](https://github.com/kotofurumiya/pokemon_data)、画像データは https://pokeapi.co/ を利用させていただいております。
タイプデータはこちらをご利用ください。
```json:pokemon_type.json
{"ノーマル":
  {"こうげき":{"○":[],"△":["いわ","はがね"],"×":["ゴースト"]},
   "ぼうぎょ":{"○":["かくとう"],"△":[],"×":["ゴースト"]}
  }
,"ほのお":
  {"こうげき":{"○":["くさ","こおり","むし","はがね"],"△":["ほのお","みず","いわ","ドラゴン"],"×":["ゴースト"]},
   "ぼうぎょ":{"○":["みず","じめん","いわ"],"△":["ほのお","くさ","こおり","むし","はがね","フェアリー"],"×":[]}
  }
,"みず":
  {"こうげき":{"○":["ほのお","じめん","いわ"],"△":["みず","くさ","ドラゴン"],"×":[]},
   "ぼうぎょ":{"○":["でんき","くさ"],"△":["ほのお","みず","こおり","はがね"],"×":[]}
  }
,"でんき":
  {"こうげき":{"○":["みず","ひこう"],"△":["でんき","くさ","ドラゴン"],"×":["じめん"]},
   "ぼうぎょ":{"○":["じめん"],"△":["でんき","ひこう","はがね"],"×":[]}
  }
,"くさ":
  {"ぼうぎょ":{"○":["ほのお","こおり","どく","ひこう","むし"],"△":["みず","でんき","くさ","じめん"],"×":[]}}
,"こおり":
  {"ぼうぎょ":{"○":["ほのお","かくとう","いわ","はがね"],"△":["こおり"],"×":[]}}
,"かくとう":
  {"ぼうぎょ":{"○":["ひこう","エスパー","フェアリー"],"△":["むし","いわ","あく"],"×":[]}}
,"どく":
  {"ぼうぎょ":{"○":["じめん","エスパー"],"△":["くさ","かくとう","どく","むし","フェアリー"],"×":[]}}
,"じめん":
  {"ぼうぎょ":{"○":["みず","くさ","こおり"],"△":["どく","いわ"],"×":["でんき"]}}
,"ひこう":
  {"こうげき":{"○":["くさ","かくとう","むし"],"△":["でんき","いわ","はがね"],"×":[]},
   "ぼうぎょ":{"○":["でんき","こおり","いわ"],"△":["くさ","かくとう","むし"],"×":["じめん"]}
  }
,"エスパー":{"ぼうぎょ":{"○":["むし","ゴースト","あく"],"△":["かくとう","エスパー"],"×":[]}}
,"むし":{"ぼうぎょ":{"○":["ほのお","ひこう","いわ"],"△":["くさ","かくとう","じめん"],"×":[]}}
,"いわ":{"ぼうぎょ":{"○":["みず","くさ","かくとう","じめん","はがね"],"△":["ノーマル","ほのお","どく","ひこう"],"×":[]}}
,"ゴースト":{"ぼうぎょ":{"○":["ゴースト","あく"],"△":["どく","むし"],"×":["ノーマル","かくとう"]}}
,"ドラゴン":{"ぼうぎょ":{"○":["こおり","ドラゴン","フェアリー"],"△":["ほのお","みず","でんき","くさ"],"×":[]}}
,"あく":{"ぼうぎょ":{"○":["かくとう","むし","フェアリー"],"△":["ゴースト","あく"],"×":["エスパー"]}}
,"はがね":{"ぼうぎょ":{"○":["ほのお","かくとう","じめん"],"△":["ノーマル","くさ","こおり","ひこう","エスパー","むし","いわ","ドラゴン","はがね","フェアリー"],"×":["どく"]}}
,"フェアリー":{"ぼうぎょ":{"○":["どく","はがね"],"△":["かくとう","むし","あく"],"×":["ドラゴン"]}}
}
```

### jsonデータを配置
マイドライブに pokemon_data.json, pokemon_type.json を置きます。
<img width="400" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2527615/76f81219-2a46-ee4d-8582-fdde140568cb.png">

### スクリプトを作成しデプロイ
勢いで書きました。
<details><summary>Apps Script</summary>

```javascript
function doPost(e) {

  //リプライ用トークン(LINE
  var reply_token = "";

  var newLine = "<br/>";

  try{ 
    Logger.log('処理開始 PokemonBOT');
        
    //投稿されたコメント
    var content = "";

    try {
      //LINEからの通知処理かどうか
      reply_token= JSON.parse(e.postData.contents).events[0].replyToken;
      if (typeof reply_token === 'undefined') {
        Logger.log('reply_token === undefined');
        return;
      }
    } catch(e) {
      return;
    }

    try {  
      //Botに送信されたメッセージ
      content = JSON.parse(e.postData.contents).events[0].message.text;
      Logger.log(content);

      if (typeof content === 'undefined') {
        throw new Error();
      }
      
      //ログをクリア
      getLogFile().getBody().clear();      
      
      Logger.log('処理開始 PokemonBOT LINE');
      newLine = "\n";
      
    } catch(e) {
      replyMessage('メッセージが読み取れませんでした。＼(^o^)／', 54, reply_token);
      Logger.log('LINEからのメッセージが読み取れなかったため処理中止します。');
      Logger.log('処理終了 PokeonBOT');
      return;
    }

    ////////////////////////////////////////////////////////////////
    //コメントが1文字だった場合は、その文字ではじまるポケモンをすべて返す
    if (content.length == 1) {
      var message = "";
      var pokemonDataList = getPokemonDataList(hiraToKana(content));
      
      //ポケモンデータ整形
      if (pokemonDataList.length > 0) {
        for(var i = 0; i < pokemonDataList.length; i++) {
          message += '[' + pokemonDataList[i].no + '] ' + pokemonDataList[i].name;
          if (pokemonDataList[i].form) {
            message += '(' + pokemonDataList[i].form + ')';
          }      
          message += newLine;
        }
        message = message.slice( 0, - newLine.length );
      } else {
        message +=　userName +  '「' + content + '」で始まるポケモンはいませんでした＼(^o^)／';
      }
      //画像はリストがあれば1個目のポケモンにして、なければコダック
      replyMessage(message, pokemonDataList.length ? pokemonDataList[0].no : 54 , reply_token);
      Logger.log(message);
      Logger.log('処理終了 PokemonBOT');
      return;
    }

    ////////////////////////////////////////////////////////////////
    //コメントが#はじまりだった場合は、ポケモンしりとりをする
    var firstStr =　content.slice(0, 1); 
    if (firstStr == "#" || firstStr == "＃") {
      content =　content.slice(1); 
    }
    
    //ポケモンデータ取得
    var pokemonData = getPokemonData(hiraToKana(content));

    if (!pokemonData) {
      replyMessage(userName + '「' + content + '」のポケモンはまだ観測されていません＼(^o^)／', 54, reply_token);
      Logger.log('処理終了 PokemonBOT (pokemonData not find.)');
      return;
    }

    ////////////////////////////////////////////////////////////////
    //ポケモンしりとりをする
    if (firstStr == "#" || firstStr == "＃") {
      var shiritoriList = getPokemonShiritori(hiraToKana(content), 5);
      //ポケモンデータ整形
      var message = hiraToKana(content) + " からはじめるポケモンしりとり" + newLine + "「";
   
      for(var i = 0; i < shiritoriList.length; i++) {
        message += shiritoriList[i] + ', ';
      }
      message = message.slice( 0, -2 );
      if (shiritoriList.length == 1) {
        message += '...';
      }
      message += '」';
      replyMessage(message, pokemonData.no, reply_token);
      return;
    }

    //進化前のポケモン取得
    var pokemonBeforeData = getPokemonBeforeData(pokemonData.no);

    //進化後のポケモン取得
    var pokemonAfterData = getPokemonAfterData(pokemonData.evolutions);
    
    //ポケモンデータ整形
    var message = "";
    message += '番号　：' + pokemonData.no + newLine;
    message += '名前　：' + pokemonData.name + newLine;
    message += 'タイプ：' + pokemonData.types + newLine;
    if (pokemonBeforeData) {
      message += '進化前：' + pokemonBeforeData.name + newLine;
    }
    if (pokemonAfterData) {
      message += '進化後：' + pokemonAfterData.name + newLine;
    }
    //ダメージ
    var damageObj = getDamage(pokemonData.types);
    if (damageObj) {
      Logger.log(damageObj);
      message += "-- ダメージ --" + newLine;
      if ("◎" in damageObj) {message += "◎：" + damageObj["◎"] + newLine;}
      if ("○" in damageObj) {message += "○：" + damageObj["○"] + newLine;}
      if ("△" in damageObj) {message += "△：" + damageObj["△"] + newLine;}
      if ("×" in damageObj && damageObj["×"].length > 0) {message += "×：" + damageObj["×"] + newLine;}      
    } else {
      Logger.log("damageObj なし");
    }
    
    Logger.log(message);
    
    if (message) {
      replyMessage(message, pokemonData.no, reply_token);
    }
    
    Logger.log('処理終了 PokemonBOT');
  } catch (e) {
    Logger.log(e);
    replyMessage('エラーが発生しました。管理者にお問い合わせください＼(^o^)／', 54, reply_token);
  } finally {
    //ログ出力  
    getLogFile().getBody().appendParagraph(Logger.getLog());
    
    return ContentService.createTextOutput(JSON.stringify({'content': 'post ok'})).setMimeType(ContentService.MimeType.JSON);
  }
}

// ポケモンの名前からポケモンデータを取得
function getPokemonData(pokemonName) {
  Logger.log('function開始 getPokemonData()');
  var returnData = "";
  
  // Google Driveフォルダーからファイル一覧を取得
  var folder = DriveApp.getFolderById("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");
  var files = folder.getFiles();
  while (files.hasNext()) {
    var file = files.next();

    if (file.getName() != 'pokemon_data.json') {
      continue;
    }
    
    // JSONファイルをUTF-8として内容を取得
    var cont = file.getBlob().getDataAsString("utf-8");
    var json = JSON.parse(cont);
    for (var i = 0; i < json.length; i++) {
      if (isNaN(pokemonName)) {
        if (pokemonName == json[i].name) {
          Logger.log('ポケモンデータゲット!');
          Logger.log(json[i]);
          returnData = json[i];
          break;
        }
      } else {
        if (pokemonName == json[i].no) {
          Logger.log('ポケモンデータゲット!');
          Logger.log(json[i]);
          returnData = json[i];
          break;
        }
      }
    }
  }
  Logger.log('function終了 getPokemonData()');
  return returnData;
}

// 進化前ポケモン
function getPokemonBeforeData(no) {
  Logger.log('function開始 getPokemonBeforeData()');
  var returnData = "";

  // Google Driveフォルダーからファイル一覧を取得
  var folder = DriveApp.getFolderById("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");
  var files = folder.getFiles();
  while (files.hasNext()) {
    var file = files.next();

    if (file.getName() != 'pokemon_data.json') {
      continue;
    }
    
    // JSONファイルをUTF-8として内容を取得
    var cont = file.getBlob().getDataAsString("utf-8");
    var json = JSON.parse(cont);

    for (var i = 0; i < json.length; i++) {
      if (no == json[i].evolutions) {
        Logger.log('ポケモンデータゲット!');
        Logger.log(json[i]);
        returnData = json[i];
        break;
      }
    }
  }

  Logger.log('進化前ポケモンはありません');
  Logger.log('function終了 getPokemonBeforeData()');
  return returnData;
}

// 進化後ポケモン
function getPokemonAfterData(evolutions) {
  Logger.log('function開始 getPokemonAfterData()');
  var returnData = "";

  if (evolutions.length == 0) {
    Logger.log('進化後ポケモンはありません');
    return returnData;
  }
  
  // Google Driveフォルダーからファイル一覧を取得
  var folder = DriveApp.getFolderById("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");
  var files = folder.getFiles();
  while (files.hasNext()) {
    var file = files.next();

    if (file.getName() != 'pokemon_data.json') {
      continue;
    }
    
    // JSONファイルをUTF-8として内容を取得
    var cont = file.getBlob().getDataAsString("utf-8");
    var json = JSON.parse(cont);

    for (var i = 0; i < json.length; i++) {
      if (evolutions == json[i].no) {
        Logger.log('ポケモンデータゲット!');
        Logger.log(json[i]);
        returnData = json[i];
        break;
      }
    }
  }

  Logger.log('進化後ポケモンが取得できませんでした');
  Logger.log('function終了 getPokemonAfterData()');
  return returnData;
}

// 指定された文字(1文字)から始まるポケモンのリストを返す
function getPokemonDataList(startStr) {
  Logger.log('function開始 getPokemonDataList()');
  var returnDataList = [];
  
  // Google Driveフォルダーからファイル一覧を取得
  var folder = DriveApp.getFolderById("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");
  var files = folder.getFiles();
  while (files.hasNext()) {
    var file = files.next();

    if (file.getName() != 'pokemon_data.json') {
      continue;
    }
    
    // JSONファイルをUTF-8として内容を取得
    var cont = file.getBlob().getDataAsString("utf-8");
    var json = JSON.parse(cont);
    for (var i = 0; i < json.length; i++) {
      if (startsWith(json[i].name, startStr)) {
        Logger.log('ポケモンデータゲット! ' + i);
        returnDataList.push(json[i]);
      }
    }
  }
  Logger.log('function終了 getPokemonDataList()');
  return returnDataList;
}

// 指定したポケモンの名前でしりとりしたリストを取得
function getPokemonShiritori(name, count) { 
  Logger.log('function開始 getPokemonShiritori()');

  var pokemonList = getPokemonListRundom();

  var shiritoriList = [name];
  var _lastStr = shiritoriReplace(name).slice(-1) ;
  
  for (var i = 0; i < 5; i++) {
    for (var y = 0; y < pokemonList.length; y++) {
      var _name = pokemonList[y].name;
      
      // 最後の文字ではじまるポケモン
      if (startsWith(_name, _lastStr)) {
        // リストにないこと、ンで終わらないこと
        if (shiritoriList.indexOf(_name) == -1 && !endWith(_name, 'ン')) {
          shiritoriList.push(_name);
          _lastStr = shiritoriReplace(_name).slice(-1) ;
          break;
        }
      }
    }
  }
  Logger.log(shiritoriList);
  return shiritoriList;
}

// ポケモンのリストをシャッフルした状態で取得
function getPokemonListRundom() { 
  Logger.log('function開始 getPokemonListRundom()');
  
  // Google Driveフォルダーからファイル一覧を取得
  var folder = DriveApp.getFolderById("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");
  var files = folder.getFiles();
  while (files.hasNext()) {
    var file = files.next();

    if (file.getName() != 'pokemon_data.json') {
      continue;
    }
    
    // JSONファイルをUTF-8として内容を取得
    var cont = file.getBlob().getDataAsString("utf-8");
    var json = JSON.parse(cont);

    // シャッフル
    for(var i = json.length - 1; i > 0; i--){
      var r = Math.floor(Math.random() * (i + 1));
      var tmp = json[i];
      json[i] = json[r];
      json[r] = tmp;
    }
    return json;
  }  
}

// リプライ
function replyMessage(message, no, reply_token) {
  Logger.log('function開始 replyMessage()');

  //LINE developerで登録をした、自分のCHANNEL_ACCESS_TOKENを入れて下さい
  var CHANNEL_ACCESS_TOKEN = 'YOU_ACCESS_TOKEN';

  var url = 'https://api.line.me/v2/bot/message/reply';
  UrlFetchApp.fetch(url, {
    'headers': {
      'Content-Type': 'application/json; charset=UTF-8',
      'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN,
    },
    'method': 'post',
    'payload': JSON.stringify({
      'replyToken': reply_token,
      'messages': [{
        'type': 'text',
        'wrap': true, //textおりかえし
        'text': message
      }
                   ,{
                     "type": "image",
                     "originalContentUrl": 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/' + no + '.png',
                     "previewImageUrl": 'https://raw.githubusercontent.com/PokeAPI/sprites/master/sprites/pokemon/' + no + '.png'
                   }
                  ],
    }),
  });
}

//ひらがなをカタカナに変換
function hiraToKana(str) {
    return str.replace(/[\u3041-\u3096]/g, function(match) {
        var chr = match.charCodeAt(0) + 0x60;
        return String.fromCharCode(chr);
    });
}

//前方一致
function startsWith(target, pattern) {
  return target.indexOf(pattern) === 0; 
}

//後方一致
function endWith(target, pattern) {
  if((target.lastIndexOf(pattern)+pattern.length===target.length)&&(pattern.length<=target.length)){
    return true;
  }
  return false; 
}

//しりとり用文字変換
function shiritoriReplace(name) {
  var _name = name;
  replaceFm = new Array('ァ','ィ','ゥ','ェ','ォ','ャ','ュ','ョ','ッ','ー');
  replaceTo = new Array('ア','イ','ウ','エ','オ','ヤ','ユ','ヨ','ツ','');
  for (var key in replaceFm) {
    _name = _name.replace(new RegExp(replaceFm[key], 'g'),replaceTo[key]);
  }
  return _name;
}

function getJson(fileName) {
  // タイプjsonを取得
  // Google Driveにjsonをs配置したフォルダーのIDをセット
  var folder = DriveApp.getFolderById("XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX");
  var files = folder.getFiles();
  while (files.hasNext()) {
    var file = files.next();
    
    if (file.getName() != fileName) {
      continue;
    }
    
    // JSONファイルをUTF-8として内容を取得
    var cont = file.getBlob().getDataAsString("utf-8");
    return JSON.parse(cont);
  }
  Logger.log(fileName + ' が取得できませんでした');
  return;
}

//指定したタイプのダメージを返す
//みずとひこうの場合 -> {△=[ほのお, みず, はがね, かくとう, むし], ×=[じめん], ○=[いわ], ◎=[でんき]}
function getDamage(typeArr) {
  Logger.log('function開始 getDamage()');
  try {
    var typeJson = getJson("pokemon_type.json");
    var defenseArr = [];
    for(var i = 0; i < typeArr.length; i++) {    
      if (typeArr[i] in typeJson) {
        defenseArr.push(typeJson[typeArr[i]]["ぼうぎょ"]);
      }
    }
    
    if (defenseArr.length == 0) {
      return;
    }
    if (defenseArr.length == 1) {
      return defenseArr[0];
    }
    
    var damageObj = {};
    var defenseObj1 = defenseArr[0];
    var defenseObj2 = defenseArr[1];
    
    //1に2をマージして damageObj にセット
    if (defenseObj1["○"].length > 0){
      for(var i = 0; i < defenseObj1["○"].length; i++) {    
        var type1 = defenseObj1["○"][i];
        
        if (defenseObj2["○"].indexOf(type1) >= 0){
          //両方に存在するので二重弱点
          if (damageObj["◎"]) {
            damageObj["◎"].push(type1);
          } else {
            damageObj["◎"] = [type1];
          }
          continue;
        }
        if (defenseObj2["△"].indexOf(type1) >= 0){
          //○と△の場合は等倍ダメージになるので対象外
          continue;
        }
        if (defenseObj2["×"].indexOf(type1) >= 0){
          //○と×の場合は△扱い
          if (damageObj["△"]) {
            damageObj["△"].push(type1);
          } else {
            damageObj["△"] = [type1];
          }
          continue;
        }
        //1にしかない場合
        if (damageObj["○"]) {
          damageObj["○"].push(type1);
        } else {
          damageObj["○"] = [type1];
        }
      }
    }
    if (defenseObj1["△"].length > 0){
      for(var i = 0; i < defenseObj1["△"].length; i++) {    
        var type1 = defenseObj1["△"][i];
        
        if (defenseObj2["△"].indexOf(type1) >= 0){
          //両方に存在するので二重耐性
          if (damageObj["×"]) {
            damageObj["×"].push(type1);
          } else {
            damageObj["×"] = [type1];
          }
          continue;
        }
        if (defenseObj2["○"].indexOf(type1) >= 0){
          //○と△の場合は等倍ダメージになるので対象外
          continue;
        }
        if (defenseObj2["×"].indexOf(type1) >= 0){
          //△と×の場合は×扱いとする（三重耐性だが
          if (damageObj["×"]) {
            damageObj["×"].push(type1);
          } else {
            damageObj["×"] = [type1];
          }
          continue;
        }
        //1にしかない場合
        if (damageObj["△"]) {
          damageObj["△"].push(type1);
        } else {
          damageObj["△"] = [type1];
        }
      }
    }
    if (defenseObj1["×"].length > 0){
      for(var i = 0; i < defenseObj1["×"].length; i++) {    
        var type1 = defenseObj1["×"][i];
        
        if (defenseObj2["○"].indexOf(type1) >= 0){
          //×と○の場合は△扱い
          if (damageObj["△"]) {
            damageObj["△"].push(type1);
          } else {
            damageObj["△"] = [type1];
          }
          continue;
        }
        if (defenseObj2["△"].indexOf(type1) >= 0){
          //△と×の場合は×扱いとする（三重耐性だが
          if (damageObj["×"]) {
            damageObj["×"].push(type1);
          } else {
            damageObj["×"] = [type1];
          }
          continue;
        }
        //1にしかない場合
        if (damageObj["×"]) {
          damageObj["×"].push(type1);
        } else {
          damageObj["×"] = [type1];
        }
      }
    }
    
    //1つ目のタイプの弱点タイプ
    var defenseObj1TypeArr = [];
    for(var damage in defenseObj1) {
      defenseObj1TypeArr = defenseObj1TypeArr.concat(defenseObj1[damage]);
    }
    Logger.log(defenseObj1TypeArr);
    
    //2にしかないタイプを damageObj にマージ
    for(var damage in defenseObj2) {
      for(var i = 0; i < defenseObj2[damage].length; i++) {    
        var type = defenseObj2[damage][i];
        //1にないタイプの場合追加
        if (defenseObj1TypeArr.indexOf(type) == -1){
          if (damageObj[damage]) {
            damageObj[damage].push(type);
          } else {
            damageObj[damage] = [type];
          }
        }
      }
    } 
    return damageObj;
  } catch(e){
    Logger.log('Error!! getDamage()');
  }
}

// log output
// ログ出力するGoogleドキュメントのIDをセット
var LOG_FILE_ID = 'YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYY';
function getLogFile() {
  if (this.logFile === undefined) {
    this.logFile = DocumentApp.openById(this.LOG_FILE_ID);
  }
  return this.logFile;
}

```
</details>

## LINE Messaging APIでやること
1. LINE Messaging APIでチャンネルを作成します
2. チャンネルトークンを発行します。(GASのスクリプトで利用します)
3. WebhookにGASでデプロイしたときに発行されたURLを指定します

## 最後に
LINE Developersに作成したチャネルにあるQRをLINEアプリで読み込み友達登録、
トーク画面でポケモンをメッセージ送信します。