0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

SlackメッセージをS3にmarkdown形式で保存するシステムを作った

Posted at

1. はじめに

私は日々のインプットのメモや雑記をSlackを使っています。しかし、多くのワークスペースに入っているとメモも散らばるし、何より後から見返しづらいです。

このプロジェクトでは、SlackのメッセージをMarkdown形式に整え、S3バケットに自動保存するシステムを構築しました。これによってSlackでのメモをひとまとめにしてS3に保存し、Obsidianなどのツールを使って見返すことができるようになります。

2. システム概要

これから説明するシステムは、以下のGitHubリポジトリのものです。お気軽に覗いてみてください!

2.1 アーキテクチャの全体像

このシステムは以下のコンポーネントで構成されています:

  1. Slack Bot: チャンネルのメッセージを監視し、イベントを検知します
  2. API Gateway: SlackからのWebhookを受け取り、Lambda関数を起動します
  3. Lambda関数: メッセージを処理し、Markdown形式に変換します
  4. S3バケット: 変換されたMarkdownファイルを保存します
  5. DynamoDB: 処理済みイベントを記録し、重複処理を防止します

処理フローとしては、Slackでメッセージが投稿されると、設定したSlack Botがイベントを検知し、API Gateway経由でLambda関数を呼び出します。Lambda関数はメッセージをMarkdown形式に変換し、S3バケットの指定パスに保存します。その後、ObsidianのS3プラグインなどを通じて、これらのファイルをローカルのObsidianボルトに同期することができます。

ChatGPT Image 2025年4月10日 02_31_55.png

2.2 技術スタック

このプロジェクトでは、以下の技術スタックを使用しています:

  • バックエンド言語: TypeScript
  • サーバーレス環境: AWS Lambda
  • API: AWS API Gateway
  • ストレージ: AWS S3
  • データベース: DynamoDB(重複排除用)
  • インフラ管理: Terraform
  • イベントソース: Slack API (Events API)

TypeScriptを使用することで、型安全性を確保しつつ、モダンなJavaScript機能を活用した開発が可能になりました。また、AWSのサーバーレスサービスを活用することで、インフラ管理の負担を最小限に抑えながら、拡張性の高いシステムを構築できました。

3. 技術の概要

このプロジェクトでは、以下の技術的なポイントに注目して実装を行いました:

  • サーバーレスアーキテクチャ: コスト効率とスケーラビリティの観点から、AWS Lambdaを採用しました。
  • TypeScript: 型安全性による開発効率の向上と実行時エラーの減少を図るため、TypeScriptを採用しました。
  • インフラのコード化: Terraformを使用してAWSリソースをコードとして管理することで、環境の再現性や変更履歴の追跡が容易になりました。これにより、デプロイプロセスの自動化とドキュメント化も実現しています。
  • Slack API連携: Events APIとWeb APIを組み合わせることで、メッセージイベントの取得とユーザー・チャンネル情報の取得を実現しました。セキュリティ面では署名検証を行い、不正なリクエストを排除しています。

これらの技術選定により、保守性が高く、拡張性のあるシステムを実現することができました。

4. 工夫した点・苦労した点

4.1 Slackイベント処理の最適化

Slackイベント処理における主な工夫点は以下の通りです:

  1. ボットメッセージのフィルタリング: ボット自身のメッセージによる無限ループを防止しました。
// ボットメッセージをスキップ(無限ループ防止)
if (messageEvent.bot_id || messageEvent.subtype === 'bot_message') {
  return {
    statusCode: 200,
    body: JSON.stringify({ success: true, message: 'Bot message ignored' })
  };
}
  1. エラーハンドリング: 様々なエラーケースに対応し、Slackに適切なレスポンスを返すようにしました。これにより、Slackのイベント再送信を防止し、システムの安定性を高めています。

  2. 必須フィールドの検証: イベントデータの検証を厳密に行い、不正なデータによる処理エラーを防止しています。

