TODO管理については、Google・Apple・Microsoftの超大手のアプリから、Todoist等の大手TODOアプリから、GTD等の方法論まで入り乱れています。
個人的にどれもしっくりこなかったので、LINEでTODOを簡単に作成して、簡単に管理するソフトを自作します。
TODO管理については本当に色々なものを試したのですが、本当に大事なことはそもそもTODOを増やさない/絞ることと気付きました。そのため、今回の記事内ではとてもシンプルな管理しかできないものになっています。複雑なものを作りたい場合は、ぜひご自身で改造してみてください。
もくじ
前提
- 作るものでできること
- TODOの入力と完了をLINEから管理できる
- タイムスタンプが自動で付いて、後から見返せる
- (期限の管理もしない)
- 利用する技術要素
- LINE Developer API
- AWS (S3, IAM, Lambda, API Gateway, CloudWatch)
- Python
- TODO哲学
- 結論、TODO管理/効率化はお遊びに過ぎない
- やる価値があることを見つけ出すことの方が大事で、難しい
- 100あるTODOをどれだけキレイに整理しても、無理なものは無理でしかない
- 100あるTODOを3個に絞って、「素早く」「試し続ける」ことの方が大事
- 好きで、特記戦力レベルの実力を持っていて、他人が望むものを基準にTODOを絞る
- (自分にしかできない良質なTODOを残すように意識していると、自然に良質なTODOが適切な数で循環するようになる)
- そうなるために、誰でもできそうなことは拒否し、もっと言えばそういったことから逃げ続ける
結論
AWS Lambdaに記載するPythonコードのコア部分は、下記のものになります。これだけでは動かず、後述の諸々の設定が必要です。
AWSのサービスを動かすと料金が発生するので、そこだけは注意してください。
import logging import os import urllib.request, urllib.parse import json import base64 import hashlib import hmac import boto3 import datetime logger = logging.getLogger() logger.setLevel(logging.INFO) def s3read (bucket, filename): s3 = boto3.client('s3') try: res = s3.get_object(Bucket=bucket, Key=filename) body = res['Body'].read() bodystr = body.decode('utf-8') return bodystr except: return '[{}]' def s3write (bucket, filename, s): s3 = boto3.resource('s3') file_contents = s bucket = s3.Object(bucket, filename) bucket.put(Body=file_contents) def lambda_handler(request, context): channel_secret = os.environ['LINE_CHANNEL_SECRET'] body = request.get('body', '') hash = hmac.new(channel_secret.encode('utf-8'), body.encode('utf-8'), hashlib.sha256).digest() signature = base64.b64encode(hash).decode('utf-8') if signature != request.get('headers').get('X-Line-Signature', ''): logger.info(f'LINE 以外からのアクセス request={request}') return {'statusCode': 200, 'body': '{}'} for event in json.loads(body).get('events', []): inpt = event['message']['text'] text = inpt # オウム返し logger.info(json.dumps(request)) logger.info(json.dumps(event)) url = 'https://api.line.me/v2/bot/message/reply' headers = { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + os.environ['LINE_CHANNEL_ACCESS_TOKEN'], } body = { 'replyToken': event['replyToken'], 'messages': [ { 'type': 'text', 'text': text, } ] } req = urllib.request.Request(url, data=json.dumps(body).encode('utf-8'), method='POST', headers=headers) with urllib.request.urlopen(req) as res: res_body = res.read().decode('utf-8') if res_body != '{}': logger.info(res_body) return {'statusCode': 200, 'body': '{}'}
手順
AWS S3
過去記事も参照しつつAWSのアカウントを作り、S3の画面に入ってください。開いた後に「Create bucket」をクリックします。

「Bucket name」に何でも良いので、名前を入れます。この名前は他人が作ったものとも被ってはいけないので、数字等を適当に組み合わせてください。その後、画面をスクロールして「Create bucket」をクリックします。

S3の設定はここまでです。入力した「Bucket name」は後ほど使用するので、コピーしておいてください。
AWS Lambda (1/3)
続いてLambdaを開き、「Create Function」をクリックします。

