[TypeScript] BoltでSlack Appを作成する ④Notion APIを叩いてタスク一覧を取得する

2022/08/05

TypeScript
Slack API
Bolt
Notion API
FireStore
Firebase

TypeScriptでSlack Appを作成してみたので、その方法の紹介です。

作成したアプリはこちら

https://github.com/irori-dev/SlackApp

前回までで、

  1. ローカルで動くところまで
  2. GAEへのデプロイと、GirtHub Actionsでデプロイできるように
  3. slackからGitHub APIを叩いて、GirtHub Actionsを実行できるように

という状態になっています。

今回は、Firestoreを活用してNotion APIとSlackとの連携をしようと思います😎

前回までの記事はこちら。

  1. [TypeScript] BoltでSlack Appを作成する ①ローカルで動かそう

  2. [TypeScript] BoltでSlack Appを作成する ②GAEにGitHub Actionsでデプロイする

  3. [TypeScript] BoltでSlack Appを作成する ③GitHub APIを叩いてGAEにデプロイする

単語紹介

Firestore

Cloud Firestore は、Firebase と Google Cloud からのモバイル、ウェブ、サーバー開発に対応した、柔軟でスケーラブルなデータベースです。Firebase Realtime Database と同様に、リアルタイム リスナーを介してクライアント アプリ間でデータを同期し、モバイルとウェブのオフライン サポートを提供します。これにより、ネットワーク レイテンシやインターネット接続に関係なく機能するレスポンシブ アプリを構築できます。

https://firebase.google.com/docs/firestore

Notion

Notionは全てをAll-in-oneで管理し、かつユーザーそれぞれが柔軟にカスタマイズできるツール

https://www.notion.so/Notion-c14d4f434be24c98a2815e66bd98ddf2

まずはNotionのAPIを触れるようにする

https://www.notion.so/my-integrations

まずはこちらから、integrationを作成しましょう。

公式ドキュメントを参考に、作成してみてください。

その後、Tokenを発行して、.envに格納しましょう。

.env

NOTION_API_TOKEN=secret_*************

Firestoreを入れて、slack IDとNotionのIDを連携させる

NotionのIDを取得する

まずは、slackから呼び出したい対象のDBをwebで開きます。

1.png

この部分の?より前の文字列eae53f7e3… がDBのIDになります。

Firestoreをimportする

次にFirestoreを導入します。

npm install firebase-admin

ただ、入れるだけではローカルで使えないので、ローカルで使える設定をしましょう。

サービス アカウント – IAM と管理 – Google Cloud Platformからプロジェクトを指定して、Firebase Admin SDKの「操作」から鍵を作成しダウンロードしておきます。

今回はそれをserviceAccountKey.jsonという名前でプロジェクトルートに置いておくことにしましょう。

serviceAccountKey.json には、databaseURLが入っていない可能性があります。

(僕の場合は入っていませんでした。)

入っていない場合、以下のように追加しておきましょう。

serviceAccountKey.json
{
  "type": "service_account",
  "project_id": "sample",
  "private_key_id": "66…",
  "private_key": "-----BEGIN PRIVATE KEY-----\nMI…",
  "client_email": "sample@appspot.gserviceaccount.com",
  "client_id": "11…",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadat…",
  "databaseURL": "https://sample.firebaseio.com" // ここです
}

.gitignore に追加するのを忘れないようにしましょう。

FirestoreにIDのデータを入れる

GCP内のコンソールで、情報を以下のように入れます。

/
├── users
|   ├ *****(UID)
|   | ├ name
|   | ├ notion_id
|   | └ slack_id
|   └ *****(UID)
└── notion
    ├ *****(UID)
    | ├ title(ex: Tasks)
    | └ id
    └ *****(UID)

Firestoreのデータを呼び出す実装をする

次に、Firestoreのinitializersをアプリ内に追加しましょう。

src/initializers/firestore.ts
import * as admin from 'firebase-admin'

if (process.env.NODE_ENV === `production`) {
  admin.initializeApp({
    credential: admin.credential.applicationDefault(),
  })
} else {
  const serviceAccount = require(`../../serviceAccountKey.json`)
  admin.initializeApp({
    credential: admin.credential.cert(serviceAccount),
  })
}
export const firestore = admin.firestore()

次に、関数を作成しましょう。

  • userのslackIDをNotionIDに変換する
  • userのNotionIDをslackIDに変換する

関数をconvertIds として作成します。

src/functions/convertIds.ts
import { firestore } from '../initializers/firestore'

const notionIdToSlackId = async (id: string): Promise<string> => {
  const targetUserRef = await firestore.collection('users').where('notion_id', '==', id).get()
  const targetUser = await targetUserRef.docs.map(doc => {
    return doc.data()
  })[0]
  return targetUser.slack_id
}

const slackIdToNotionId = async (id: string): Promise<string> => {
  const targetUserRef = await firestore.collection('users').where('slack_id', '==', id).get()
  const targetUser = await targetUserRef.docs.map(doc => {
    return doc.data()
  })[0]
  return targetUser.notion_id
}

export { notionIdToSlackId, slackIdToNotionId }

次に、作成したNotionのDBのIDをfirestoreに入れたところから呼び出す関数をfetchNotionDbId として作成します。

src/functions/fetchNotionDbId.ts
import { firestore } from '../initializers/firestore'

export default async (title: string): Promise<string> => {
  const targetNotionDbRef = await firestore.collection('notion').where('title', '==', title).get()
  const targetNotionDb = await targetNotionDbRef.docs.map(doc => {
    return doc.data()
  })[0]
  return targetNotionDb.id
}

次に、Notion内のDB、Tasksを呼び出す関数、fetchTasks を作成します。

