Todoistの完了タスクをAPIとGoogle Apps Scriptを経由してSpreadsheetに記録する

## 目的

Todoistの完了タスクをログに残したい。有料プランに切り替えれば済む話なのかもしれないが、それだけのために課金するのもどうか。
ということで他の方法で記録していたのですが、今まではIFTTTで簡単に実現していました。IFTTTの有料化に伴い、別の処理に置き換えます。

## 概要

TodoistのWebhooksにより、指定したURLに完了タスクデータをPOSTします。
あらかじめGoogle Apps ScriptのPOST処理を作成し、このURLを使用します。
処理の中でSpreadsheetに記録します。このときヘッダ行を見て記録する列を認識します。

処理の流れとは逆の順に準備を進めます。（多少行って戻ってしますが）

1. 記録先のSpreadsheetを作成する
1. 記録処理のGoogle Apps Scriptを用意する
1. WebhookのURLを設定する

## 手順

### 記録先のSpreadsheetを作成する

* 任意の場所、名前でSpreadsheetを作成する
* このとき1行目を下の画像のように設定する（スクリプトの処理を理解した上で変更も可能）
* URLを後で使う

![screenshot1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/131050/68150894-31c3-8a88-4931-35195c7afb36.png)

### 記録処理のGoogle Apps Scriptを用意する

* 任意の場所、名前でGoogle Apps Scriptを作成する  
作成する方法、準備については他の記事を参照  
https://qiita.com/tags/googleappsscript
* スクリプトを下の内容に書き換え

```js
var SPREADSHEET_ID = "********"; // ここにSpreadsheetのURLを
var TODOIST_API_TOKEN = "********"; // 後述

// Compiled using ts2gas 3.6.3 (TypeScript 3.9.7)
var Todoist = /** @class */ (function () {
    function Todoist() {
    }
    Todoist.prototype.read = function () {
        var _this = this;
        if (typeof this.labels !== "undefined" || typeof this.projects !== "undefined") {
            return;
        }
        var url = "https://api.todoist.com/sync/v8/sync"
            + ("?token=" + TODOIST_API_TOKEN)
            + "&sync_token=*"
            + '&resource_types=[%22labels%22,%22projects%22]';
        var response = UrlFetchApp.fetch(url);
        var data = JSON.parse(response.getContentText("UTF-8"));
        this.labels = {};
        if (Array.isArray(data.labels)) {
            data.labels.forEach(function (label) { return _this.labels[label.id] = label.name; });
        }
        this.projects = {};
        if (Array.isArray(data.projects)) {
            data.projects.forEach(function (project) { return _this.projects[project.id] = project.name; });
        }
    };
    Todoist.prototype.labelName = function (id) {
        this.read();
        return this.labels[id];
    };
    Todoist.prototype.labelNames = function (ids) {
        var _this = this;
        var names = [];
        ids.forEach(function (id) { return names.push(_this.labelName(id)); });
        return names.join(",");
    };
    Todoist.prototype.projectName = function (id) {
        this.read();
        return this.projects[id];
    };
    return Todoist;
}());
var MySpreadsheet = /** @class */ (function () {
    function MySpreadsheet(id, sheetId) {
        if (sheetId === void 0) { sheetId = 0; }
        if (id.match(/^https:\/\//)) {
            this.ss = SpreadsheetApp.openByUrl(id);
        }
        else {
            this.ss = SpreadsheetApp.openById(id);
        }
        this.sheet = this.ss.getSheets()[sheetId];
    }
    MySpreadsheet.prototype.headers = function () {
        var range = this.sheet.getRange(1, 1, 1, this.sheet.getMaxColumns());
        return range.getValues()[0];
    };
    MySpreadsheet.prototype.appendRow = function (row) {
        var insertRow = [];
        this.headers().forEach(function (name, index) {
            insertRow.push((name && name in row) ? row[name] : "");
        });
        this.ss.appendRow(insertRow);
    };
    MySpreadsheet.prototype.unshiftRow = function (row) {
        this.sheet.insertRowAfter(1);
        var range = this.sheet.getRange(2, 1, 1, this.sheet.getMaxColumns());
        this.headers().forEach(function (name, index) {
            if (name) {
                range.getCell(1, index + 1).setValue(row[name]);
            }
        });
    };
    return MySpreadsheet;
}());
var RESPONSE_STATUS;
(function (RESPONSE_STATUS) {
    RESPONSE_STATUS["SUCCESS"] = "success";
    RESPONSE_STATUS["ERROR"] = "error";
})(RESPONSE_STATUS || (RESPONSE_STATUS = {}));
var MyResponse = /** @class */ (function () {
    function MyResponse(status, message) {
        this.status = status;
        this.message = message;
    }
    MyResponse.prototype.toJSON = function () {
        return {
            status: this.status,
            message: this.message
        };
    };
    MyResponse.prototype.create = function () {
        var output = ContentService.createTextOutput();
        output.setMimeType(ContentService.MimeType.JSON);
        output.setContent(JSON.stringify(this));
        return output;
    };
    return MyResponse;
}());
function doPost(e) {
    var data = JSON.parse(e.postData.contents);
    var response = new MyResponse(RESPONSE_STATUS.SUCCESS, "ok");
    try {
        var todoist = new Todoist();
        var row = {
            content: data.event_data.content,
            project: todoist.projectName(data.event_data.project_id),
            due: "",
            completed_at: Utilities.formatDate(new Date(data.event_data.date_completed), Session.getScriptTimeZone(), "yyyy-MM-dd HH:mm:ss"),
            labels: "",
            priority: data.event_data.priority,
            url: data.event_data.url
        };
        if (data.event_data.due !== null) {
            row.due = data.event_data.due.date;
        }
        if (data.event_data.labels.length > 0) {
            row.labels = todoist.labelNames(data.event_data.labels);
        }
        var ss = new MySpreadsheet(SPREADSHEET_ID);
        ss.appendRow(row);
    }
    catch (err) {
        console.error(err);
        console.log(data);
        response.status = RESPONSE_STATUS.ERROR;
        response.message = err.message;
    }
    return response.create();
}
```

コンパイル前のコードはこちら。修正する場合は参考にしてください。
https://github.com/kunikada/todoist-logs-completed

### WebhookのURLを設定する準備

* Todoistの管理コンソールを開く  
App Management Console  
https://developer.todoist.com/appconsole.html
* `Create a new app`  
`App name`は適当に
* `Test token`を後で使う

### Google Apps Scriptを公開する

* さきほどのスクリプトを開く
* `Test token`をコード中の`TODOIST_API_TOKEN`に設定
* コードを保存する
* メニュー > 公開 > ウェブアプリケーションとして導入...
* URLを後で使う

### WebhookのURLを設定する

* Todoistの管理コンソールを開く
* 作成したAppを開く
* `Webhooks callback URL`にさきほどのURLを設定
* `Watched Events`の`item:completed`にチェック
* `Save webhooks configuration`

### おわり

お疲れさまでした。

## 参考

Todoist API 公式ドキュメント
https://developer.todoist.com/

Google Apps Script リファレンス
https://developers.google.com/apps-script/reference