AlexaとTodoistでやることリスト・お買い物リスト

Todoistって、Amazon Echoと連携できるんですね！
しかもWebAPIが充実しているので、さらに連携の輪が広がりそうです。

ちなみに、Todoistは、タスク管理ツールです。一般には、やることリストとか、買い物リストが挙げられますが、メモ的に使えて、かつ、期限を設定して予定を立てたり、失念するのを避けるのに役立ちます。

todoist
　https://todoist.com/

今回作成する全体の構成はこんな感じです。
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/866e09d3-ba34-8361-2ba2-50d40d100c0f.png)

すでに、todoistはAlexaとの連携をサポートしていますし、AndroidやiPhone用のアプリもありますので、その部分は特に難しいところはありません。
今回は、やることリスト・買い物リストの表示をWebページとして表示します。todoistはオフィシャルでWebAPIが充実していますし、npmモジュールもあるので、活用の幅が広がります。
また、さらに、PWA化することでネイティブアプリ化し、それをAndroidで固定アプリに設定することで、やることリスト表示の専用機に仕立て上げます。

毎度のことですが、ソースコードもろもろをGitHubに上げておきました。

poruruba/todolist
　https://github.com/poruruba/todolist

#準備

##todoistのアカウント登録

まずは、以下のURLから、todoistのアカウント登録をします。

todoist
　https://todoist.com/ja

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/11257668-8801-4ff8-c241-d4951867d808.png)

「はじめる」ボタンまたは右上のサインアップをクリックします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/3a03117b-d6ff-b036-bed6-af509e50a159.png)

ここでは、スマートフォンにインストールしているAmazon Alexaアプリに設定しているアカウントでサインアップし、認証を完了させます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/963b8770-3167-65a9-0683-e2440ebe6be7.png)

これでアカウントが登録されました。

##todoistアプリをインストール

Google Playから、Todoistをインストールします。
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/938bfb45-6af3-2612-b1ee-689dfb8ae2a4.png)

todoistにアカウント登録したときのアカウントでログインします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/302367ed-d817-f788-dfb7-724cc7b66438.png)

ログインが完了しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/d7768d12-7e1d-229f-e8fb-1a257b4af06e.png)

##Alexa連携を設定

すでに、スマートフォンに、「Amazon Alexa」がインストールされている前提で進めます。
以降は、Androidでの操作です。

まずは、下の方の「その他」→「設定」→「リスト」を選択します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/99bad897-4c23-2037-ac6a-2c53499e7405.png)

そうすると、Any.do、AnyList、Todoistが表示されていますので、Todoistの右側の⊕をタッチします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/06e9b098-edb0-b0c1-0f3e-d2985b01fbde.png)

以下のように表示されるので、「有効にして使用する」ボタンを押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/2509f64f-4216-0f75-66d5-35255eded3b4.png)

リストへのアクセス権の確認が表示されますので、「アクセス権を保存」ボタンを押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/efa8cee5-d2ef-423e-c995-5f3940114044.png)

そうすると、todoistのサイトに飛んで、Agreeするかが聞かれますので、「Agree」ボタンを押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/fce0f8fd-4829-80a0-f2c6-e0242dec3caa.png)

これで、連携設定が完了しました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/a506a73b-a9de-aa2d-d0dc-cf857a6f453e.png)

もう一度、todoistアプリの方を開いてみましょう。
そうすると、左上の三のマークをタッチし、プロジェクトのところをタッチすると、「Alexa ToDo リスト」というのと、「Alexaの買い物リスト」が増えているのがわかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/d598c4bc-c70d-b858-dda8-550b95d4811b.png)

最後に、この「Alexa ToDo リスト」と「Alexaの買い物リスト」をお気に入りにしておきます。
「プロジェクト」→「プロジェクトを管理」をタッチし、「Alexa ToDo リスト」と「Alexaの買い物リスト」の右側にあるハートマークをタップし、赤色にします。これによって、この2つがお気に入りのプロジェクトとなりました。この意味はあとで、わかります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/eaa2000b-815d-fa41-ec31-c92b1897ab07.png)

