📟

【検証】LINE に送った画像を Supabase Storage に保存する

2024/03/23に公開

この資料について

【LINE×Supabase】あなただけの単語カードアプリを作ろう! の資料を元に作成しています。実際に動かす場合は、 step2 まで完了していることを想定しています。
今回のハンズオンでの変更の差分はこちらになります。

利用技術

Supabase について

Supabase はオープンソースの Firebase 代替プラットフォームです。主な特徴は以下の通りです。

  • Postgres をバックエンドとしたリアルタイムデータベースを提供
  • ストレージや認証、リアルタイム機能などを1つのプラットフォームで利用できる
  • Firebase と同様のインターフェイスで開発しやすい
  • データの暗号化・アクセス制御・監査ログなどセキュリティ機能が充実
  • 無料プランが用意されており、開発者が気軽に利用できる
  • GitHubでオープンソース開発が進められており、コミュニティ主導
  • ダッシュボードでインフラのステータスやメトリクスが透明性高く公開

つまり、Firebase のような機能をオープンソースで実現し、しかもデータセキュリティや透明性に優れたプラットフォームということができます。Postgresとの親和性が高く、アプリのバックエンドを素早く構築できるサービスです。

Bot について

Bot は次のように動きます

ユーザー動作 Bot アクション
画像を送信した時 画像が Supabase Storage に保存され、メッセージが表示
文字を送信した時 保存された画像の情報が一覧でボタンメッセージで表示
ボタンを押した時 保存されている画像がメッセージに表示

実際の動作としては次のように動きます。

step2 まで完了させる

ローカルに step2 を clone する

git clone https://github.com/4geru/supabase-line-bot.git
cd supabase-line-bot
git checkout origin/complete-step2
supabase functions deploy line-bot --no-verify-jwt

step2 の手順が完了していると次のように、メッセージを送信すると返事をしてくれます。

Supabase Storage の設定

bucket の作成

「Storage」 > 「New bucket」から bucket を作成します。
フォームは、次のように設定します。アップロードした画像は外部からアクセスできるように、Public bucket「ON 」にしています。

Name of bucket:  photo
Public bucket: ON

Policies の作成

SupabaseのPolicyを利用することで、データベースのテーブルや行に対するアクセス権限を定義し、管理できます。

今回はハンズオンのため、全てのユーザーがアクセスできるように設定します。実際の運用では、Supabase Authを利用してユーザーのアクセスを制御することが推奨されますが、Supabase Storageに関する詳細は今回の記事では割愛し、別の機会に詳しく解説します。

次に、Policiesを作成する手順を説明します。

  1. 「Storage」 > 「Policies」 > 「New policy」を選択します。

  2. 「For full customization」を選択します。

  3. 画像のアップロードと閲覧を許可するため、「SELECT」「INSERT」を選択し、「Review」をクリックします。

  4. 作成されたポリシーのフォーマットを確認し、「Save policy」をクリックします。

画像を保存する

ファイルの作成

画像に関係するファイルの作成します。

touch supabase/functions/line-bot/image-functions.ts

saveImage 関数の実装

image-functions.ts の中にコードを追加していきます。まず、画像を保存する saveImage 関数を書きます。

supabase/functions/line-bot/image-functions.ts
export const saveImage = async (event, supabase) => {
  const filePath = `${event.source.userId}/photo/${event.message.id}.jpg` // 画像の保存先のpathを指定
  // リクエストヘッダー
  const headers = {
    "Content-Type": "application/json",
    "Authorization": "Bearer " + Deno.env.get('LINE_CHANNEL_ACCESS_TOKEN')
  }

  // https://developers.line.biz/ja/reference/messaging-api/#get-content
  const response = await fetch(`https://api-data.line.me/v2/bot/message/${event.message.id}/content`,
    {
        method: "GET",
        headers: headers,
    }
  );
  const blob = await response.blob();
  console.log(response)
  // https://supabase.com/docs/reference/javascript/storage-from-upload
  const { data, error } = await supabase
    .storage
    .from('photo')
    .upload(filePath, blob, {
      contentType: 'image/jpg'
    })
}

saveImage のコードの解説をします。

LINE の画像データを取得 API

LINE に送信された画像データは、LINE Messaging API のイベントオブジェクトには含まれません。そのため、content API を使用して取得する必要があります。content API を利用する際には、受け取った message_id を指定することで、画像、動画、音声などのメディアファイルを取得することができます。
※ LINE プラットフォームでは大容量データの送受信のために専用のドメイン名(api-data.line.me)を使用しています。そのため、API クライアントの定義時にはこの点に注意が必要です。

Supabase Storage API

Supabase Storage API の upload 関数 を使用すると、指定したパスにファイルを保存することができます。Blob 形式のデータも保存可能です。今回は、LINE content API から受け取ったデータをそのまま Supabase に保存します。
Storage の filePath には ${userId}/photo/${messageId} というパスを指定し、そこにファイルが保存されます。

index.ts の改修

LINE Bot が画像を受け取った際に saveImage 関数を呼び出すように変更します。
画像が送信された場合、イベントは Image Message として受け取られ、event.typemessageevent.message.typeimage となります。

