LINE BOTからTodoistのタスク作成

妻からの買い物リストの管理をTodoistでやっています。
LINEのMessaging APIを使ってみたかったのと、もっと簡単にタスク作成をしたかったので、Todoistのタスク作成をするLINE BOTを作ってみました。

## LINE BOTの準備

準備するもの
* LINE Businessアカウント
* HTTPSに対応したサーバ

## LINE Businessアカウント

　* 以下から利用登録
https://business.line.me/ja/services/bot

<img src="https://qiita-image-store.s3.amazonaws.com/0/154533/c9bd2753-7081-125f-a841-9e66dc3eb56f.png" width="400px">

Messaging APIかDeveloper Trialかお好きな方を。

#### Messaging APIとDeveloper Trialの違い

||Developer Trial|Messaging API(Free Plan)|
|:--|:--|:--|
|Push API(※1)|利用できる|利用できない|
|追加可能友だち数|50人まで|制限なし|
|プレミアムID|利用できない|利用可能|
|API Rate limits|1,000/分|10,000/分|

* (※1)Push API
Push APIはキャンペーン通知などBOTから任意のイベントでメッセージを送る

### LINE BOTの設定

LINE@ MANAGERから設定します。

* Webhook送信: 利用する
利用しないとサーバにリメッセージが送られない

* Botのグループトーク参加: どちらでも好きな方を
* 自動応答メッセージ: 利用しない
利用するにすると固定メッセージしか返さないので注意

<img src="https://qiita-image-store.s3.amazonaws.com/0/154533/f8d9cc11-ddcf-2e87-6ada-d2dc7c748e45.png" width="400px">

## HTTPSに対応したサーバ

HerokuやAWSをつかった記事がよくありますけど、今回はGoogle Apps Script(GAS)で構築します。

## GASの実装

* GASの作成  
その他から表示されなかったら「アプリを追加」から作成  
<img width="400" alt="スクリーンショット 2017-01-29 21.56.51.png" src="https://qiita-image-store.s3.amazonaws.com/0/154533/31d6a353-31b0-0f5e-d423-9b3ab91f24ae.png">


### LINEからのリクエストを受け取る

まずはオウム返しするサンプルを作成します

Messaging APIはPOSTでリクエストしてくるのでdoPost関数を実装します

```js:コード.gs
var CHANNEL_ACCESS_TOKEN = 'LINE Developersから取得'

function doPost(e) {
  // 返事用のトークン。一度のみ使うことができる
  var replyToken = JSON.parse(e.postData.contents).events[0].replyToken;
  // 投稿したメッセージ
  var userMessage = JSON.parse(e.postData.contents).events[0].message.text;

  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': replyToken,
      'messages': [{
        'type': 'text',
        'text': userMessage, // オウム返しをする
      }],
    }),
  });
  return ContentService.createTextOutput(JSON.stringify({'content': 'post ok'})).setMimeType(ContentService.MimeType.JSON);
}
```

### ウェブアプリケーションとして導入

Messaging APIからGASへのリクエストを行うためにGASを公開してURLをLINE側に登録します。

* GASから導入 > ウェブアプリケーションとして導入で公開
* URLをコピってLINE DevelopersのWebhook URLに設定

　<img width="400" alt="スクリーンショット 2017-01-29 21.56.51.png" src="https://qiita-image-store.s3.amazonaws.com/0/154533/fa53397b-df44-6cbc-a1c3-a8b32cc105f3.png">

あとは作成したアカウントを友達登録してメッセージを送信するとオウム返ししてくれます。

## Todoistのタスク登録

続いて本題のTodoistのタスク登録を実装します。
TodoistのAPI: https://developer.todoist.com/

まずはTodoistのAPIをたたくためのAPIトークンを取得

* Todoistの設定 > アカウント > APIトークン
 　<img width="400" alt="スクリーンショット 2017-01-29 21.56.51.png" src="https://qiita-image-store.s3.amazonaws.com/0/154533/da188d0a-4acf-d733-bbe2-fac15b49a2ee.png">

そして取得したAPIトークンでTodoistのAPIにリクエスト

```js:コード.gs
var CHANNEL_ACCESS_TOKEN = 'LINE Developersから取得'

function doPost(e) {
  // 返事用のトークン。一度のみ使うことができる
  var replyToken = JSON.parse(e.postData.contents).events[0].replyToken;
  // 投稿したメッセージ
  var userMessage = JSON.parse(e.postData.contents).events[0].message.text;

  var status = createTask(userMessage);
    
  if (status === 200) {
    answerText = userMessage + "を作成した！";
  } else {
    answerText = "何か失敗した。。。";
  }

  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': replyToken,
      'messages': [{
        'type': 'text',
        'text': answerText
      }],
    }),
  });
  return ContentService.createTextOutput(JSON.stringify({'content': 'post ok'})).setMimeType(ContentService.MimeType.JSON);
}

// タスク登録
function createTask(userMessage) {
  var url = "https://todoist.com/API/v7/sync",
      uuid = Utilities.getUuid(),
      tempId = Utilities.getUuid(),
      formData, response;
  
  formData = {
    "token": "TODOIST API TOKEN",
    "commands": JSON.stringify([{
      "type": "item_add",
      "temp_id": tempId,
      "uuid": uuid,
      "args": {
        "content": userMessage[0],
        "project_id": 000000 // TodoistのプロジェクトのID
      }
    }])
  };

  response = UrlFetchApp.fetch(url, {
    "method" : "post",
    "payload" : formData
  });
  return response.getResponseCode();
}
```

