freeeに関係ない話題ですが、Google Apps Script(以下GAS)でできる効率化の例として番外編としてfreee API以外のGASでの活用に関しても記事にしています。
今回は、「Gmailでスターを付けたメールからTodoistのタスクを作成し、メールの受信をSlackに通知する」に挑戦したいと思います。
前回の記事では、OAuthライブラリを使って、Todoist APIの認証を通しました。
Todoistのプロジェクトを取得する
Todoistにはプロジェクトというタスクをまとめるフォルダのようなものがあります。
今回は、指定したあるプロジェクト内にGmailのスター付きメールから新たにタスクを作成したいのですが、タスク作成時にプロジェクトを指定するためには、まずプロジェクトのIDを取得する必要があります。
ということで、いつものようにprojectsのエンドポイントの確認からはじめます。
公式ドキュメントによると以下がprojectsを取得するためにリクエストを送信するエンドポイントです。
https://api.todoist.com/rest/v1/projects
サンプルリクエストのcurlコマンドを確認して、これをGASに置き換えます。
function logAllProjects() {
const accessToken = getTodoistAccessToken_(); // アクセストークンを取得
const url = 'https://api.todoist.com/rest/v1/projects';
const params = {
headers: { 'Authorization': `Bearer ${accessToken}` }
};
const response = UrlFetchApp.fetch(url, params);
const json = response.getContentText();
const aryProject = JSON.parse(json);
console.log(aryProject.length);
console.log(aryProject);
}
リクエストヘッダに必要なアクセストークンは、前回の記事で作成したgetTodoistAccessToken_()関数を使って取得しています。
実行してみると…
無事レスポンスが得られました。
プロジェクト名からプロジェクトIDを取得する関数
この関数を少しアレンジしてプロジェクト名からプロジェクトIDを取得する関数を作成してみます。
function logProjectIdByName() {
const name = 'Inbox';
const id = getProjectIdByName_(name);
console.log(id);
}
function getProjectIdByName_(name) {
const accessToken = getTodoistAccessToken_();
const url = 'https://api.todoist.com/rest/v1/projects';
const params = {
headers: { 'Authorization': `Bearer ${accessToken}` }
};
const response = UrlFetchApp.fetch(url, params);
const json = response.getContentText();
const aryProject = JSON.parse(json);
const project = aryProject.find(obj => obj.name === name);
return project.id;
}
同一名称のプロジェクトは存在しないという前提であれば、Arrayオブジェクトに対して使用できるfind() メソッド を使用すると便利です。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/Array/find
find()
メソッドは、提供されたテスト関数を満たす配列内の最初の要素を返します。 テスト関数を満たす値がない場合は、undefined
を返します。
getProjectIdByName_(name)関数は他の関数からも呼び出せるようにしているので、ログ出力用のデモ関数のlogProjectIdByName()関数を実行してみます。
プロジェクトIDだけなので、あっさりとしたログですが、無事成功しました。
Todoistのタスクをテスト作成する
続いてTodoistのタスクを作成します。これもまず、公式ドキュメントを確認してエンドポイントとリクエストボディの形式を確認します。
すべてのリクエストと同様に、Authorizationヘッダを提供します。また、Content-Type: application/jsonヘッダと、POSTリクエストであるためオプションで X-Request-Id を提供します。
https://developer.todoist.com/rest/v1/#adding-a-new-project
サンプルリクエストのcurlコマンドは以下の通りです。
これをいつものようにConvert curl commands to codeのサイトでJavaScriptに変換すると以下のようになります。
fetch('https://api.todoist.com/rest/v1/tasks', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Request-Id': '$(uuidgen)',
'Authorization': 'Bearer 0123456789abcdef0123456789'
},
// body: '{"content": "Buy Milk", "project_id": 2203306141}',
body: JSON.stringify({
'content': 'Buy Milk',
'project_id': 2203306141
})
});
ここで登場する
'X-Request-Id': '$(uuidgen)',
が何をするためのものなのか、ちょっとよくわからないので、公式ドキュメントのOverviewを確認したところ
POSTリクエストは、一意の文字列を含む追加のX-Request-Id HTTPヘッダーを提供し、修正が一度だけ適用されることを保証することができます。以前に処理されたリクエストと同じ ID を持つリクエストは破棄されます。
これは必須ではありませんが、リクエストの再試行ロジックを実装する際に 有用です。このヘッダー値は36バイトを越えてはいけません。シェルコードの例では、uuidgen を使って生成する予定です。
https://developer.todoist.com/rest/v1/?shell#overview
必須項目ではないらしいので、ひとまず除外します。
ということで、JavaScriptをGASに読み替えたスクリプトは以下のようになります。
function() {
const accessToken = getTodoistAccessToken_(); // アクセストークンを取得
const url = 'https://api.todoist.com/rest/v1/tasks';
const payload = {
content: 'Buy Milk',
project_id: getProjectIdByName_('Inbox') // インボックスにタスクを作成
};
const params = {
contentType: 'application/json',
headers: { Authorization: `Bearer ${accessToken}` },
method: 'post',
payload: JSON.stringify(payload), // payloadをJSON文字列化しないとエラーになる
muteHttpExceptions: true
};
const response = UrlFetchApp.fetch(url, params);
const json = response.getContentText();
const obj = JSON.parse(json);
console.log(obj);
}
project_id: getProjectIdByName_('Inbox')
インボックスにタスクを作成するためにプロジェクトIDが必要ですので、先程作成したgetProjectIdByName_()関数を利用しています。
payload: JSON.stringify(payload),
そしてもう1つ。POSTリクエストの時のここがポイントです。payloadの値をオブジェクトのままリクエスト送信するとエラーになるため、JSON.stringify() メソッドでJSON文字列化します。
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
JSON.stringify()
メソッドは、ある JavaScript のオブジェクトや値を JSON 文字列に変換します。置き換え関数を指定して値を置き換えたり、置き換え配列を指定して指定されたプロパティのみを含むようにしたりすることもできます。
実行すると無事タスクがインボックスに作成されました。
作成したいタスクのpayloadを設定する
先程のテストタスクを作成する関数をアレンジして、payloadを引数に渡してタスクを作成する関数を作成します。
function createTodoistTask_(payload) {
const accessToken = getTodoistAccessToken_(); // アクセストークンを取得
const url = 'https://api.todoist.com/rest/v1/tasks';
const params = {
contentType: 'application/json',
headers: { Authorization: `Bearer ${accessToken}` },
method: 'post',
payload: JSON.stringify(payload), // payloadをJSON文字列化しないとエラーになる
muteHttpExceptions: true
};
const response = UrlFetchApp.fetch(url, params);
const json = response.getContentText();
const obj = JSON.parse(json);
console.log(obj);
}
連載第1回目の記事で取得したスター付きのメールの
- 件名
- 本文
- 送信者
- 受信日時
を利用してタスクを作成すると想定して進めます。
POSTリクエストのボディに指定できるパラメータは以下の通りです。
- content:タスク名
- description:説明
- project_id:プロジェクトID
- section_id:セクションID
- parent_id:親タスクID
- parent:親タスク(非推奨)
- order:同一親タスク内での順序
- label_ids:配列で指定、ラベルID
- priority:優先度
- due_string:自然言語で指定される期限日時(例: “next Monday”, “Tomorrow”)
- due_date:YYYY-MM-DDフォーマットで指定される、ユーザーのタイムゾーンに基づく期限日時
- due_datetime:RFC3339形式(タイムゾーン:UTC)によって指定される期限日時
- due_lang:due_string が英語で書かれていない場合の言語を指定する 2文字のコード
- assignee:責任者ユーザーID(設定されておりかつ共有タスクの場合のみ)
今回は
- content:メール送信者|件名
- description:メール本文
- project_id:インボックスのID
- due_datetime:メール受信日時の翌日AM10:00
を指定します。
スター付きメールを取得する関数に各メールからタスクを作成するスクリプトを追加しました。
function newStarredMails2Task() {
// Gmailの検索演算子:1日以内に受信したスター付きのメール
const query = 'is:starred newer_than:1d ';
// 検索演算子の条件に該当するメールが含まれるスレッドのすべてのメールを取得
const aryAllMessages = new GmailSearch(query).allMessages;
// 取得すべきメッセージがなければ処理を中止
if (aryAllMessages.length === 0) { return };
// isStarred()メソッドでスター付きのメールのみに絞り込み
const aryStarredMessages = aryAllMessages.filter(message => message.isStarred());
// プロパティストアからメール絞り込みの起算日時を取得
const dateLastMail = new Date(PropertiesService.getScriptProperties().getProperty('LAST_DATE_STAR'));
// 起算日時以降のメールのみに絞り込み
const aryNewMessage = aryStarredMessages.filter(message => message.getDate() > dateLastMail);
// 取得すべきメッセージがなければ処理を中止
if (aryNewMessage.length === 0) { return };
// GmailMessageクラスのメソッドを利用してメールから必要な情報を取得してTodoistに通知
aryNewMessage.forEach(messageNew => {
const subject = messageNew.getSubject();
const plainBody = messageNew.getPlainBody();
const sender = messageNew.getFrom();
const dateMail = messageNew.getDate();
const tomorrowAM1000 = new Date(dateMail.getFullYear(), dateMail.getMonth(), dateMail.getDate() + 1, 10, 0, 0); // 翌日のAM10時のDateオブジェクト
const rfc3339TomorrowAM1000 = Utilities.formatDate(tomorrowAM1000, 'UTC', "yyyy-MM-dd'T'HH:mm:ssXXX"); // RFC3339形式(タイムゾーン:UTC)の文字列に変換
const payload = {
content: sender + '|' + subject,
description: plainBody,
project_id: getProjectIdByName_('Inbox'),
due_datetime: rfc3339TomorrowAM1000
};
createTodoistTask_(payload);
Utilities.sleep(300); // APIへの負荷軽減のためスリープ処理を追加
});
// 起算日時以降のメールを日付で降順に並び替え
aryNewMessage.sort((a, b) => b.getDate() - a.getDate());
// 最新のメールから受信日時を取得してプロパティストアに次回の起算日時として保存
const messageLast = aryNewMessage[0];
const dateLast = messageLast.getDate();
PropertiesService.getScriptProperties().setProperty('LAST_DATE_STAR', dateLast);
}
まずGmailMessageオブジェクトの各メソッドを利用して、必要な情報を取得して定数に代入します。
const subject = messageNew.getSubject(); // 件名
const plainBody = messageNew.getPlainBody(); // メール本文
const sender = messageNew.getFrom(); // 受信日時
ここが今回のハイライトです。タスクの期限を時間まで指定しようとするとdue_datetimeプロパティを指定しないといけません。due_datetimeプロパティの値は、RFC3339形式(タイムゾーン:UTC)の文字列と公式ドキュメントで指定されています。
RFC3339形式とは
2021-10-07T14:42:41Z
のようなフォーマットで表現される日時です。このフォーマットへの変換は、Utilities.formatDate(date, timeZone, format)メソッドを用いて実行します。この時、ドキュメントの指定にあるようにタイムゾーンがUTCとなるように引数を設定しましょう。
そして、第3引数に以下のような文字列を指定することでRFC3339形式の日時の文字列が取得できます。
- “yyyy-MM-dd’T’HH:mm:ssXXX”
- “yyyy-MM-dd’T’HH:mm:ss’Z'”
フォーマットはどちらかを指定します。語尾がXXXのものは、タイムゾーンの指定に応じてフォーマットが変化します。一方で語尾が’Z’のものは、タイムゾーン指定にかかわらずUTCに変換されます。
function logRFC3339DateString() {
const now = new Date();
const rfc3339JSTXXX = Utilities.formatDate(now, 'JST', "yyyy-MM-dd'T'HH:mm:ssXXX");
const rfc3339JSTZ = Utilities.formatDate(now, 'JST', "yyyy-MM-dd'T'HH:mm:ss'Z'");
const rfc3339UTCXXX = Utilities.formatDate(now, 'UST', "yyyy-MM-dd'T'HH:mm:ssXXX");
const rfc3339UTCZ = Utilities.formatDate(now, 'UST', "yyyy-MM-dd'T'HH:mm:ss'Z'");
console.log(rfc3339JSTXXX); // 2022-09-04T01:45:42+09:00
console.log(rfc3339JSTZ); // 2022-09-04T01:45:42Z
console.log(rfc3339UTCXXX); // 2022-09-04T01:45:42Z
console.log(rfc3339UTCZ); // 2022-09-04T01:45:42Z
}
ということで、期限日時をメールの受信日時の翌日AM10:00となるように設定します。
const dateMail = messageNew.getDate();
const tomorrowAM1000 = new Date(dateMail.getFullYear(), dateMail.getMonth(), dateMail.getDate() + 1, 10, 0, 0);
const rfc3339TomorrowAM1000 = Utilities.formatDate(tomorrowAM1000, 'UTC', "yyyy-MM-dd'T'HH:mm:ssXXX");
- メールの受信日時を取得
- 受信日時から翌日AM10:00のDateオブジェクトを生成
- DateオブジェクトからRFC3339形式の文字列に変換
トリガー実行
最後にnewStarredMails2Task()関数をトリガーで定期実行することで、受信したスター付きメールTodoistのタスクが自動で作成されるようになります。
15分おきや1時間おきなど、業務の都合に合わせて設定しましょう。
おわりに
今回は、「Todoist APIでタスクを新たに作成する」を紹介しました。
特にタスクの期限日時を設定するdue_datetimeプロパティがRFC3339形式と癖があるのですが、海外サービスのAPIは、あらゆる日時をこのRFC3339形式(UTC)で扱うことが多く、DateオブジェクトからRFC3339形式の文字列に変換する方法などは、覚えておくと便利です。
シリーズ目次
- Gmailのスター付きメールからTodoistのタスクを作成&Slackに通知する その1 – isStarred()メソッドを使って、未処理のスター付きメールのみを絞り込む
- Gmailのスター付きメールからTodoistのタスクを作成&Slackに通知する その2 – Todoist APIの認証を通す
- Gmailのスター付きメールからTodoistのタスクを作成&Slackに通知する その3 – Todoist APIでタスクを新たに作成する
- Gmailのスター付きメールからTodoistのタスクを作成&Slackに通知する その4 – Incoming Webhooksを使ってSlackに通知
Google Apps Scriptを勉強したい方へ
この記事を見て、GASを勉強したいなと思われた方はぜひノンプログラマーのためのスキルアップ研究会(通称 ノンプロ研)にご参加ください。私も未経験からこの学習コミュニティに参加し、講座を受講したことでGASが書けるようになりました。
学習コミュニティ「ノンプログラマーのためのスキルアップ研究会」
挫折しがちなプログラミングの学習も、コミュニティの力で継続できます。ノンプロ研でお待ちしております!