> ## Documentation Index
> Fetch the complete documentation index at: https://basecamp.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Get Profile Image

> プロフィール画像を取得してみよう

## 始める前に

今回は、プロフィール画像の取得方法として

* [Airstack API](#1-airstack-api) を利用する方法（難易度<sup>\*1</sup>：★★★☆☆）
* [Frames Context](#2-frames-context) を利用する方法（難易度<sup>\*1</sup>：★☆☆☆☆）

をそれぞれ説明します。

※１）プログラミング初心者における難易度のレベル

## 1. Airstack API を利用する方法

この方法では、**Lesson 2** のソースコードを元に必要な処理を追加していきますので、まずは **Lesson 2** のソースコードに **修正後のコード** の内容に従い修正を加え、**Airstack API** から取得したデータを利用できる状態にします。

### 修正前のコード

```typescript /app/frames/reveal/route.tsx theme={null}
import { Button } from "frames.js/next";
import { frames } from "app/frames/frames";
import { appURL } from "app/utils";

const handleRequest = frames(async (ctx) => {

  let error: string | null = null;
  let isLoading = false;

  const fetchUserData = async (fid: string) => {
    isLoading = true;
    try {
      const airstackUrl = `${appURL()}/api?userId=${encodeURIComponent(fid)}`;
      const airstackResponse = await fetch(airstackUrl);
      if (!airstackResponse.ok) {
        throw new Error(`Airstack HTTP error! status: ${airstackResponse.status}`);
      }
    } catch (err) {
      console.error("Error fetching data:", err);
      error = (err as Error).message;
    } finally {
      isLoading = false;
    }
  };

  let fid: string | null = null;
  if (ctx.message?.requesterFid) {
    fid = ctx.message.requesterFid.toString();
    console.log("Using requester FID:", fid);
  } else {
    console.log("No ctx.url available");
  }
  console.log("Final FID used:", fid);

  if (fid) {
    await Promise.all([fetchUserData(fid)]);
  }

  return {
    image: `${appURL()}/02.png`,
    buttons: [
      <Button action="post" target="/">
        最初のページへ
      </Button>,
    ],
  };

});

export const GET = handleRequest;
export const POST = handleRequest;
```

### 修正後のコード

```typescript /app/frames/reveal/route.tsx {6-12,24-33,51-54} theme={null}
import { Button } from "frames.js/next";
import { frames } from "app/frames/frames";
import { appURL } from "app/utils";

const handleRequest = frames(async (ctx) => {

  interface UserData {
    fid: string;
    profileImageUrl: string;
  }

  let userData: UserData | null = null;
  let error: string | null = null;
  let isLoading = false;

  const fetchUserData = async (fid: string) => {
    isLoading = true;
    try {
      const airstackUrl = `${appURL()}/api?userId=${encodeURIComponent(fid)}`;
      const airstackResponse = await fetch(airstackUrl);
      if (!airstackResponse.ok) {
        throw new Error(`Airstack HTTP error! status: ${airstackResponse.status}`);
      }
      const airstackData = await airstackResponse.json();
      if (airstackData.userData.Socials.Social && airstackData.userData.Socials.Social.length > 0) {
        const social = airstackData.userData.Socials.Social[0];
        userData = {
          fid: social.userId || "",
          profileImageUrl: social.profileImage || "",
        };
      } else {
        throw new Error("No user data found");
      }
    } catch (err) {
      console.error("Error fetching data:", err);
      error = (err as Error).message;
    } finally {
      isLoading = false;
    }
  };

  let fid: string | null = null;
  if (ctx.message?.requesterFid) {
    fid = ctx.message.requesterFid.toString();
    console.log("Using requester FID:", fid);
  } else {
    console.log("No ctx.url available");
  }
  console.log("Final FID used:", fid);

  const shouldFetchData = fid && (!userData || (userData as UserData).fid !== fid);
  if (shouldFetchData && fid) {
    await Promise.all([fetchUserData(fid)]);
  }

  return {
    image: `${appURL()}/02.png`,
    buttons: [
      <Button action="post" target="/">
        Reset
      </Button>,
    ],
  };
});

export const GET = handleRequest;
export const POST = handleRequest;
```

次に、変数 `profileImageUrl` に対して、**Airstack API** からデータを取得できた場合は `userData.profileImageUrl` の値を、**Airstack API** からデータを取得できなかったは `${appURL()}/unavailable.png` を値として定義し、`image:` の値として設定します。

```typescript /app/frames/reveal/route.tsx {56-59} theme={null}
import { Button } from "frames.js/next";
import { frames } from "app/frames/frames";
import { appURL } from "app/utils";

const handleRequest = frames(async (ctx) => {

  interface UserData {
    fid: string;
    profileImageUrl: string;
  }

  let userData: UserData | null = null;
  let error: string | null = null;
  let isLoading = false;

  const fetchUserData = async (fid: string) => {
    isLoading = true;
    try {
      const airstackUrl = `${appURL()}/api?userId=${encodeURIComponent(fid)}`;
      const airstackResponse = await fetch(airstackUrl);
      if (!airstackResponse.ok) {
        throw new Error(`Airstack HTTP error! status: ${airstackResponse.status}`);
      }
      const airstackData = await airstackResponse.json();
      if (airstackData.userData.Socials.Social && airstackData.userData.Socials.Social.length > 0) {
        const social = airstackData.userData.Socials.Social[0];
        userData = {
          fid: social.userId || "",
          profileImageUrl: social.profileImage || "",
        };
      } else {
        throw new Error("No user data found");
      }
    } catch (err) {
      console.error("Error fetching data:", err);
      error = (err as Error).message;
    } finally {
      isLoading = false;
    }
  };

  let fid: string | null = null;
  if (ctx.message?.requesterFid) {
    fid = ctx.message.requesterFid.toString();
    console.log("Using requester FID:", fid);
  } else {
    console.log("No ctx.url available");
  }
  console.log("Final FID used:", fid);

  const shouldFetchData = fid && (!userData || (userData as UserData).fid !== fid);
  if (shouldFetchData && fid) {
    await Promise.all([fetchUserData(fid)]);
  }

  const profileImageUrl = userData ? (userData as UserData).profileImageUrl : `${appURL()}/unavailable.png`;

  return {
    image: profileImageUrl,
    buttons: [
      <Button action="post" target="/">
        Reset
      </Button>,
    ],
  };
});

export const GET = handleRequest;
export const POST = handleRequest;
```

最後に、今回はレッスンはプロフィール画像を使ったサンプルのため、フレームの画像の比率を **1:1** に設定します。

```typescript /app/frames/reveal/route.tsx {60-62} theme={null}
import { Button } from "frames.js/next";
import { frames } from "app/frames/frames";
import { appURL } from "app/utils";

const handleRequest = frames(async (ctx) => {

  interface UserData {
    fid: string;
    profileImageUrl: string;
  }

  let userData: UserData | null = null;
  let error: string | null = null;
  let isLoading = false;

  const fetchUserData = async (fid: string) => {
    isLoading = true;
    try {
      const airstackUrl = `${appURL()}/api?userId=${encodeURIComponent(fid)}`;
      const airstackResponse = await fetch(airstackUrl);
      if (!airstackResponse.ok) {
        throw new Error(`Airstack HTTP error! status: ${airstackResponse.status}`);
      }
      const airstackData = await airstackResponse.json();
      if (airstackData.userData.Socials.Social && airstackData.userData.Socials.Social.length > 0) {
        const social = airstackData.userData.Socials.Social[0];
        userData = {
          fid: social.userId || "",
          profileImageUrl: social.profileImage || "",
        };
      } else {
        throw new Error("No user data found");
      }
    } catch (err) {
      console.error("Error fetching data:", err);
      error = (err as Error).message;
    } finally {
      isLoading = false;
    }
  };

  let fid: string | null = null;
  if (ctx.message?.requesterFid) {
    fid = ctx.message.requesterFid.toString();
    console.log("Using requester FID:", fid);
  } else {
    console.log("No ctx.url available");
  }
  console.log("Final FID used:", fid);

  const shouldFetchData = fid && (!userData || (userData as UserData).fid !== fid);
  if (shouldFetchData && fid) {
    await Promise.all([fetchUserData(fid)]);
  }

  const profileImageUrl = userData ? (userData as UserData).profileImageUrl : `${appURL()}/unavailable.png`;

  return {
    image: profileImageUrl,
    imageOptions: {
      aspectRatio: "1:1",
    },
    buttons: [
      <Button action="post" target="/">
        Reset
      </Button>,
    ],
  };
});

export const GET = handleRequest;
export const POST = handleRequest;
```

問題なくプロフール画像が取得できていれば次のページにボタンをクリックしたユーザーのプロフィール画像が表示されるはずです。

## 2. Frames Context を利用する方法

<Warning>
  この方法は **Airstack API** のデータを用いないため API キーの必要がなく、比較的簡単に実装できますが、**Airstack API** からデータを取得する処理を残しておかないと **Airstack API** への接続が発生せず、フレームの製作者を識別できないため、**Moxie** の報酬対象外となりますのでご注意ください。
</Warning>

この方法では、**Airstack API** に関する処理やファイルが必要ないため、**Lesson 1** のソースコードを元に必要な処理を追加していきます。

まずは、**Frames Context** に含まれるデータがどのような内容か、どのデータを取得する必要があるのか確認するため、`ctx` の内容をコンソールログに出力します。

```typescript /app/frames/reveal/route.tsx {7} theme={null}
import { Button } from "frames.js/next";
import { frames } from "app/frames/frames";
import { appURL } from "app/utils";

const handleRequest = frames(async (ctx) => {

  console.log("Context:", ctx);

  return {
    image: `${appURL()}/02.png`,
    buttons: [
      <Button action="post" target="/">
        Reset
      </Button>,
    ],
  };
});

export const GET = handleRequest;
export const POST = handleRequest;
```

修正が終わったら `npm run dev` でローカルサーバーを起動し、フレームのボタンをクリックしてコンソールログに表示されるデータを確認します。

<Accordion title="コンソールのログ">
  おそらく以下のようなログが表示されるはずです。

  ```bash theme={null}
  Context: {
    basePath: '/frames',
    initialState: undefined,
    request: Request {
      method: 'POST',
      url: 'http://localhost:3000/frames/reveal?__bi=1-p',
      headers: Headers {
        host: 'localhost:3000',
        connection: 'keep-alive',
        'content-type': 'application/json',
        accept: '*/*',
        'accept-language': '*',
        'sec-fetch-mode': 'cors',
        'user-agent': 'node',
        'accept-encoding': 'gzip, deflate',
        'content-length': '802',
        'x-forwarded-host': 'localhost:3000',
        'x-forwarded-port': '3000',
        'x-forwarded-proto': 'http',
        'x-forwarded-for': '::1'
      },
      destination: '',
      referrer: 'about:client',
      referrerPolicy: '',
      mode: 'cors',
      credentials: 'same-origin',
      cache: 'default',
      redirect: 'follow',
      integrity: '',
      keepalive: false,
      isReloadNavigation: false,
      isHistoryNavigation: false,
      signal: AbortSignal { aborted: false }
    },
    url: URL {
      href: 'http://localhost:3000/frames/reveal?__bi=1-p',
      origin: 'http://localhost:3000',
      protocol: 'http:',
      username: '',
      password: '',
      host: 'localhost:3000',
      hostname: 'localhost',
      port: '3000',
      pathname: '/frames/reveal',
      search: '?__bi=1-p',
      searchParams: URLSearchParams { '__bi' => '1-p' },
      hash: ''
    },
    baseUrl: URL {
      href: 'http://localhost:3000/frames',
      origin: 'http://localhost:3000',
      protocol: 'http:',
      username: '',
      password: '',
      host: 'localhost:3000',
      hostname: 'localhost',
      port: '3000',
      pathname: '/frames',
      search: '',
      searchParams: URLSearchParams {},
      hash: ''
    },
    stateSigningSecret: undefined,
    walletAddress: [AsyncFunction: walletAddress],
    debug: false,
    __debugInfo: {},
    pressedButton: { action: 'post', index: 1 },
    searchParams: { __bi: '1-p' },
    message: {
      url: 'http://localhost:3000/',
      buttonIndex: 1,
      castId: { fid: 1, hash: '0x0000000000000000000000000000000000000000' },
      inputText: '',
      requesterFid: 291942,
      state: '',
      connectedAddress: undefined,
      address: undefined,
      transactionId: undefined,
      isValid: true,
      casterFollowsRequester: false,
      requesterFollowsCaster: false,
      likedCast: false,
      recastedCast: false,
      requesterVerifiedAddresses: [],
      requesterCustodyAddress: '',
      requesterUserData: {
        profileImage: 'https://i.seadn.io/gae/N1SdZkJsgf3erECLe24BL6BKIvvlAsqlUhG65nT0GqHlyjAa-IRgiYzLHUkf7mtad_ZZKO_QrqBUq6IMf_Rku-zTrlaB2PjC6WA?w=500&auto=format',
        displayName: 'FREAK',
        username: 'djfreak.eth',
        bio: 'DJ, Graphic Designer, Crypto Investor, Art Collector.\n' +
          'Host: /dance-music\n' +
          'Council Member: /sonata\n' +
          'Moderator: /conbini\n' +
          'Spotify Playlist: https://linktr.ee/djfreak',
        location: ''
      },
      walletAddress: [Function: walletAddress]
    },
    clientProtocol: { id: 'farcaster', version: 'vNext' },
    state: undefined
  }
  ```
</Accordion>

次に、**Frames Context** のデータの中に必要なデータがあるか確認をします。

以下はデータの構造をより分かりやすくするために不必要なデータを削除し、必要なデータをハイライトしたものです。

```bash {3,10-11} theme={null}
Context: {
  basePath: '/frames',
  message: {
    url: 'http://localhost:3000/',
    buttonIndex: 1,
    castId: { fid: 1, hash: '0x0000000000000000000000000000000000000000' },
    inputText: '',
    requesterFid: 291942,
    ========== 省略 ==========
    requesterUserData: {
      profileImage: 'https://i.seadn.io/gae/N1SdZkJsgf3erECLe24BL6BKIvvlAsqlUhG65nT0GqHlyjAa-IRgiYzLHUkf7mtad_ZZKO_QrqBUq6IMf_Rku-zTrlaB2PjC6WA?w=500&auto=format',
      displayName: 'FREAK',
      username: 'djfreak.eth',
      bio: 'DJ, Graphic Designer, Crypto Investor, Art Collector.\n' +
        'Host: /dance-music\n' +
        'Council Member: /sonata\n' +
        'Moderator: /conbini\n' +
        'Spotify Playlist: https://linktr.ee/djfreak',
      location: ''
    }
  }
}
```

今回のケースでは `message > requesterUserData > profileImage` の内容が必要なはずなので、画像のURLとして使えるように `profileImage` の値を変数 `profileImageUrl` に代入します。

```typescript /app/frames/reveal/route.tsx {9} theme={null}
import { Button } from "frames.js/next";
import { frames } from "app/frames/frames";
import { appURL } from "app/utils";

const handleRequest = frames(async (ctx) => {

  console.log("Context:", ctx);

  const profileImageUrl = ctx.message?.requesterUserData?.profileImage || `${appURL()}/unavailable.png`;

  return {
    image: `${appURL()}/02.png`,
    buttons: [
      <Button action="post" target="/">
        Reset
      </Button>,
    ],
  };
});

export const GET = handleRequest;
export const POST = handleRequest;
```

あとは変数 `profileImageUrl` を `image:` の値として設定します。

```typescript /app/frames/reveal/route.tsx {12} theme={null}
import { Button } from "frames.js/next";
import { frames } from "app/frames/frames";
import { appURL } from "app/utils";

const handleRequest = frames(async (ctx) => {

  console.log("Context:", ctx);

  const profileImageUrl = ctx.message?.requesterUserData?.profileImage || `${appURL()}/unavailable.png`;

  return {
    image: profileImageUrl,
    buttons: [
      <Button action="post" target="/">
        Reset
      </Button>,
    ],
  };
});

export const GET = handleRequest;
export const POST = handleRequest;
```

最後に、この方法の場合もフレームの画像の比率を **1:1** に設定します。

```typescript /app/frames/reveal/route.tsx {13-15} theme={null}
import { Button } from "frames.js/next";
import { frames } from "app/frames/frames";
import { appURL } from "app/utils";

const handleRequest = frames(async (ctx) => {

  console.log("Context:", ctx);

  const profileImageUrl = ctx.message?.requesterUserData?.profileImage || `${appURL()}/unavailable.png`;

  return {
    image: profileImageUrl,
    imageOptions: {
      aspectRatio: "1:1",
    },
    buttons: [
      <Button action="post" target="/">
        Reset
      </Button>,
    ],
  };
});

export const GET = handleRequest;
export const POST = handleRequest;
```

問題なくプロフール画像が取得できていれば次のページにボタンをクリックしたユーザーのプロフィール画像が表示されるはずです。