4.2 重複メッセージ防止の仕組み(DynamoDBによる冪等性確保)

Slackイベントは、ネットワークの問題などにより重複して送信されることがあります。そのため、DynamoDBを使用して、すでに処理したイベントを記録し、重複処理を防止する仕組みを実装しました。

export const checkAndMarkProcessed = async (eventId: string, ttlHours: number = 24): Promise<boolean> => {
  // TTL(有効期限)を計算
  const expiryTime = Math.floor(Date.now() / 1000) + (ttlHours * 60 * 60);
  
  try {
    // 条件付き書き込み - event_idが存在しない場合のみ書き込みを行う
    await dynamoDB.put({
      TableName: TABLE_NAME,
      Item: {
        event_id: eventId,
        expiry_time: expiryTime,
        processed_at: new Date().toISOString()
      },
      ConditionExpression: 'attribute_not_exists(event_id)'
    }).promise();
    
    return true; // 新規イベント
  } catch (error) {
    if (error instanceof Error && error.name === 'ConditionalCheckFailedException') {
      return false; // 既に処理済みのイベント
    }
    // その他のエラー時は安全側に倒して処理を続行
    return true;
  }
};

DynamoDBのTTL(Time to Live)機能を活用することで、古いレコードを自動的に削除し、データベースのサイズを管理しています。

4.3 S3バケットへの追記処理の実装

S3バケットへのファイル保存において、同じ日のメッセージを一つのファイルに追記していく実装を行いました:

export const saveToS3 = async (channelName: string, messageMarkdown: string, messageDate: Date): Promise<string> => {
  // S3のキー(ファイルパス)
  const s3Key = `40_slack_memo/${safeChannelName}/${dateString}.md`;
  
  try {
    // ファイルが既に存在するか確認
    let existingContent = '';
    try {
      const existingObject = await s3.getObject({
        Bucket: bucketName,
        Key: s3Key
      }).promise();
      
      existingContent = existingObject.Body?.toString('utf-8') || '';
    } catch (error) {
      // ファイルが存在しない場合は空文字列のまま
      if (error instanceof Error && error.name !== 'NoSuchKey') {
        throw error;
      }
    }
    
    // 新しいコンテンツを追加
    const updatedContent = existingContent + safeMessageMarkdown;
    
    // S3にファイルを保存
    await s3.putObject({
      Bucket: bucketName,
      Key: s3Key,
      Body: updatedContent,
      ContentType: 'text/markdown'
    }).promise();
    
    return s3Key;
  } catch (error) {
    throw error;
  }
};

この実装では、まず既存ファイルの内容を取得し、新しいメッセージを追加してから再度保存しています。ただし、この方法では同時書き込みによるデータの上書きが発生する可能性があるため、より堅牢な実装方法(S3のバージョニング機能の活用や、追記専用のAPIの使用など)も検討していく必要があります。

5. まとめと今後の展望

5.1 プロジェクトの成果

このプロジェクトにより、以下のような成果が得られました:

  1. 知識の自動キャプチャ: Slackでの会話内容を自動的にObsidianで使用可能な形式で保存できるようになりました
  2. サーバーレスアーキテクチャの活用: 管理コストを抑えつつ、スケーラブルなシステムを構築できました
  3. インフラのコード化: Terraformを使用してインフラをコード化し、再現性と保守性を高めました
  4. セキュリティの考慮: 署名検証や重複排除など、セキュリティと信頼性を考慮した実装を行いました

5.2 改善ポイントと将来の拡張可能性

今後の改善点や拡張可能性としては、以下のような項目が考えられます:

  1. Slackのリッチテキスト対応: メンション、リンク、コードブロックなどのSlackの書式をMarkdownに適切に変換する
  2. ファイル保存に対応させる: 画像やPDFなどをS3に保存できるようにする
  3. AIによる要約機能: 日々の会話を要約してダイジェストを作成する機能
0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?