supabase/functions/line-bot/index.ts
import { supabaseClient } from './supabaseClient.ts'
import { saveImage } from './image-functions.ts'
// ...
serve(async (req) => {
  const { events } = await req.json()
  console.log(events)
  if (events && events[0]?.type === "message" && events[0]?.message.type === "image") {
    await saveImage(events[0], supabaseClient(req))
    replyMessage(events, [{type: "text", text: "画像を保存しました"}])
  }

✅ 確認!!

デプロイを行い検証をします。

supabase functions deploy line-bot --no-verify-jwt

LINE Bot のから画像を送信します。
「Storage」 > 「photo」 > 「ユーザーID」 > 「photo」 > 「メッセージID.jpg」 を開くとアップロードされた画像を確認できます。

画像の情報を取得する

画像の一覧をボタン形式で表示します。
Supabase Storage API の中にある指定した path に含まれる画像一覧を取得する APIを利用し、登録順に並び替えた上で上位10件を選択します。
取得した画像情報を ボタンテンプレート としてユーザーに返信します。ボタンは最大4つまでで、ボタンのラベルは最大20文字までとなっているため、適宜調整が必要です。ボタンは postback アクション として設定し、画像のファイル名を JSON 形式で送信します。
※ LINE 公式ドキュメントではクエリストリングの形式を使用していますが、他の形式でも送信は可能です。

supabase/functions/line-bot/image-functions.ts
export const getImageButtonMessage = async (event, supabase) => {
  // https://supabase.com/docs/reference/javascript/storage-from-list
  const { data, error } = await supabase
    .storage
    .from('photo')
    .list(`${event.source.userId}/photo`, {
      limit: 5,
      offset: 0,
      sortBy: { column: 'created_at', order: 'desc' }
    })
  console.log({data, error})

  const images = data.filter(item =>
    // LINE に保存されている画像は jpg のみ
    item.name.match(/jpg/g)// ||
    // item.name.match(/jpeg/g) ||
    // item.name.match(/png/g)
  )
  return {
    "type": "template",
    "altText": "This is a buttons template",
    "template": {
      "type": "buttons",
      "text": "Please select image",
      "actions": images.slice(0, 4).map(item => ({
        "type": "postback",
        "label": item.name.slice(0, 20),
        "data": JSON.stringify({name: item.name})
      }))
    }
  }
}

メッセージを送信する

Supabase Storage には getPublicUrl が提供されており、指定した path の画像情報を取得できます。
LINE API に画像メッセージの形式を指定し、replyMessage に渡すために return します。
※ 画像メッセージは、画像フォーマット(JPEG or PNG), プロトコル https のみなど制約があるため、公式ドキュメント を参考してください。

supabase/functions/line-bot/image-functions.ts
export const getImageMessages = async (event, supabase) => {
  const postbackData = JSON.parse(event.postback.data)
  // https://supabase.com/docs/reference/javascript/storage-from-getpublicurl
  const { data } = supabase
    .storage
    .from('photo')
    .getPublicUrl(`${event.source.userId}/photo/${postbackData.name}`)
  return [
    {
      "type": "text",
      "text": JSON.stringify(data)
    },
    {
      "type": "image",
      "originalContentUrl": data.publicUrl,
      "previewImageUrl": data.publicUrl
    }
  ]
}

index.ts の更新

テキストメッセージのコードとボタンを押した時の postback イベントを受け取った時の処理を追加し、先ほど作成した getImageButtonMessage / getImageMessages の関数を呼び出します。

supabase/functions/line-bot/index.ts
import { saveImage, getImageButtonMessage, getImageMessages } from './image-functions.ts'

// ...
serve(async (req) => {
  const { events } = await req.json()
  console.log(events)
  // 画像メッセージを受信した時
  if (events && events[0]?.type === "message" && events[0]?.message.type === "image") {
    await saveImage(events[0], supabaseClient(req))
    replyMessage(events, [{type: "text", text: "画像を保存しました"}])
  }
  // テキストメッセージを受信した時
  if (events && events[0]?.type === "message" && events[0]?.message.type === "text") {
    const buttonTemplate = await getImageButtonMessage(events[0], supabaseClient(req))
    replyMessage(events, [buttonTemplate])
  }
  // postbackイベントを受信した時
  if (events && events[0]?.type === "postback") {
   const imageMessages = await getImageMessages(events[0], supabaseClient(req))
    replyMessage(events, imageMessages)
  }

✅ 確認!!

デプロイを行い検証をします。

supabase functions deploy line-bot --no-verify-jwt

メッセージを送信し、ボタンテンプレートを表示されるかを確認します。
ボタンを押すと先ほど登録した写真が表示されます。

Supabase の課金情報

Supabaseは無料で2つまでプロジェクトを作ることができます。無料版ではDBの容量やデータ転送量に制限があります。有料版だとそれらの制限が拡張できるイメージで、無料版と有料版に特に大きな機能差はない形になっています。有料版は月$25から始まり、8GBのDB容量、250GBのデータ転送量などが含まれています。

また、有料版に切り替えた場合でも突如大きな請求が来ないように課金額に上限を設けることができるので安心してサービス運用が可能です。

料金の詳細はこちらをご覧ください。

Discussion