あと、お好みで、todoistアプリをAndroidのホーム画面にウィジェットとして登録してもよいかと思います。

#とりあえずAlexa＋todoist連携を試してみる

以下に、発話例がありますので、試してみましょう。

Amazon Alexa で Todoist を使う
　https://get.todoist.help/hc/ja/articles/360010721059-Amazon-Alexa-%E3%81%A7-Todoist-%E3%82%92%E4%BD%BF%E3%81%86

例えば以下をAmazon Echoに話しかけてみましょう。

・アレクサ、やることリストに洗濯を追加して。
・アレクサ、今日のやることリストは？
・アレクサ、やることリストの洗濯を完了にして
・アレクサ、買い物リストに納豆を追加して
・アレクサ、今日の買い物リストは？
・アレクサ、買い物リストの納豆を完了にして

めでたく、こんな感じで追加されました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/5d930594-d43d-1c74-d61f-f5661307719d.png)

#Node.jsサーバからtodoistのリストを取得する

たいていの方は上記まででよいのですが、勉強をかねて、拡張に挑戦します。
Node.jsからの操作には、以下のnpmモジュールを利用させていただきました。

romgrk/node-todoist
　https://github.com/romgrk/node-todoist

以下も使っています。

node-fetch/node-fetch
　https://github.com/node-fetch/node-fetch

リスト取得する前に、ユーザごとにAPIトークンの取得が必要です。

todoist Developer: Authorization
　https://developer.todoist.com/sync/v8/#authorization

OAuthに似たやり取りで、ブラウザ側とサーバ側での連携が必要です。

まずは、サーバのURLをtodoistに登録しておく必要があります。以下のURLを開いて、「Create a new app」ボタンを押下します。

App Management Console
　https://developer.todoist.com/appconsole.html

 ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/21612751-e05e-bbc0-4853-f818f0fe8783.png)

App display nameには適当な名前を、App service URLには、これから立ち上げるNode.jsのWebページのURLを指定しておきます。

次に作成したappを選択肢、OAuth redirect URLを指定します。後で作成するのですが、立ち上げるNode.jsのWebページと同じにしておきます。（Single Page Applicationとして実装するため）。最後に、「Save settings」ボタンを押下します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/fcccd22c-1574-22de-f065-cf87983ca598.png)

その時に表示される、「Client ID」「Client secret」を覚えておきます。後で使うので。

①サーバ側に、ユーザ識別子.jsonというファイルを作成しておきます。
これがばれてしまうと、ログインされてしまいますので、ユーザ識別子は、推測されにくいランダムな値にしましょう。

②ブラウザからClient IDを指定して、ログインを開始する。

以下のように形成されるURLにジャンプします。

```https://todoist.com/oauth/authorize?client_id=" + TODOIST_CLIENT_ID + "&scope=data:read&state=" + value```

TODOIST_CLIENT_IDは、先ほど覚えておいたClient IDです。stateには正しくは乱数等推測されにくいものにするのですが、手を抜いていて、ユーザの識別子を指定します。①の通り、このユーザの識別子の名前＋.jsonで、サーバ側にファイルが作成されている前提です。

③todoistのログインページが表示されるので、ログインする。

todoistのアカウント登録時に使った認証アカウントでログインします。

④認可コードを取得する

ログインが完了すると、OAuth redirect URLで指定したURLにジャンプしてきます。その時に、認可コードとstateが返ってきます。
stateは、②で指定した値のはずです。

⑤認可コードからAPIトークンを取得する

認可コードを取得したので、これを使って以降のtodoistのWebAPI呼び出しに必要なAPIトークンを取得します。この取得には、Client secretが必要です。秘匿の値として扱う必要があるので、Nodeサーバに渡して、サーバ側で実施します。