「Function name」に「todo」、「Runtime」に「Python 3.8」を入力し、「Create function」をクリックします。

「Permissions」をクリックし、続いて「role-name」のリンクをクリックします。

AWS IAM
そうすると、自動でIAMの画面が開きます。「Attach polices」をクリックします。

「Filter policies」に「s3」を入力し、「AmazonS3FullAccess」のチェックボックスをチェックし、「Attach policy」をクリックします。

これで、IAMの設定は完了です。
AWS API Gateway
今度は、API Gatewayを開きます。開いたら、「Create API」をクリックします。

「Rest API」の「Build」をクリックします。

「API name」に「todo」を入力し、「Create API」をクリックします。

「Actions > Create Resource」をクリックします。

「Resource Name」に「todo」と入力し、「Create Resource」をクリックします。

「Actions > Create Method」をクリックします。

「POST」を選択し、横のチェックマークをクリックします。

「Use Lambda Proxy integration」のチェックボックスをチェックし、「Lambda Function」に「todo」を入力した上で、「Save」をクリックします。その後、確認のポップアップが出るので「OK」をクリックします。

「Actions > Deploy API」をクリックします。

「Deployment stage」で「[New Stage]」を選択し、「Stage name」に「dev」を入力し、最後に「Deploy」をクリックします。

これで、API Gatewayの設定は完了です。
AWS Lambda (2/3)
Lambdaの画面に戻り「Configuration」をクリックした後に、「Add trigger」をクリックします。

「API Gateway」を選択し、「todo」「dev」「Open」を入力した上で、「Add」をクリックします。

「Details」をクリックし、表示されるURLをコピーしておきます。

Lambdaにはまた戻ってくるので、タブは開きっぱなしにしておいてください。
LINE Developer API
こちらのサイトを参考にして、LINE Developer APIのアカウントを作成してください。「Botのプロバイダーを作成しよう!」という節まで実施してください。
その後、「Create a new channel」を選びます。

Messaging APIを選択。

「Channel name」「Channel description」「Category」「Subcategory」に適当なものを記入し、ページ下部の規約を確認した上でチェックボックスをチェック、最後に「Create」をクリックする。
再度、規約確認のポップアップが出てくるので、確認して了承する。


画面切り替わり後、ページ下部の「Channel Secret」をどこかにコピります。

画面上部に戻り、「Messaging API」を選択する。

表示されるQRコードで、LINEから友達追加をしてください。

「Webhook settings」の「Edit」をクリックします。

先程LambdaでコピーしたURLを貼り付け、「Update」をクリックします。その上で、「Use Webhook」のボタンをオンにしておきます。

「Auto-reply messages」の「Edit」をクリックします。

「あいさつメッセージ」と「応答メッセージ」をオフにしておきます。

元の画面に戻り、「Channel access token」の「Issue」をクリックし、表示される文字列をコピペしておきます。

これで、LINE Developerの設定は完了です。
AWS Lambda (3/3)
「todo」アイコンをクリックします。

下にスクロールしていき、「Environment variables」の「Edit」をクリックします。

「Add environment variable」をクリックします。

「LINE_CHANNEL_ACCESS_TOKEN」と「LINE_CHANNEL_SECRET」のについて、LINE Developerの項でコピーした値を入れます。長い方の値が、「LINE_CHANNEL_ACCESS_TOKEN」です。入れたら、「Save」をクリックしてください。

上部にプログラムを記入できる欄があるので、既存のものを上書きする形で下記のコードをコピペします。その後、「Deploy」をクリックします。コードは、こちらのサイトを参考にさせていただきました。
なお、コード中のS3バケット名は、本手順の最初の方でコピーしたものに書き換えてください。

