LINE botでちょっとしたタスクをメモする

# 解決すること
普段のちょっとした買い物メモやtodoを妻とのトーク（LINE）にメモることが多いが、優先度やらなにやらに関わらず書き込む為、その後の会話で上の方へ流れていってしまうことが多い。
これを簡単に分類した状態で記録、参照、管理できるようにしたい。

# 解決方法
LINE bot + GASを使い、特定コマンドとメモ（タスク）をLINE上からファイル単位で管理できるようにする。

# 成果物
トーク上から各コマンドとファイル名、メモしたいタスクを入力することで管理できます。


<blockquote class="twitter-tweet" data-lang="ja"><p lang="ja" dir="ltr">Qiita用テスト <a href="https://t.co/jGZm2WTqHr">pic.twitter.com/jGZm2WTqHr</a></p>&mdash; koob (@koob12340160) <a href="https://twitter.com/koob12340160/status/1150751362168213505?ref_src=twsrc%5Etfw">2019年7月15日</a></blockquote>
<script async src="https://platform.twitter.com/widgets.js" charset="utf-8"></script>

### 仕組み
①トークから特定ワードによる各コマンドを入力
②GAS上で入力内容を解析
③特定のコマンドを検出できたら、対象のGoogle Documentに対して[新規作成]、[読み取り]、[書き込み]、[削除]、を行う
④処理内容をリプライする

ユーザーが入力した各タスクは、入力時にファイル名を指定されたGoogle Document上で管理される為、Google Documentのファイル名によってタスクが分類されることとなります。
ユーザーは後述する各コマンドを用いて、Google Documentを操作し、タスクを管理できます。
（例えばファイル名「ワイン」を指定後、タスクとして「赤ワイン」「白ワイン」「ロゼワイン」などと入力すれば、「ワイン」というファイル名のGoogle Documentに、「赤ワイン」「白ワイン」「ロゼワイン」がテキストとして記録される）

### 各コマンド

#### 新規タスクを登録する場合（コマンド：「メモ」）
入力例）
「メモ　[ファイル名]
　タスク１
　タスク２
　タスク３...」
⇒[ファイル名]に指定されたタイトルのGoogle Documentが作成され、そこに2行目以降のタスクが書き込まれる。すでに存在するファイル名の場合、今回入力のタスクが追記される。

#### 記録したタスクを参照する場合（コマンド：「タスク」）
入力例１）
「タスク」
⇒メモを記録したGoogle Documentファイルの一覧がリプライされる。

入力例２）
「タスク　[ファイル名]」
⇒[ファイル名]に指定されたGoogle Documentが参照され、そのテキスト部分がリプライされる。

#### タスクが完了した場合（コマンド：「完了」）
入力例）
「完了　[ファイル名]
　タスク１
　タスク２
　タスク３...」
⇒[ファイル名]に指定されたGoogle Documentを参照し、テキストに今回入力のタスクが存在すれば削除する。

#### Google Documentファイルを削除する場合（コマンド：「ファイル削除」）
入力例）
「ファイル削除　[ファイル名]」
⇒[ファイル名]に指定されたGoogle Documentファイルを削除する。