```js:api/controllers/todoist/index.js
exports.handler = async (event, context, callback) => {
	var body = JSON.parse(event.body);
	var apikey = event.requestContext.apikeyAuth.apikey;
	if( !checkAlnum(apikey) )
		throw 'apikey invalid';

	var conf = await readConfigFile(apikey);

	if( event.path == '/todoist-callback' ){
		var param = {
			client_id: TODOIST_CLIENT_ID,
			client_secret: TODOIST_CLIENT_SECRET,
			code: body.code
		};
		var json = await do_post("https://todoist.com/oauth/access_token", param );

		conf.token = json;
		await writeConfigFile(apikey, conf);

		return new Response({});
        }else
```

body.codeが、ブラウザから取得した認可コードです。
そうすると、todoistサーバから、APIトークンが返ってくるので、それをユーザ識別子ごとのファイルに保存します。ユーザ識別子はブラウザ側からAPI KeyとしてHTTPヘッダに指定してもらいます。

あとは、ブラウザからのリクエストに対して、todoistのnpmモジュールTodoistを使ってリストを取得します。取得には、APIトークンが必要ですので、ユーザ識別子ごとのファイルから取り出して使っています。

```js:api/contollers/todoist/index.js
	if( event.path == '/todoist-list' ){
		const todoist = Todoist(conf.token.access_token);

		await todoist.sync();
		const projects = todoist.projects.get();
		var favorite = projects.filter(item => item.is_favorite );
		var favorite_ids = favorite.map(item => item.id);
		const items = todoist.items.get();
		var favorite_items = items.filter(item => favorite_ids.includes(item.project_id));

		const notes = todoist.notes.get();
		var item_ids = favorite_items.map(item => item.id);
		var favorite_notes = notes.filter(item => item_ids.includes(item.item_id));

		return new Response({items: favorite_items, projects: projects, notes: favorite_notes });
	}
```

あと、細かな処理をしていますが、やっているのは、
・プロジェクトIDからプロジェクト名に変換するために、プロジェクト一覧を取得
・プロジェクトのリストから、お気に入りにしたプロジェクトのIDを取得
・すべてのリストを取得し、お気に入りのプロジェクトIDのものを抽出
・すべてのノートを取得し、お気に入りのプロジェクトIDのものを抽出

(参考) todoist Sync API
　https://developer.todoist.com/sync/v8/

さきほどの、ブラウザ側とサーバ側の間の認可コードのやり取りにおいて、ブラウザ側の部分は以下です。

```public/js/start.js
        if( searchs.code ){
            var param = {
                code: searchs.code
            };
            history.replaceState(null, null, '.');
            do_post_apikey(base_url + '/todoist-callback', param, searchs.state)
            .then(json =>{
                console.log(json);
                Cookies.set("todo_apikey", searchs.state, { expires: EXPIRES });
                this.apikey = searchs.state;
                this.todo_list_update()
                .then(() =>{
                    setInterval( () =>{
                        this.todo_list_update(true);
                    }, UPDARTE_INTERVAL * 60 * 1000);
                });
            });
```

認証およびAPIトークンがサーバ側で保持出来たら認証完了です。ブラウザ側では、ユーザ識別子をapikeyとしてCookieに保持しておきます。

リスト取得は以下の部分です。

```js:public/js/start.js
        todo_list_update: async function(silent){
            if( !this.apikey )
                return;

            try{
                if( !silent )
                    this.progress_open();
                var param = {};
                var json = await do_post_apikey(base_url + '/todoist-list', param, this.apikey);

                this.todo_projects = json.projects;
                this.todo_notes = json.notes;

                var today = new Date();
                today.setHours(0, 0, 0, 0)
                var todayTime = today.getTime();
                var tomorrow = new Date();
                tomorrow.setHours(0, 0, 0, 0)
                tomorrow.setDate(tomorrow.getDate() + 1);
                var tomorrowTime = tomorrow.getTime();

                this.todo_list_expire = json.items.filter( item => item.due && Date.parse(item.due.date) < todayTime );
                this.todo_list_today = json.items.filter( item => item.due && Date.parse(item.due.date) >= todayTime && Date.parse(item.due.date) < tomorrowTime );
                this.todo_list_other = json.items.filter( item => item.due && Date.parse(item.due.date) >= tomorrowTime );
                this.todo_list_someday = json.items.filter( item => !item.due );
            }catch(error){
                console.error(error);
                alert(error);
            }finally{
                if( !silent )
                    this.progress_close();
            }
        },
```

