Slackで自分にきたメンションを、自動でタスク化してNotionに登録するアプリの作成した話

## はじめに

忙しいor面倒臭すぎて自分に振られた作業を、手動でタスク化出来ない。
Notionとかで管理したいけど、面倒すぎる。
そうだ自動化しよう。

---

## まずは完成した時の画像をどうぞ
以下は、実際にアプリが動作したときのスクリーンショットです。

### **1. Slackでの依頼メンション**
Slackで自分宛に送信された依頼メッセージ

![Slackでの依頼メンション](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3527006/665a489e-28b3-18a6-6141-28c1f51d5a24.png)


### **2. Notionに登録されたタスク**
上記メンションが解析され、Notionにタスクとして登録

![Notionに登録されたタスク](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3527006/3743839e-c3b6-21c5-cf30-6a9b9880ff06.png)



# 以下作業手順

## 実現したいこと
1. **Slackメンションをトリガー**:
   - 自分宛のメンション（特定のキーワードを含む）を受信。
   - キーワード例: "対応", "お願い", "緊急", "依頼"。
2. **Gemini-Proでタスク解析**:
   - メンション内容を解析し、タスク名・説明・期限日を抽出。
3. **Notionにタスクを登録**:
   - タスクデータをNotionデータベースに登録。

---

## 使用技術
- **TypeScript**: プロジェクト全体で使用。
- **Next.js**: サーバーレスAPIを構築。
- **Google Gemini-Pro API**: 自然言語処理でメンション内容を解析（`gemini-1.5-flash`モデル）。
- **Notion API**: タスクをデータベースに登録。
- **Slack API**: メンションイベントを取得。

---

## 環境構築

### 必要なもの
- Slackワークスペース
- Notionアカウント
- Google Gemini-Pro APIキー

---

## コード解説

### **1. Slackイベント処理（`events.ts`）**
Slackメンションを受け取り、タスクを作成します。

```typescript
import { NextApiRequest, NextApiResponse } from "next";
import { analyzeTask } from "../../../utils/gemini";
import { addTaskToNotion } from "../../../utils/notion";

const processedMentions: Set<string> = new Set();
const taskKeywords = process.env.TASK_KEYWORDS?.split(",") || [];
const targetUserId = process.env.TARGET_USER_ID || "";

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  if (req.body.type === "url_verification") {
    return res.status(200).json({ challenge: req.body.challenge });
  }

  try {
    const event = req.body.event;
    const mentionId = event.client_msg_id || event.event_ts;

    if (!mentionId || processedMentions.has(mentionId)) return res.status(200);

    if (!event.text.includes(`<@${targetUserId}>`) || 
        !taskKeywords.some((keyword) => event.text.includes(keyword))) {
      return res.status(200).send("No task-related keywords found.");
    }

    const { taskName, dueDate, remarks } = await analyzeTask(event.text);

    const properties = {
      タスク名: { title: [{ text: { content: taskName } }] },
      備考: { rich_text: [{ text: { content: remarks || "" } }] },
      期限日: { date: { start: dueDate || "" } },
    };

    await addTaskToNotion(properties);
    processedMentions.add(mentionId);

    res.status(200).send("タスクが正常に登録されました。");
  } catch (error) {
    res.status(500).send("内部サーバーエラー");
  }
}
```

---

### **2. `gemini-1.5-flash`でタスク解析（`gemini.ts`）**
Google gemini-1.5-flashを使用してメンション内容を解析し、タスクデータを生成します。

```typescript
import { GoogleGenerativeAI } from "@google/generative-ai";
import { generatePrompt } from "./prompt";

const genAI = new GoogleGenerativeAI(process.env.GEMINI_API_KEY!);

export async function analyzeTask(input: string) {
  const model = genAI.getGenerativeModel({ model: "gemini-1.5-flash" });
  const prompt = generatePrompt(input);
  const result = await model.generateContent(prompt);

  let taskData = JSON.parse(result.response.text().replace(/`/g, ""));
  taskData.dueDate = taskData.期限日?.replace(/(\d{4})\/(\d{2})\/(\d{2})/, "$1-$2-$3");
  
    return {
      taskName: taskData["タスク名"] || "未定義タスク", // タスク名がundefinedならデフォルト値を設定
      dueDate: taskDueDate || "2025-01-01", // 期限日がnullならデフォルト日付を設定
      remarks: taskData["備考"] || "説明なし", // 備考がundefinedならデフォルト値を設定
    };
}
```

---

### **3. Notion API連携（`notion.ts`）**
タスクデータをNotionデータベースに登録します。

```typescript
import { Client } from "@notionhq/client";

const notion = new Client({ auth: process.env.NOTION_TOKEN! });

export async function addTaskToNotion(properties) {
  return await notion.pages.create({
    parent: { database_id: process.env.NOTION_DATABASE_ID! },
    properties,
  });
}
```

---

### **4. プロンプト生成（`prompt.ts`）**
Gemini-Pro用のプロンプトを生成します。

```typescript
export function generatePrompt(text: string): string {
  return `
      以下のSlackメンションを解析し、指定されたNotionデータベースフォーマットに合うタスクを生成してください。
      
      Slackメンション:
      "${text}"
      
      Notionデータベースのフォーマット:
      - タスク名: タスクの主題
      - 備考: 詳細説明
      - 期限日: YYYY-MM-DD形式
      
      出力例:
      {
        "タスク名": "新しいプロジェクトの企画書を作成する",
        "備考": "プロジェクトの詳細はSlackスレッドを参照",
        "期限日": "2025-01-30"
      }
      
      解析対象:
      - メンションに期限が含まれていない場合、"期限日"は空欄にしてください。
      - 備考が指定されていない場合は空欄にしてください。
      - 必要に応じて備考を補足してください。
    `;
}

```

---

## 難しかった部分
1. **Slackメンションの無限ループ**:
   - 同じメンションが何度も処理されてしまい、Notionにタスクが無限に登録される問題が発生。
   - **解決策**: Slackイベントの`client_msg_id`や`event_ts`を利用して処理済みのメンションを`Set`で管理。

2. **JSONのパースエラー**:
   - Geminiのレスポンスに含まれる余分なバッククォートやフォーマットミスでエラーが発生。
   - **解決策**: レスポンスの前後の整形処理を追加。

---

## 工夫した部分
- **イベントIDで重複処理を防止**:
  - Slackイベントごとに一意のIDを使い、処理済みかを追跡。
- **キーワードフィルタリング**:
  - 特定のキーワード（例: "対応", "お願い", "緊急", "依頼"）を使い、タスク化対象を絞り込む。
- **柔軟なJSONパース処理**:
  - Geminiのレスポンスフォーマットに合わせて柔軟にデータを整形するロジックを実装。

---

## 参考にした記事

- [Notion API を使用してデータベースを操作する - Zenn](https://zenn.dev/kou_pg_0131/articles/notion-api-usage)

- [Notion APIでデータベースを操作する - Qiita](https://qiita.com/thomi40/items/fe2a828746f31ad827ba)

- [Slack App作成からEvent APIまでの流れ - Qiita](https://qiita.com/fuku_pg/items/f5ae2a7437135fcb258c)

- [Slack APIを利用してカスタム通知を実装する - Zenn](https://qiita.com/to3izo/items/c2d16f8b3e52b09e543e)