# 実装したコード（GAS）
コードやLINE Messaging APIの使用方法等に関する具体的な説明はここではしません。
基本的な部分であれば、以下の記事やその参考リンクが参考になるかと思います。
・[Google Apps ScriptでLINE BOTつくったら30分で動かせた件](https://qiita.com/hakshu/items/55c2584cf82718f47464)
・[GASで気圧の変化をお知らせしてくれるLINEbotをつくる](https://qiita.com/koob/items/79c6d6fdf1f7cb1a5b18)

※以下コードはメソッドごとに分けて記述。
※「{YOUR_ACCESS_TOKEN}」にはLINE botに割り当てられたアクセストークンを、「{YOUR_FOLDER_ID}」にはGoogle Drive上でメモ用のGoogle Documentファイルを保存したいフォルダのIDを記述してください。
※一部初期設定では使用できない関数を使用しています。GASのメニュー「リリース」より、「Googleの拡張サービス」を選択し、「Drive API」を「ON」に設定してください。

```javascript:コード.gas
// メッセージ区分
var _typeNew = "1"; // 新規
var _typeRefTasks = "2"; // 照会（タスク一覧）
var _typeRefFiles = "3"; // 照会（ファイル一覧）
var _typeDelTasks = "4"; // 削除（タスク）
var _typeDelFiles = "5"; // 削除（ファイル）
var _typeUnknown = "9"; // 不明

function doPost(e) {
  // LINE developersのメッセージ送受信設定に記載のアクセストークン
  const LINE_ACCESS_TOKEN = {YOUR_ACCESS_TOKEN};
  // 応答メッセージ用のAPI URL
  const LINE_REPLY_URL = "https://api.line.me/v2/bot/message/reply";
  // LINEMessageAPIの返り値
  const LINE_REPLY_TOKEN = JSON.parse(e.postData.contents).events[0].replyToken;

  var userMessage = JSON.parse(e.postData.contents).events[0].message.text;
  var postText = main(userMessage);
  
  UrlFetchApp.fetch(LINE_REPLY_URL, {
    "headers": {
      "Content-Type": "application/json; charset=UTF-8",
      "Authorization": "Bearer " + LINE_ACCESS_TOKEN,
    },
    "method": "post",
    "payload": JSON.stringify({
      "replyToken": LINE_REPLY_TOKEN,
      "messages": [{
        "type": "text",
        "text": postText,
      }],
    }),
  });
  
  return ContentService.createTextOutput(JSON.stringify({"content": "post ok"})).setMimeType(ContentService.MimeType.JSON);
}
```

```javascript:コード.gas
function main(userMessage){
  // Google Documentを配置するフォルダの指定
  const FOLDER_ID = {YOUR_FOLDER_ID};
  var folder = DriveApp.getFolderById(FOLDER_ID);

  // ユーザーの入力値に不要な改行や空白が入っていた場合、除去する
  var arrTmp = userMessage.split(/\n/);
  var arrMessage = arrTmp.filter(function( value ) {
    var strTmp = value.trim();
    return value !== "";
  });

  // 入力コマンドと指定ファイル名だけの配列を作成
  var arrMessageTop = arrMessage[0].split(/\s/);
  // メッセージ区分の判定
  var messageType = judgMessageType(arrMessageTop);
  var postText = "";
  var fileTytle = "";
  var fileId = "";
  
  // メッセージ区分に応じてファイル操作や、リプライメッセージの作成を行う
  if (messageType == _typeNew) {
    // 区分：新規
    if (!arrMessageTop[1]){
      // タイトルの指定がない場合はシステム日付をセットする
      arrMessageTop[1] = Utilities.formatDate(new Date(), "Asia/Tokyo", "yyyyMMdd");
    }
    fileTytle = arrMessageTop[1];
    fileId = getFile(folder, messageType, fileTytle);
    
    if (fileId){
      // Google Document ファイルが取得できた場合、指定ファイルに新規メモを追記
      var writeText = writeFileText(fileId, arrMessage, messageType);
      postText = "ファイル名『" + fileTytle + "』にメモしました。\n\n[現在のタスク一覧]" + writeText;
    }
    else{
      // ファイルIDが取得できない場合はエラー
      postText = "エラーが発生しました。";
    }
  }
  else if (messageType == _typeRefTasks){
    // 区分：照会（タスク一覧）
    fileTytle = arrMessageTop[1];
    fileId = getFile(folder, messageType, fileTytle);
    
    if (fileId){
      // 指定ファイルのテキストを取得
      var fileText = DocumentApp.openById(fileId).getBody().getText();
      postText = "[ファイル名『" + fileTytle + "』のタスク一覧]\n\n" + fileText;
    }
    else{
      // ファイルIDが取得できない場合は指定フォルダからファイル一覧を取得する
      postText = "指定のファイル名が存在しません。\n";
      postText = RefFiles(folder, postText);
    }
  }
  else if (messageType == _typeRefFiles){
    // 区分：照会（ファイル一覧）
    postText = RefFiles(folder, postText);
  }
  else if (messageType == _typeDelTasks){
    // 区分：削除（タスク）
    // Google Documentを開き、テキストを取得
    if (arrMessageTop[1]){
      // ファイル名が指定されている場合
      fileTytle = arrMessageTop[1];
      fileId = getFile(folder, messageType, fileTytle);
      
      if (fileId){
        // ファイルIDが取得できた場合
        // 指定ファイルのテキストを取得して、配列化する
        var fileText = DocumentApp.openById(fileId).getBody().getText();
        var arrFileText = fileText.split(/\n/);
        var removeText = "";

        // 入力タスクと指定ファイルのタスク一覧とが一致するかをチェック
        arrMessage.forEach(function(value, index){
          if (index > 0){
            // 指定ファイル内に入力タスクが存在した場合、配列から削除する
            var matchIndex = arrFileText.indexOf(value);
            if (matchIndex != -1){
              arrFileText.splice(matchIndex, 1);
              removeText = removeText + value + "\n";
            }
          }
        });
        
        // 残ったタスク一覧を指定ファイルに書き込む
        var writeText = writeFileText(fileId, arrFileText, messageType);
        postText = "ファイル名『" + fileTytle + "』から、タスクを削除しました。\n\n[完了タスク]\n" + removeText + "\n[現在のタスク一覧]" + writeText;
      }
      else{
        // ファイルIDが取得できない場合は指定フォルダからファイル一覧を取得する
        postText = "指定のファイル名は存在しません。\n";
        postText = RefFiles(folder, postText);
      }
    }
    else{
      // ファイル名の指定がない場合はエラー
      postText = "「完了タスク」コマンド実行時には、タスクをメモしているファイル名を指定してください。\n";
      postText = RefFiles(folder, postText);
    }
  }
  else if (messageType == _typeDelFiles){
    // 区分：削除（ファイル）
    // 指定ファイルを取得
    fileTytle = arrMessageTop[1];
    fileId = getFile(folder, messageType, fileTytle);

    if (fileId){
      // 指定ファイルが存在すれば、削除する
      folder.removeFile(folder.getFilesByName(fileTytle).next());
      postText = "ファイル名『" + fileTytle + "』を削除しました。\n\n";
    }
    else{
      postText = "指定のファイルは存在しません。\n\n";
    }
    
    postText = RefFiles(folder, postText);
  }
  else{
    // 区分：不明
    postText = "コマンドが不明です。";
  }
  
  // doPost()へメッセージを返す
  return postText;
}
```


```javascript:コード.gas
// メッセージ区分を判定
function judgMessageType(arrMessageTop){
  // 区分値検出用の文字列
  const TYPE_NEW = "メモ";
  const TYPE_REF = "タスク";
  const TYPE_DEL_TASK = "完了";
  const TYPE_DEL_FILE = "ファイル削除";
  var messageType = _typeUnknown;

  if (arrMessageTop[0] == TYPE_NEW){
    // 区分：新規
    messageType = _typeNew;
  }
  else if (arrMessageTop[0] == TYPE_REF){
    if (arrMessageTop[1]){
      // ファイル名が指定されている場合
      // 区分：照会（タスク一覧）
      messageType = _typeRefTasks;
    }
    else {
      // 区分：照会（ファイル一覧）
      messageType = _typeRefFiles;
    }
  }
  else if (arrMessageTop[0] == TYPE_DEL_TASK){
    // 区分：削除（タスク）
    messageType = _typeDelTasks;
  }
  else if (arrMessageTop[0] == TYPE_DEL_FILE){
    // 区分：削除（ファイル）
    messageType = _typeDelFiles;
  }
  
  return messageType;
}
```

```javascript:コード.gas
// 対象のGoogle Documentを取得
function getFile(folder, messageType, fileTytle) {
  var file= folder.getFilesByName(fileTytle);
  var retId = "";
  
  if(file.hasNext()){
    retId = file.next().getId();
  }
  else{
    if (messageType == _typeNew){
      // 指定フォルダ配下に同じタイトルのGoogle Documentがなければ作成する
      var file = Drive.Files.insert({
        "title":    fileTytle,
        "mimeType": "application/vnd.google-apps.document",
        "parents":  [{"id": folder.getId()}]
      });

      retId = file.id;
    }
  }
  return retId;
}
```

```javascript:コード.gas
// Google Documentのテキストに新規メモを追記
function writeFileText(fileId, arrMessage, messageType){
  // Google Documentを開き、テキストを取得
  var file = DocumentApp.openById(fileId);
  var fileText = file.getBody().getText();
  
  arrMessage.forEach(function(value, index){
    if (messageType == _typeDelTasks && index == 0){
      // 区分：削除（タスク）の時はテキストを初期化する
      fileText = "";
    }
    if (index > 0){
      fileText = fileText + "\n" + value;
    }
  });
  
  // テキストを書き込む
  file.getBody().setText(fileText);

  return fileText;
}
```

```javascript:コード.gas
// ファイル一覧の照会
function RefFiles(folder, postText){
    // 区分：照会（ファイル一覧）
    // 格納ファイルの一覧を取得
    var files= folder.getFiles();
    var fileNames = "";

    while(files.hasNext()) {
      // ファイル一覧を文字列に変換
      var file = files.next();
      fileNames = fileNames + "\n" + file.getName();
    }
    
    postText = postText + "現在のファイル一覧は以下です。特定ファイルのタスク内容を確認する場合は「タスク　[任意のファイル名]」と入力してください。\n\n"
    
    if (fileNames == ""){
      postText = postText + "※メモファイルが1件もありません。「メモ　[任意のファイル名]」コマンドで新規作成してください。";
    }
    else{
      postText = postText + fileNames;
    }
  
  return postText;
}
```

###終わりに
今回のbot作成はGASによるGoogle Drive上のファイル操作のいい勉強になりました。
とはいえ、（私や妻が使う分にはいいとしても）コマンドを覚える必要があるなど、まだまだ改善点はあります。
使い勝手についてはLINE Developersの各機能を使えば改善可能なはずなので、今後はそちらも取り入れてみたいですね。