これまたいろいろやっていますが、要は、期限切れのタスクの抽出、今日のタスクの抽出、明日以降のタスクの抽出、期限が設定されていないタスクの抽出、をしています。
this.todo_list_XXXXという変数に格納していますが、あとはVueが表示をよろしくやってくれます。

ブラウザで表示してみましょう。

　https://【立ち上げたサーバのURL】/index.html

最初に、API Keyの設定が必要です。「API Key」ボタンを押下して、ユーザ識別子を設定します。
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/a03d859f-3b46-57cf-9311-43c88084b187.png)

さきほど追加した「洗濯」が表示されています。
期限を設定していなかったので、いつか のグループに入っています。
 
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/a58800de-31a0-999b-1edd-7acf98503970.png)
 
todoistのAndroidアプリでもいいですし、todoistのWebページからでもどちらでよいですが、「洗濯」タスクの期限を今日にしてみましょう

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/6ba18078-e410-b571-b01b-4b1578a57af1.png)

10分ごとに、リロードしておいたので、ちょっと？待てば、今日のタスクに移動するかと思います。

# PWA化する

詳細は、以下を参考にしてください。今回はPush通知は使っていません。

[PWAを試してみよう](https://qiita.com/poruruba/items/77cbb6448d804e6e996b)

以下を追記しています。

index.htmlに以下を追加。

```html:public/index.html
  <link rel="manifest" href="manifest.json">
```

ページロード直後に以下を呼び出し。

```js:public/js/start.js
        if ('serviceWorker' in navigator) {
            navigator.serviceWorker.register('sw.js').then(async (registration) => {
                console.log('ServiceWorker registration successful with scope: ', registration.scope);
            }).catch((err) => {
                console.log('ServiceWorker registration failed: ', err);
            });
        }
```

sw.jsを作成し、public/sw.jsにデプロイ

これで、右上のアドレスバーのところの⊕を押せば、PWAアプリとしてインストールできるようになっているかと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/59febb61-fa4a-4ace-23a1-d62ccc8ff6d4.png)

#Androidでアプリ固定化

余っているAndroidタブレットを使って、常時リスト表示したいと思います。

Androidから同様にブラウザ(Chrome)から開き、メニューから「アプリをインストール」を選択して、アプリとしてインストールしておきます。
まずは、単独で起動でき、リストが表示されるところまで確認しておきます。

次に、アプリを固定化します。
（以降は、Androidのバージョンによって見え方は違うかもしれません。）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/1a02f77d-ef6d-b751-3dc9-5d9683080866.png)

Androidの「設定」→「セキュリティ」→「アプリの固定」を選択します。
おそらくOFFになっているかと思いますので、ONにします。

次に、現在実行中のアプリ一覧を表示し、PWAアプリとして起動したものの上にあるアイコンをタッチします。
そうすると、「固定」があるので、それをタッチします。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/3f1cdad2-bb59-1c27-984f-5c8f73893c0a.png)

こんな表示が出てきますので、よく読んで「OK」をタッチします。
これで、常時表示ができました。他のアプリが選択できなくなったと思います！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/c8caa591-274e-f16b-401f-45b5f08f4ce3.png)

できました！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/261826/ea2807f2-00ba-3e04-eb8d-0d61963a5fea.png)

アプリ固定を解除して戻りたい時には、ホームボタンと戻るボタンを一緒に長押しすれば戻れます。

#終わりに

続編です。
　 [LINEボットでtodoist連携](https://qiita.com/poruruba/items/7ea8cc5bcd025d9069d8)

以上