始める前に
今回は、プロフィール画像の取得方法として- Airstack API を利用する方法(難易度*1:★★★☆☆)
- Frames Context を利用する方法(難易度*1:★☆☆☆☆)
1. Airstack API を利用する方法
この方法では、Lesson 2 のソースコードを元に必要な処理を追加していきますので、まずは Lesson 2 のソースコードに 修正後のコード の内容に従い修正を加え、Airstack API から取得したデータを利用できる状態にします。修正前のコード
/app/frames/reveal/route.tsx
Copy
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;
修正後のコード
/app/frames/reveal/route.tsx
Copy
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: の値として設定します。
/app/frames/reveal/route.tsx
Copy
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;
/app/frames/reveal/route.tsx
Copy
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 を利用する方法
この方法は Airstack API のデータを用いないため API キーの必要がなく、比較的簡単に実装できますが、Airstack API からデータを取得する処理を残しておかないと Airstack API への接続が発生せず、フレームの製作者を識別できないため、Moxie の報酬対象外となりますのでご注意ください。
ctx の内容をコンソールログに出力します。
/app/frames/reveal/route.tsx
Copy
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 でローカルサーバーを起動し、フレームのボタンをクリックしてコンソールログに表示されるデータを確認します。
コンソールのログ
コンソールのログ
おそらく以下のようなログが表示されるはずです。
Copy
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
}
Copy
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 に代入します。
/app/frames/reveal/route.tsx
Copy
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: の値として設定します。
/app/frames/reveal/route.tsx
Copy
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;
/app/frames/reveal/route.tsx
Copy
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;