import logging import os import urllib.request, urllib.parse import json import base64 import hashlib import hmac import boto3 import datetime logger = logging.getLogger() logger.setLevel(logging.INFO) def s3read (bucket, filename): s3 = boto3.client('s3') try: res = s3.get_object(Bucket=bucket, Key=filename) body = res['Body'].read() bodystr = body.decode('utf-8') return bodystr except: return '[{}]' def s3write (bucket, filename, s): s3 = boto3.resource('s3') file_contents = s bucket = s3.Object(bucket, filename) bucket.put(Body=file_contents) def lambda_handler(request, context): channel_secret = os.environ['LINE_CHANNEL_SECRET'] body = request.get('body', '') hash = hmac.new(channel_secret.encode('utf-8'), body.encode('utf-8'), hashlib.sha256).digest() signature = base64.b64encode(hash).decode('utf-8') if signature != request.get('headers').get('X-Line-Signature', ''): logger.info(f'LINE 以外からのアクセス request={request}') return {'statusCode': 200, 'body': '{}'} for event in json.loads(body).get('events', []): inpt = event['message']['text'] text = '' now = (datetime.datetime.now() + datetime.timedelta(hours=7)) # ICT / Thai Time if inpt in ['todo', 'Todo']: bodystr = s3read('todo-for-lambda', 'todo.json') dics = json.loads(bodystr) for d in dics: if 'done' in d and not d['done']: text += '{:03}'.format(d['id']) + ': ' + d['text'].replace('\n', '\n ') + '\n' text = text[:-1] if text else 'There is no todo' elif inpt in ['review', 'Review']: bodystr = s3read('todo-for-lambda', 'todo.json') dics = json.loads(bodystr) for d in dics: done = '(DONE - ' + d['done'] + ') ' if d['done'] else '' text += '{:02}'.format(d['id']) + ': ' + done + ' ' + d['text'].replace('\n', '\n ') + ' [' + d['timestamp'] + ']\n' text = text[:-1] if text else 'There is no todo' elif inpt[:4] in ['done', 'Done']: bodystr = s3read('todo-for-lambda', 'todo.json') dics = json.loads(bodystr) if inpt[5:].isnumeric(): n = int(inpt[5:]) for i in range(len(dics)): if dics[i]['id'] == n: dics[i]['done'] = now.strftime('%Y-%m-%d %H:%M:%S') s3write('todo-for-lambda', 'todo.json', json.dumps(dics)) text = 'ID: ' + str(n) + ' was marked as done' break if i == len(dics)-1: text = 'There is no specified number' else: text = 'Please put "done" together with proper ID' else: bodystr = s3read('todo-for-lambda', 'todo.json') dics = json.loads(bodystr) if dics[0]: n = dics[-1]['id'] + 1 dics.append({'id': n, 'text': inpt, 'timestamp': now.strftime('%Y-%m-%d %H:%M:%S'), 'done': False}) else: dics = [{'id': 0, 'text': inpt, 'timestamp': now.strftime('%Y-%m-%d %H:%M:%S'), 'done': False}] s3write('todo-for-lambda', 'todo.json', json.dumps(dics)) text = 'New todo was registered' logger.info(json.dumps(request)) logger.info(json.dumps(event)) url = 'https://api.line.me/v2/bot/message/reply' headers = { 'Content-Type': 'application/json', 'Authorization': 'Bearer ' + os.environ['LINE_CHANNEL_ACCESS_TOKEN'], } body = { 'replyToken': event['replyToken'], 'messages': [ { 'type': 'text', 'text': text, } ] } req = urllib.request.Request(url, data=json.dumps(body).encode('utf-8'), method='POST', headers=headers) with urllib.request.urlopen(req) as res: res_body = res.read().decode('utf-8') if res_body != '{}': logger.info(res_body) return {'statusCode': 200, 'body': '{}'}
AWS CloudWatch
Lambdaが上手く動かない場合は、CloudWatchでログを追うことができます。「Log groups」をクリックします。

「aws/lambda/todo」をクリックします。

取り敢えず、一番上にあるログを見てみます。

一番上にある [ERROR] の行について、三角形をクリックします。

これで直すべきところが見えます。

まとめ
これで全手順が完了です。登録したLINE宛にメッセージを入れることで、下記の操作ができるようになっています。
- (TODO内容を入力): TODOが登録される
- “todo” or “Todo”: 登録したTODOを表示する
- “done XXX” or “Done XXX”: XXX番のTODOを完了扱いにする (todoメッセージで表示されないくなる)
- “review” or “Review”: 完了したTODO含め、タイムスタンプ付きで一覧表示する