LINE bot と Notion API を組み合わせて自分用フィードアプリ（？）を作ってみた！！

# はじめに
突然ですが、今話題沸騰中のNotionというコラボレーションツールをご存じでしょうか。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2135866/00b81bff-d054-c384-d0a7-25252b61145e.png)


多機能かつシンプルなノート機能に加えて、チームコラボレーション機能やほかのアプリとの連携機能が充実しておりすべての情報をNotionに集約させるという使い方ができます。そのため、**オールインワンワークスペース**なんて銘打っていたりもします。


https://www.notion.so/ja-jp/product?utm_source=google&utm_campaign=14877655427&utm_medium=125922091617&utm_content=552465064293&utm_term=notion&targetid=aud-1190785634793:kwd-312974742&gclid=CjwKCAjwn8SLBhAyEiwAHNTJbW5YFn2iY34pjsQJHEtlcD04_r_lc3Of-tZ4ItY7tbd-rhzl0-4CjRoCL4MQAvD_BwE



私はNotionと一緒にSave to Notion[^1]というChromeの拡張機能を使い、NotionをWebクリップとして活用しています。
保存すると画像のように一覧で表示されるよう設定しており、読み終わった記事にはチェックを入れて管理しています。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2135866/69dcc85f-33ab-7c04-1a91-bf472fb9ef4c.png)

同じように利用されている方ならきっと共感していただけると信じているのですが、

### せっかく良記事をクリップしたのにたまるばかりで一向に消化できない！！

なんてことありませんかね？

私の場合、先ほどの画像の下に50件ほどの未読の記事がたまってしまいました。。。

そこで日々の通勤時間やちょっとした隙間時間にも消化できるよう、
Notion API と LINE Botを組み合わせて、自分用フィードアプリ（？）を作ってみました！！


# 構成と環境
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2135866/c5c71113-6d49-3bee-5fa4-cec0615e0d64.png)

### API
・Notion API
・LINE Messaging API

### 実行環境
・Node.js  v6.10.0
・Notion SDK for JavaScript 0.4.4
・LINE Messaging API SDK 7.4.0
・Express 4.17.1
・Ngrok 2.3.4
・Powershell 5.1.19041

# 仕組み
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2135866/bed06c8a-4815-a9f4-3ba0-6c98f5dc8f1a.png)



先に種明かしをしておきますと、NotionのデータベースのレコードからAPIで一部の属性を取得し、
その内容をLINE Botを経由してスマートフォンに送り付けるというシンプルな仕組みです。
今回はたまったWebクリップから未読の状態のものを抽出し、その記事のタイトルとURLを5件LINEに送っています。