src/functions/fetchTasks.ts
import axios from 'axios'
import formatTaskResult from './formatTaskResult'
import fetchNotionDbId from './fetchNotionDbId'

interface Result { // ここはNotionに設定している値を元に作成します
  url: string
  title: string
  startsAt?: string
  endsAt?: string
  assignee?: string
  status?: string
  [key: string]: any
}

export default async (bodyParameters: object): Promise<Result[]> => {
  const tasksDbId = await fetchNotionDbId('Tasks')
  const headers = {
    headers: { 
      Authorization: `Bearer ${process.env.NOTION_API_TOKEN}`,
      'Notion-Version': '2021-08-16'
    }
  }

  try {
    const results = await axios.post(`https://api.notion.com/v1/databases/${tasksDbId}/query`, bodyParameters, headers)
    const array: Result[] = []
    for (const result of results.data['results']) {
      array.push(await formatTaskResult(result))
    }
    return array
  } catch (error) {
    return []
  }
}

formatTaskResult がまだありませんよね、こんな感じで作成します。

(ここは作成しているNotionの情報とリンクさせる必要があります、適宜読み替えてください。)

src/functions/formatTaskResult.ts
import { notionIdToSlackId } from './convertIds'

interface Result {
  url: string
  title: string
  startsAt?: string
  endsAt?: string
  assignee?: string
  status?: string
  [key: string]: any
}

export default async (result: JSON): Promise<Result> => {
  const regex = /[¥-]/g
  const url = `https://www.notion.so/${result['id'].replace(regex, '')}`
  const title = result['properties']['Name']['title'][0]['plain_text']
  const startsAt = result['properties']['Starts at']['date'] != null ? result['properties']['Starts at']['date']['start'] : '未記入'
  const endsAt = result['properties']['Ends at']['date'] != null ? result['properties']['Ends at']['date']['start'] : '未記入'
  const assignee = await getNames(result['properties']['Asignee']['people'])
  const status = result['properties']['status']['select'] != null ? result['properties']['status']['select']['name'] : '未設定'

  return {
    url: url,
    title: title,
    startsAt: startsAt,
    endsAt: endsAt,
    assignee: assignee,
    status: status,
  }
}

const getNames = async (people: JSON[]): Promise<string> => {
  let mentions: string = ''

  for (const person of people) {
    const slackId = await notionIdToSlackId(person['id'])
    mentions = `${mentions} <@${slackId}>`
  }
  return mentions
}

assigneeの部分では、ユーザーのIDをNotionIDからslackIDに変換をしています。

ついでに、slackのメンションが飛ぶように<@{slackId}>としていますね。

そして、最後にslackアプリ内でこれらの関数を呼び出して返却するようにしましょう。

src/commands/getAllTasks.ts
import { app } from '../initializers/bolt'
import fetchTasks from '../functions/fetchTasks'

export default (): void => {
  app.message(`sample get all tasks`, async ({ _message, say }): Promise<void> => {
    const taskTitles = await formattedText()
    await say(taskTitles)
  })
}

const formattedText = async (): Promise<string> => {
  let text: string = ''
  const bodyParameters = {
    filter: {
      property: 'status',
      select: {
        does_not_equal: 'DONE'
      }
    }
  }
  try {
    const results = await fetchTasks(bodyParameters)
    results.forEach((result) => {
      text = `${text}\nassignee: ${result.assignee}, startsAt: ${result.startsAt}, endsAt: ${result.startsAt}, status: ${result.status}, <${result.url}|${result.title}>`
    })
    return text
  } catch (error) {
    return 'something happened'
  }
}

これで、TaskのうちstatusがDONE以外のものをまとめて呼び出せます!

( irori get all taskssample get all tasksとして呼び出してみてくださいね)

2.png

また、自分がAsigneeに入っているもののみを呼び出すsample get my tasksも作成します。

import { app } from '../initializers/bolt'
import fetchTasks from '../functions/fetchTasks'
import { slackIdToNotionId } from '../functions/convertIds'

export default (): void => {
  app.message(`sample get my tasks`, async ({ message, say }): Promise<void> => {
    const userNotionId = await slackIdToNotionId(message.user)
    const taskTitles = await formattedText(userNotionId)
    
    await say(taskTitles)
  })
}

const formattedText = async (userNotionId: string): Promise<string> => {
  let text: string = ''
  const bodyParameters = {
    filter: {
      and: [
        {
          property: 'status',
          select: {
            does_not_equal: 'DONE'
          }
        },
        {
          property: 'Asignee',
          people: {
            contains: userNotionId
          }
        },
      ]
    }
  }
  try {
    const results = await fetchTasks(bodyParameters)
    results.forEach((result) => {
      text = `${text}\nassignee: ${result.assignee}, startsAt: ${result.startsAt}, endsAt: ${result.startsAt}, status: ${result.status}, <${result.url}|${result.title}>`
    })
    return text
  } catch (error) {
    return 'something happened'
  }
}

このように、bodyParametersを変えるだけで簡単に呼び出せますね。

次回は、slack内のテキストをNotionに保存させる機能を作成しようと思います!

お楽しみに〜😎

Related Posts

[TypeScript] BoltでSlack Appを作成する ③GitHub APIを叩いてGAEにデプロイする

[TypeScript] BoltでSlack Appを作成する ③GitHub APIを叩いてGAEにデプロイする

[TypeScript] BoltでSlack Appを作成する ⑤Slack上でスタンプを押すとNotionに保存する

[TypeScript] BoltでSlack Appを作成する ⑤Slack上でスタンプを押すとNotionに保存する

[TypeScript] BoltでSlack Appを作成する ⑥slackの投稿にスタンプで返信するとTwitterに投稿する

[TypeScript] BoltでSlack Appを作成する ⑥slackの投稿にスタンプで返信するとTwitterに投稿する