これで、何か登録したいタスクをメッセージに投稿すると、Todoistに登録される
<img width="200" alt="スクリーンショット 2017-01-29 21.56.51.png" src="https://qiita-image-store.s3.amazonaws.com/0/154533/180bfe43-14ce-c06f-6404-e753e67b8164.jpeg">

## Todoistのタスク一覧取得

次に、登録したタスク一覧をTemplate messageのCarouselを使って返すようにしました

Carouselはこんな感じのやつ
<img width="200" alt="スクリーンショット 2017-01-29 21.56.51.png" src="https://qiita-image-store.s3.amazonaws.com/0/154533/627a0f0c-b7e0-2b9c-1794-be847c3301b1.png">

### コード

注意点として、

* Template messageのCarouselは5件までしか横にスライドできないので、columnの配列の件数を5件にする必要がある
* 5件以上ある場合は複数メッセージ(messages)を返す必要がある

```js:コード.gs
var CHANNEL_ACCESS_TOKEN = 'LINE Developersから取得'

function doPost(e) {
  // 返事用のトークン。一度のみ使うことができる
  var replyToken = JSON.parse(e.postData.contents).events[0].replyToken;
  // 投稿したメッセージ
  var userMessage = JSON.parse(e.postData.contents).events[0].message.text;

  // > listだとタスク登録しない
  if (userMessage.indexOf("> list") === 0) {
      var response = getTasks();
      messages = createCarousel(response.items));
  } else {
    var status = createTask(userMessage);
    
    if (status === 200) {
      answerText = userMessage + "を作成した！";
    } else {
      answerText = "何か失敗した。。。";
    }
    messages = [{
      'type': 'text',
      'text': answerText
    }];
  }

  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': replyToken,
      'messages': messages,
    }),
  });
  return ContentService.createTextOutput(JSON.stringify({'content': 'post ok'})).setMimeType(ContentService.MimeType.JSON);
}

// タスク一覧の取得
function getTasks() {
  var url = "https://todoist.com/API/v7/projects/get_data",
      formData, options, response;
  
  formData = {
    "token": "TODOIST API TOKEN",
    "project_id": "Todoist Project　ID"
  };

  response = UrlFetchApp.fetch(url, {
    "method" : "post",
    "payload" : formData
  });
  return JSON.parse(response.getContentText());
}

// Carousel情報の作成
function createCarousel(items) {
  var i, max,
      columns = [],
      itemsArray = [],
      messages = [];
      
  // Template messageのCarouselは5件までしか横にスライドできないので、複数メッセージを返す必要がある
  for (i = 0; i < items.length; i += 5) {
    itemsArray.push(items.slice(i, i + size));
  }
  // columnsに5件ずつの複数のメッセージを作成
  for (i = 0, max = itemsArray.length; i < max; i++) {
    messages.push( {
      type: "template",
      altText: "一覧",
      template: {
        type: "carousel",
        columns: createColumns(itemsArray[i])
      }
    } );
  }
  
  return messages;
}

function createColumns(items) {
  var i, max,
      columns = [];
      
  for (i = 0, max = items.length; i < max && i < 5; i++) {
    columns.push({
      text: items[i].content,
      actions: [{            // ボタンの設定
        "type": "message",
        "label": "完了",
        "text": "> finish " + items[i].id
      }]
    });
  }
  return columns;
}
```

これで"> list"と投稿すると一覧を返します

<img width="300" alt="スクリーンショット 2017-01-29 21.56.51.png" src="https://qiita-image-store.s3.amazonaws.com/0/154533/e06a49ee-7a4b-0e9a-98a4-e86c4a06d0c9.jpeg">

## リッチメニューの作成

LINEの公式アカウントを見ているとトークを開いた途端に下から出てくるやつです。（ヤマト運輸とか）
\> listと打つのはめんどいのでリッチメニューからタスク一覧をすぐに表示できるようにします。

LINE@ MANAGER > リッチコンテンツ作成 > リッチメニューから作成します。

<img width="400" alt="スクリーンショット 2017-01-29 21.56.51.png" src="https://qiita-image-store.s3.amazonaws.com/0/154533/d4487d02-e728-ee12-b81b-d04eda276ecf.png">

## おまけ

Google Apps Scriptのコード内からセキュアな文字列を使いたいときスクリプトプロパティを使うといい感じです。

<img width="300" alt="スクリーンショット 2017-01-29 21.56.51.png" src="https://qiita-image-store.s3.amazonaws.com/0/154533/ec1fa2fb-125b-9b7f-b1f1-9d0bca086c51.png">

```js:コード.gs
var property = PropertiesService.getScriptProperties().getProperty("CHANNEL_ACCESS_TOKEN");
```