LineからNotionにタスクを登録する 

# 背景
- 普段、todo管理にNotionを使用している。
- todo管理のデータベースは以下のようになっており、
実施する時間とカテゴリを設定できるようになっている。
![Screenshot 2024-01-28 9.34.36.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542172/d33ccd17-0341-2d85-51c8-25b8a0a84b18.png)

- todoの登録を通勤時間とかスキマ時間にぱぱっとしたいことがある。（Notionをいちいち開くのはめんどくさい）

# ゴール
![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542172/8cdc1e12-d2eb-e909-bb8a-65a39aa11276.gif)
こんな感じで、LineからNotionへtodoを登録し、種別の設定もできるようにする。


# 前提
- Lineをインストールしていること
- Notionをインストールしていること
- AWSのアカウントがあること

# 全体構成
LINEから、Lambdaを通してNotionへ。
```mermaid
flowchart LR

User[User]
Line[Line]
Lambda[AWS Lambda]
Notion[Notion]

User --> Line
Line --> Lambda
Lambda --> Notion
```

# 手順
以下の手順で、LineとNotionを連携していきます。
1. Notion APIの設定
2. Line Messaging APIの設定
3. AWS LambdaとLineの連携
4. AWS LambdaとNotionの連携


## Notion APIの設定
### My-Integrationsを設定しシークレットを取得
[My-Integrations](https://www.notion.so/my-integrations)より、Integrationsの設定を行い、シークレットを取得します。
![Screenshot 2024-01-28 9.49.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542172/fa9e3957-4249-79bc-f9f1-b4479f20b579.png)

### データベースをMy Integrationにコネクトする。
データベースを作成したら、画像のようにMyIntegrationをコネクトする。
![Screenshot 2024-01-28 14.44.46.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542172/8bab5347-93ae-cfff-57db-d58752d76a4a.png)


### データベースのIDを取得
https://www.notion.so/xxxxxxxxxxxxxx?v=yyyyyyyyyyyyyyyy
のxxxxxxxxxxxxxxがデータベースのIDとなります。


## Line Messaging APIの設定
1. [Messaging api](https://developers.line.biz/ja/services/messaging-api/)にログインをし、Providersを作成する。
![Screenshot 2024-01-28 10.04.14.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542172/4b31eedf-5839-0736-f1b7-d9f6e9b8f890.png)
2. Channelを作成する。（Channel typeをMessaging APIとする）
![Screenshot 2024-01-28 10.05.15.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542172/3d24c4c9-4d9a-1904-f551-1ba2a1c78d34.png)

## AWS LambdaとLineの連携
### AWS Lambdaの初期設定
#### 新しい関数をpythonで作成する。
↓作成直後画面。
![Screenshot 2024-01-28 10.12.26.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542172/851d1aaa-0231-5d9d-4b32-bdb9a4aeb1a7.png)

#### 関数URLを設定し、publicでアクセスできるようにする。
![Screenshot 2024-01-28 10.14.24.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542172/9795ab49-9028-9cb9-e626-c020ed0a4288.png)
![Screenshot 2024-01-28 10.14.51.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542172/4344a296-be95-2f22-ad61-f8f835c0d467.png)
認証タイプはNONE。


### AWS LambdaとLineの連携
先程設定した関数URLをLine Messaging APIのWebhook URLに入力する。
![Screenshot 2024-01-28 10.17.44.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542172/d2267098-f14e-f493-026a-17dac159da4c.png)
![Screenshot 2024-01-28 10.17.55.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542172/2d070405-a251-a692-65ca-7cdc20472209.png)

また、Use WebhookもONにしておく。また、Auto-reply messagesの応答メッセージもOFFにしておく。
Verifyを押して、「Success」が表示されたら連携成功。
これで、このMessaging APIにアクセスがあった時、AWS Lambdaの関数が呼び出されるようになった。
### Lineから送ったメッセージ情報をLambdaで取得する
手元のスマホから、このチャンネルにメッセージを送ってみる
![Screenshot 2024-01-28 10.33.17.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542172/48b016b4-8cd8-bd97-ce26-aef556ae301b.png)
この状態で、AWS Lambdaのログを見てみると、
![Screenshot 2024-01-28 10.34.30.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542172/dbc49e62-c0b2-3e93-f1bb-fb89bac19b45.png)
ログストリームにイベントが来ていることがわかる。つまり、Lineから送ったメッセージがMessaging APIを通して、AWS Lambdaに来ていることがわかった。

Lambdaから、Lineのメッセージの情報を取得したい。詳細は説明しませんが、以下のコードで取得できます。CHANNEL_SECRETとCHANNEL_ACCESS_TOKENは、messaging-apiの設定画面より取得してください。

<details><summary>プログラム例</summary><div>

```python
import json
import base64
import hashlib
import hmac
import os

CHANNEL_SECRET = os.environ['CHANNEL_SECRET']
CHANNEL_ACCESS_TOKEN = os.environ["CHANNEL_ACCESS_TOKEN"]


def lambda_handler(event, context):
    #signatureの取得
    x_line_signature = event["headers"]["x-line-signature"] if event["headers"]["x-line-signature"] else event["headers"]["X-Line-Signature"]
    body = event["body"]
    hash = hmac.new(CHANNEL_SECRET.encode('utf-8'), body.encode('utf-8'), hashlib.sha256).digest()
    signature = base64.b64encode(hash)
    print("event")
    print(event)
    
    #認証
    if signature != x_line_signature.encode():
        print("signature error")
        return {
            'statusCode': 200,
            'body': {"signature": x_line_signature}
        }
        
    for evt in json.loads(body)['events']:
        #イベント情報の取得
        if(evt["type"] == "message"):
            #メッセージタイプなら
            text = evt['message']['text']
            print("receive message", text) #textを表示
        
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }
```
</div></details>
これで、textに、Lineから送ったメッセージが格納されています。

## LambdaとNotionの連携
つぎは、LambdaとNotionを連携していきます。
[Notion APIの設定](##Notion-APIの設定)で取得したIntegrationのシークレットキーとデータベースのIDをLambdaの環境変数に追加していきます。
![Screenshot 2024-01-28 14.42.29.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542172/b7a52be0-1e83-a45b-79a5-d6dfc38533dc.png)
こんな感じになっていると思います。
これでLambdaからNotionのデータベースにアクセス可能となります。

## Lambdaを介してLineとNotionを連携する
### LineからNotionのデータベースへの追加
Lineで入力した項目をNotionのtodoデータベースに追加していきましょう。
コードは以下のとおりです。
※Line Messaging APIのreplyの細かい説明は省きます。

<details><summary>プログラム例</summary><div>

```python
import json
import base64
import hashlib
import hmac
import os
import urllib.request
import datetime
from dateutil import tz

CHANNEL_SECRET = os.environ['CHANNEL_SECRET']
CHANNEL_ACCESS_TOKEN = os.environ["CHANNEL_ACCESS_TOKEN"]
NOTION_SECRET = os.environ["NOTION_SECRET"]
ID_DB = os.environ["ID_DB"]

time_zone = tz.gettz('Asia/Tokyo')

now = datetime.datetime.now(tz=time_zone)
date_format1 = '%Y-%m-%dT12:00:00.000+09:00'
date_format2 = '%Y-%m-%dT13:00:00.000+09:00'


def lambda_handler(event, context):
    #signatureの取得
    x_line_signature = event["headers"]["x-line-signature"] if event["headers"]["x-line-signature"] else event["headers"]["X-Line-Signature"]
    body = event["body"]
    hash = hmac.new(CHANNEL_SECRET.encode('utf-8'), body.encode('utf-8'), hashlib.sha256).digest()
    signature = base64.b64encode(hash)
    print("event")
    print(event)
    
    #認証
    if signature != x_line_signature.encode():
        print("signature error")
        return {
            'statusCode': 200,
            'body': {"signature": x_line_signature}
        }

    for evt in json.loads(body)['events']:
        #イベント情報の取得
        if(evt["type"] == "message"):
            #メッセージタイプなら
            text = evt['message']['text']
            print("receive message", text) #textを表示
        
            url =  "https://api.notion.com/v1/pages"
            headers = {"content-type": "application/json", "Notion-Version": "2022-02-22", "Authorization": f"Bearer {NOTION_SECRET}"}
            json_data = {
                "parent": {"database_id": ID_DB}, 
                "properties": 
                {
                    "Name":
                    {
                        "title":
                        [
                            {
                                "text": {"content": text} #Lineで入力した項目をcontentに入れる
                            }
                        ]
                    },
                    "Date": {
                        "date": {
                            "start": now.strftime(date_format1),
                            "end": now.strftime(date_format2)
                        }
                    },
                }
            }
            req = urllib.request.Request(url, data=json.dumps(json_data).encode('utf-8'), method='POST', headers=headers)
            with urllib.request.urlopen(req) as res:
                json_data = json.loads(res.read())
                print(json_data)
                todo_id = json_data["id"].replace("-", "")
                
                reply_body = {
                    'replyToken': json.loads(body)['events'][0]['replyToken'],
                    'messages': [
                        {
                         'type': 'text',
                         'text': f"{text}を追加しました。id={todo_id}",
                       }
                    ]
                }
                headers = {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN}
                url = 'https://api.line.me/v2/bot/message/reply'
                req = urllib.request.Request(url, data=json.dumps(reply_body).encode('utf-8'), method='POST', headers=headers)
                with urllib.request.urlopen(req) as res:
                    print(res.read().decode("utf-8"))
                    
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }
```
</div></details>

※日付は、入力した日の12:00-13:00になるようにしています。
![Screenshot 2024-01-28 15.20.38.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542172/54815826-d4ec-995d-5306-fde25261e2f5.png)
![Screenshot 2024-01-28 15.13.21.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542172/4df4829f-c146-e424-58e9-309a01443528.png)
しっかりtodoが入っていますね。ここで、todoのidを取得しているのですが、理由は後述します。

次は、追加したLabelを更新していきましょう。

### カテゴリの更新
Lambdaで取得したNotionのLabelをLineで返答するようにしてみましょう。
※Line Messaging APIのreplyの細かい説明は省きます。

<details><summary>プログラム例</summary><div>

```python
import json
import base64
import hashlib
import hmac
import os
import urllib.request
import datetime
from dateutil import tz

CHANNEL_SECRET = os.environ['CHANNEL_SECRET']
CHANNEL_ACCESS_TOKEN = os.environ["CHANNEL_ACCESS_TOKEN"]
NOTION_SECRET = os.environ["NOTION_SECRET"]
ID_DB = os.environ["ID_DB"]


def lambda_handler(event, context):
    #signatureの取得
    x_line_signature = event["headers"]["x-line-signature"] if event["headers"]["x-line-signature"] else event["headers"]["X-Line-Signature"]
    body = event["body"]
    hash = hmac.new(CHANNEL_SECRET.encode('utf-8'), body.encode('utf-8'), hashlib.sha256).digest()
    signature = base64.b64encode(hash)
    print("event")
    print(event)
    
    #認証
    if signature != x_line_signature.encode():
        print("signature error")
        return {
            'statusCode': 200,
            'body': {"signature": x_line_signature}
        }
        
    #Notionで、Label(カテゴリ)を取得する。
    categoryList = []
    url =  "https://api.notion.com/v1/databases/" + ID_DB
    headers = {"content-type": "application/json", "Notion-Version": "2022-02-22", "Authorization": f"Bearer {NOTION_SECRET}"}
    req = urllib.request.Request(url, method='GET', headers=headers)
    with urllib.request.urlopen(req) as res:
        json_data = json.loads(res.read())
        print(json_data)
        options = json_data["properties"]["Label"]["select"]["options"]
        for option in options:
            #option["name"]にLabelの種類が入っている。
            categoryList.append(option["name"])


    #Lineでカテゴリを選ばせる
    items = [] #LineのquickReplyで選ばせるitems
    for c in categoryList:
        items.append({"type": "action", "action": {"type": "message", "label": c, "text": c}})

    headers = {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN}
    print(json.loads(body)['events'][0]['replyToken'])
    body = {
        'replyToken': json.loads(body)['events'][0]['replyToken'],
        'messages': [
            {
             'type': 'text',
             'text': "カテゴリを選んでください",
             "quickReply": {
                "items": items
              }
           }
        ]
    }
    url = 'https://api.line.me/v2/bot/message/reply'
    req = urllib.request.Request(url, data=json.dumps(body).encode('utf-8'), method='POST', headers=headers)
    with urllib.request.urlopen(req) as res:
        print(res.read().decode("utf-8"))
        

    #一旦コメントアウト        
    # for evt in json.loads(body)['events']:
    #     #イベント情報の取得
    #     if(evt["type"] == "message"):
    #         #メッセージタイプなら
    #         text = evt['message']['text']
    #         print("receive message", text) #textを表示
        
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }

```
</div></details>

すると、下の画像のようにNotion APIから取得したカテゴリの候補が表示されます。
![Screenshot 2024-01-28 14.57.11.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542172/025b12ba-cd6f-ed77-1f02-033d40c54cfa.png)

このカテゴリ1、カテゴリ2、カテゴリ3のいずれかをクリックしたら、todoのLabelを更新するようにしたいです。
todoを追加→カテゴリ選択の流れにするため、カテゴリ選択時、todoのid情報もセットでLineに送ってやる必要があります。

ここで一工夫します。先程取得したtodoのidをLineのreplyに追加します。
```python
items.append({"type": "action", "action": {"type": "message", "label": c, "text": todo_id + "&&&" + c}})
```
これで、ユーザーがLine上でカテゴリを選択した時、todoのidも一緒にLambdaに送られます。
Lambda上では、&&&を区切り文字として区切ってやることで、todoのidとカテゴリ情報の２つの情報を扱うことができます。
```python
tmp = message.split("&&%")
# tmp[0]にはtodoのid情報が、tmp[1]にはカテゴリ情報が格納されている。
```

ただこのままだと、Lambdaはカテゴリを更新することができないので、

Lineのメッセージに"&&&"が含まれたらtodo更新する。
Lineのメッセージに"&&&"が含まれなかったらtodo追加する。

として処理します。


最終的なコードは以下のとおりです。
<details><summary>プログラム例</summary><div>

```python
import json
import base64
import hashlib
import hmac
import os
import urllib.request
import datetime
from dateutil import tz

CHANNEL_SECRET = os.environ['CHANNEL_SECRET']
CHANNEL_ACCESS_TOKEN = os.environ["CHANNEL_ACCESS_TOKEN"]
NOTION_SECRET = os.environ["NOTION_SECRET"]
ID_DB = os.environ["ID_DB"]

time_zone = tz.gettz('Asia/Tokyo')

now = datetime.datetime.now(tz=time_zone)
date_format1 = '%Y-%m-%dT12:00:00.000+09:00'
date_format2 = '%Y-%m-%dT13:00:00.000+09:00'


def lambda_handler(event, context):
    #signatureの取得
    x_line_signature = event["headers"]["x-line-signature"] if event["headers"]["x-line-signature"] else event["headers"]["X-Line-Signature"]
    body = event["body"]
    hash = hmac.new(CHANNEL_SECRET.encode('utf-8'), body.encode('utf-8'), hashlib.sha256).digest()
    signature = base64.b64encode(hash)
    print("event")
    print(event)
    
    #認証
    if signature != x_line_signature.encode():
        print("signature error")
        return {
            'statusCode': 200,
            'body': {"signature": x_line_signature}
        }

    for evt in json.loads(body)['events']:
        #イベント情報の取得
        if(evt["type"] == "message"):
            #メッセージタイプなら
            text = evt['message']['text']
            if("&&&" in text): #&&&が入ってたら、複合メッセージ。&&&が入ってなかったら、タスクを登録する。
                tmp = text.split("&&&") #&&&で分ける
                url =  "https://api.notion.com/v1/pages/" + tmp[0]
                headers = {"content-type": "application/json", "Notion-Version": "2022-02-22", "Authorization": f"Bearer {NOTION_SECRET}"}
                json_data = {
                    "parent": {"database_id": ID_DB}, 
                    "properties": 
                    {
                        "Label": {
                          "select": 
                            {
                              "name": tmp[1]
                            }
                        }                             
                    }
                }
                req = urllib.request.Request(url, data=json.dumps(json_data).encode('utf-8'), method='PATCH', headers=headers)
                with urllib.request.urlopen(req) as res:
                    headers = {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN}
                    body = {
                        'replyToken': json.loads(body)['events'][0]['replyToken'],
                        'messages': [
                            {
                             'type': 'text',
                             'text': "更新しました。",
                           }
                        ]
                    }
                    url = 'https://api.line.me/v2/bot/message/reply'
                    req = urllib.request.Request(url, data=json.dumps(body).encode('utf-8'), method='POST', headers=headers)
                    with urllib.request.urlopen(req) as res:
                        print(res.read().decode("utf-8"))
            else:
                url =  "https://api.notion.com/v1/pages"
                headers = {"content-type": "application/json", "Notion-Version": "2022-02-22", "Authorization": f"Bearer {NOTION_SECRET}"}
                json_data = {
                    "parent": {"database_id": ID_DB}, 
                    "properties": 
                    {
                        "Name":
                        {
                            "title":
                            [
                                {
                                    "text": {"content": text} #Lineで入力した項目をcontentに入れる
                                }
                            ]
                        },
                        "Date": {
                            "date": {
                                "start": now.strftime(date_format1),
                                "end": now.strftime(date_format2)
                            }
                        },
                    }
                }
                req = urllib.request.Request(url, data=json.dumps(json_data).encode('utf-8'), method='POST', headers=headers)
                with urllib.request.urlopen(req) as res:
                    json_data = json.loads(res.read())
                    print(json_data)
                    todo_id = json_data["id"].replace("-", "")
                    
                    #Notionで、Label(カテゴリ)を取得する。
                    categoryList = []
                    url =  "https://api.notion.com/v1/databases/" + ID_DB
                    headers = {"content-type": "application/json", "Notion-Version": "2022-02-22", "Authorization": f"Bearer {NOTION_SECRET}"}
                    req = urllib.request.Request(url, method='GET', headers=headers)
                    with urllib.request.urlopen(req) as res:
                        json_data = json.loads(res.read())
                        print(json_data)
                        options = json_data["properties"]["Label"]["select"]["options"]
                        for option in options:
                            #option["name"]にLabelの種類が入っている。
                            categoryList.append(option["name"])
                
                
                    #Lineでカテゴリを選ばせる
                    items = [] #LineのquickReplyで選ばせるitems
                    for c in categoryList:
                        items.append({"type": "action", "action": {"type": "message", "label": c, "text": todo_id + "&&&" + c}})
                
                    headers = {'Content-Type': 'application/json', 'Authorization': 'Bearer ' + CHANNEL_ACCESS_TOKEN}
                    print(json.loads(body)['events'][0]['replyToken'])
                    body = {
                        'replyToken': json.loads(body)['events'][0]['replyToken'],
                        'messages': [
                            {
                             'type': 'text',
                             'text': "カテゴリを選んでください",
                             "quickReply": {
                                "items": items
                              }
                           }
                        ]
                    }
                    url = 'https://api.line.me/v2/bot/message/reply'
                    req = urllib.request.Request(url, data=json.dumps(body).encode('utf-8'), method='POST', headers=headers)
                    with urllib.request.urlopen(req) as res:
                        print(res.read().decode("utf-8"))
                    
    return {
        'statusCode': 200,
        'body': json.dumps('Hello from Lambda!')
    }
```
</div></details>

すると、
![output.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/542172/8cdc1e12-d2eb-e909-bb8a-65a39aa11276.gif)
こんな感じでtodoの追加、カテゴリの更新ができる。
手元のスマホからNotionのデータベースにデータを追加できるのは使い勝手が良いので、皆様も是非試してください。