# 成果物
長くなるので先に実際の動作をご覧ください。
LINE上で"未読記事"と入力すると、「これを読め！！！」と言わんばかりに5件送り付けてくれます。
[![Image from Gyazo](https://i.gyazo.com/783bfccbb19829e2427512441542baa6.gif)](https://gyazo.com/783bfccbb19829e2427512441542baa6)　[![Image from Gyazo](https://i.gyazo.com/39dbc8f1029b15bb29d30ba2852ea7c6.gif)](https://gyazo.com/39dbc8f1029b15bb29d30ba2852ea7c6)






# 書いたJavaScript
### 下準備
importやクレデンシャルを用意します。'xxxxx'となっている部分はご自身のもので置き換えてください。

```Javascript
// ローカル（自分のPC）でサーバーを公開するときのポート番号です
const PORT = process.env.PORT || 3000;

//client Libraryのインポート
import  { Client }   from "@notionhq/client"
import  express       from "express";
import  line        from "@line/bot-sdk";


// Messaging APIで利用するクレデンシャル（秘匿情報）です。
const config = {
  channelSecret: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
  channelAccessToken: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx....'
};

//Notion APIのクレデンシャル (Internal Integration Token)
const notion = new Client({ auth: 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' });

//Notion データベースのID
const databaseId = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx';
```


### Notion API
ここでは関数`getNotionPages()`を定義し、次の二つのことを実行しています。
まず`notion.database.query`で対象のデータベースから未読となっているページのIDを取得します。
その次に`notion.pages.retrieve`関数にページIDを渡しタイトルとURLを取得したうえで、`Promise`オブジェクトで返しています。

```JavaScript
//Notionに保存した記事のURLとタイトルを取得する
async function getNotionPages(){
  
  //からの配列を宣言しておく
  let notion_page =[]; 

  //未読記事を取得（readが"false"のレコードを抽出する）
  const notion_db_childitem = await notion.databases.query(
    { database_id : databaseId,
      filter: {
        or: [
            { 
                property: 'read',
                checkbox: {
                    equals: false,
                },
            },
        ]
      },
      sorts: [
          {
              property: 'Name',
              direction: 'descending',
          },
      ]
    }
  )

  //ランダムに未読記事のURLとタイトルを10件づつ取得する 
  for(let i = 0; i < 5 ; i++){
    
    //ランダムな整数を生成（0以上、DBの要素数以下）
    let n = Math.floor(Math.random() * notion_db_childitem.results.length);

    const page = await notion.pages.retrieve ({ 
      page_id: notion_db_childitem.results[n].id
    })
    
    //要素[タイトル、URL]を追加
    notion_page.push([page.properties.Name.title[0].plain_text,page.properties.URL.url])
  }
  
  return  Promise.resolve(notion_page);
}
```
### LINE bot

LINE Messaging APIのwebhookという機能を使って、ユーザーからのテキスト送信をリッスンしています。
テキストが送られてきた場合に、`sampleFunction`を実行しテキストが`未読記事`か判定します。
trueだった場合には上記の`getNotionPages()`を実行したのち、記事のタイトルとURLをLINE botに送信しています。

```JavaScript
//関数
const sampleFunction = async (event) => {
    // ユーザーメッセージが「未読記事」かどうか
    if (event.message.text !== '未読記事') {
        return client.replyMessage(event.replyToken, {
            type: 'text',
            text: '「未読記事」と入力してください'
        });
    } else {
        // 「リプライ」を使って先に返事しておきます
        await client.replyMessage(event.replyToken, {
            type: 'text',
            text: '記事を取得中です……'
        });

        //返信用テキスト
        let pushText ='';
        let pushtext_title  = '';
        let pushtext_url    = '';
        
        
        try {
            await getNotionPages().then(
              result => {
                for(let elm of result){
                  pushtext_title  = elm[0];
                  pushtext_url    = elm[1];
                  client.pushMessage(event.source.userId, {
                    type: 'text',
                    text: pushtext_title + '\n' + pushtext_url
                });
                }
              }
            );
          } catch (error) {
              pushText = '検索中にエラーが発生しました。ごめんね。';
            // APIからエラーが返ってきたらターミナルに表示する
            console.error(error);
        }

        // 「プッシュ」で後からユーザーに通知します
        return client.pushMessage(event.source.userId, {
          type: 'text',
          text: '5件の未読記事を表示しました。'  
          },);
      }
};


// ########################################
//  LINEサーバーからのWebhookデータを処理する部分
// ########################################

// LINE SDKを初期化します
const client = new line.Client(config);

// LINEサーバーからWebhookがあると「サーバー部分」から以下の "handleEvent" という関数が呼び出されます
async function handleEvent(event) {
    // 受信したWebhookが「テキストメッセージ以外」であればnullを返すことで無視します
    if (event.type !== 'message' || event.message.type !== 'text') {
        return Promise.resolve(null);
    }
    // サンプル関数を実行します
    return sampleFunction(event);
}



// ########################################
//          Expressによるサーバー部分
// ########################################

// expressを初期化します
const app = express();

// HTTP POSTによって '/webhook' のパスにアクセスがあったら、POSTされた内容に応じて様々な処理をします
app.post('/webhook', line.middleware(config), (req, res) => {
  
  // 検証ボタンをクリックしたときに飛んできたWebhookを受信したときのみ以下のif文内を実行
  if (req.body.events.length === 0) {
    res.send('Hello LINE BOT! (HTTP POST)'); // LINEサーバーに返答します（なくてもよい）
    console.log('検証イベントを受信しました！'); // ターミナルに表示します
    return; // これより下は実行されません
  } else {
    // 通常のメッセージなど … Webhookの中身を確認用にターミナルに表示します
    console.log('受信しました:', req.body.events);
  }

  // あらかじめ宣言しておいた "handleEvent" 関数にWebhookの中身を渡して処理してもらい、
  // 関数から戻ってきたデータをそのままLINEサーバーに「レスポンス」として返します
  Promise.all(req.body.events.map(handleEvent)).then((result) => res.json(result));
});

// 最初に決めたポート番号でサーバーをPC内だけに公開します
// （環境によってはローカルネットワーク内にも公開されます）
app.listen(PORT);
console.log(`ポート${PORT}番でExpressサーバーを実行中です…`);
``` 

# 改善点

現状はシンプルな造りなので次のようなことができればより良くなると考えています。
・LINE BOT のリッチメニューの導入

https://developers.line.biz/ja/docs/messaging-api/using-rich-menus/

・LINE BOTから読了状態を更新する（`notion.database.update()`でできそう）


https://developers.notion.com/reference/update-a-database

:::note warn
作るとは言っていない
:::

# 終わりに
LINEを四六時中触っているということもあり、隙間時間に記事が送られてくるとシームレスに学習を続けられて結構便利です。
ただ、Notion API はまだベータ版が公開されて間もないということもあり、情報が少なく躓くところが多々ありました。
この記事がAPIをたたいてみるという方の一助となれば幸いです。





[^1]: https://chrome.google.com/webstore/detail/save-to-notion/ldmmifpegigmeammaeckplhnjbbpccmm