-
-
Notifications
You must be signed in to change notification settings - Fork 30
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[FH-2] Open Frames Implementation (#163) #185
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
@@ -1,133 +1,134 @@ | ||||
import { client } from '@/db/client' | ||||
import { frameTable, interactionTable } from '@/db/schema' | ||||
import type { BuildFrameData, FramePayload } from '@/lib/farcaster' | ||||
import { updateFrameStorage } from '@/lib/frame' | ||||
import { buildFramePage, validatePayload, validatePayloadAirstack } from '@/lib/serve' | ||||
import type { BaseConfig, BaseStorage } from '@/lib/types' | ||||
import { FrameError } from '@/sdk/error' | ||||
import templates from '@/templates' | ||||
import { waitUntil } from '@vercel/functions' | ||||
import { type InferSelectModel, eq } from 'drizzle-orm' | ||||
import { notFound } from 'next/navigation' | ||||
import type { NextRequest } from 'next/server' | ||||
|
||||
export const dynamic = 'force-dynamic' | ||||
export const dynamicParams = true | ||||
export const fetchCache = 'force-no-store' | ||||
import { NextRequest } from 'next/server'; | ||||
import { client } from '@/db/client'; | ||||
import { frameTable, interactionTable } from '@/db/schema'; | ||||
import type { BuildFrameData, FramePayload } from '@/lib/farcaster'; | ||||
import { updateFrameStorage } from '@/lib/frame'; | ||||
import { buildFramePage } from '@/lib/serve'; | ||||
import type { BaseConfig, BaseStorage } from '@/lib/types'; | ||||
import { FrameError } from '@/sdk/error'; | ||||
import templates from '@/templates'; | ||||
import { waitUntil } from '@vercel/functions'; | ||||
import { type InferSelectModel, eq } from 'drizzle-orm'; | ||||
import { notFound } from 'next/navigation'; | ||||
import { validateFramePayload } from '@/lib/validation'; | ||||
|
||||
export const dynamic = 'force-dynamic'; | ||||
export const dynamicParams = true; | ||||
export const fetchCache = 'force-no-store'; | ||||
|
||||
export async function POST( | ||||
request: NextRequest, | ||||
{ params }: { params: { frameId: string; handler: string } } | ||||
) { | ||||
const searchParams: Record<string, string> = {} | ||||
const searchParams: Record<string, string> = {}; | ||||
|
||||
request.nextUrl.searchParams.forEach((value, key) => { | ||||
if (!['frameId', 'handler'].includes(key)) { | ||||
searchParams[key] = value | ||||
searchParams[key] = value; | ||||
} | ||||
}) | ||||
}); | ||||
|
||||
const frame = await client | ||||
.select() | ||||
.from(frameTable) | ||||
.where(eq(frameTable.id, params.frameId)) | ||||
.get() | ||||
.get(); | ||||
|
||||
if (!frame) { | ||||
notFound() | ||||
notFound(); | ||||
} | ||||
|
||||
if (!frame.config) { | ||||
notFound() | ||||
notFound(); | ||||
} | ||||
|
||||
const template = templates[frame.template] | ||||
const template = templates[frame.template]; | ||||
|
||||
const payload = (await request.json()) as FramePayload | ||||
const payload = (await request.json()) as FramePayload; | ||||
|
||||
const handlerFn = template.handlers[params.handler as keyof typeof template.handlers] | ||||
const handlerFn = template.handlers[params.handler as keyof typeof template.handlers]; | ||||
|
||||
if (!handlerFn) { | ||||
notFound() | ||||
notFound(); | ||||
} | ||||
|
||||
const validatedPayload = await validatePayload(payload) | ||||
|
||||
let buildParameters = {} as BuildFrameData | ||||
const validatedPayload = await validateFramePayload(payload); | ||||
|
||||
let buildParameters = {} as BuildFrameData; | ||||
|
||||
try { | ||||
buildParameters = await handlerFn({ | ||||
body: validatedPayload, | ||||
config: frame.config as BaseConfig, | ||||
storage: frame.storage as BaseStorage, | ||||
params: searchParams, | ||||
}) | ||||
}); | ||||
} catch (error) { | ||||
if (error instanceof FrameError) { | ||||
return Response.json( | ||||
{ message: error.message }, | ||||
{ | ||||
status: 400, | ||||
} | ||||
) | ||||
); | ||||
} | ||||
|
||||
console.error(error) | ||||
console.error(error); | ||||
|
||||
return Response.json( | ||||
{ message: 'Unknown error' }, | ||||
{ | ||||
status: 500, | ||||
} | ||||
) | ||||
); | ||||
} | ||||
|
||||
if (buildParameters.transaction) { | ||||
waitUntil(processFrame(frame, buildParameters, payload)) | ||||
waitUntil(processFrame(frame, buildParameters, payload)); | ||||
|
||||
return new Response(JSON.stringify(buildParameters.transaction), { | ||||
headers: { | ||||
'Content-Type': 'application/json', | ||||
}, | ||||
}) | ||||
}); | ||||
} | ||||
|
||||
const renderedFrame = await buildFramePage({ | ||||
id: frame.id, | ||||
linkedPage: frame.linkedPage || undefined, | ||||
...(buildParameters as BuildFrameData), | ||||
}) | ||||
}); | ||||
|
||||
waitUntil(processFrame(frame, buildParameters, payload)) | ||||
waitUntil(processFrame(frame, buildParameters, payload)); | ||||
|
||||
return new Response(renderedFrame, { | ||||
headers: { | ||||
'Content-Type': 'text/html', | ||||
}, | ||||
}) | ||||
}); | ||||
} | ||||
|
||||
async function processFrame( | ||||
frame: InferSelectModel<typeof frameTable>, | ||||
parameters: BuildFrameData, | ||||
payload: FramePayload | ||||
) { | ||||
const storageData = parameters.storage as BaseStorage | undefined | ||||
const storageData = parameters.storage as BaseStorage | undefined; | ||||
|
||||
if (storageData) { | ||||
await updateFrameStorage(frame.id, storageData) | ||||
await updateFrameStorage(frame.id, storageData); | ||||
} | ||||
|
||||
if (frame.webhooks) { | ||||
const webhookUrls = frame.webhooks | ||||
const webhookUrls = frame.webhooks; | ||||
|
||||
if (!webhookUrls) { | ||||
return | ||||
return; | ||||
} | ||||
|
||||
for (const webhook of parameters?.webhooks || []) { | ||||
if (!webhookUrls?.[webhook.event]) { | ||||
continue | ||||
continue; | ||||
} | ||||
|
||||
fetch(webhookUrls[webhook.event], { | ||||
|
@@ -142,34 +143,28 @@ async function processFrame( | |||
}), | ||||
}) | ||||
.then(() => { | ||||
console.log('Sent webhook') | ||||
console.log('Sent webhook'); | ||||
}) | ||||
.catch((e) => { | ||||
console.error('Error sending webhook', e) | ||||
}) | ||||
console.error('Error sending webhook', e); | ||||
}); | ||||
} | ||||
} | ||||
|
||||
const airstackKey = frame.config?.airstackKey || process.env.AIRSTACK_API_KEY | ||||
|
||||
const airstackPayloadValidated = await validatePayloadAirstack(payload, airstackKey) | ||||
|
||||
console.log(JSON.stringify(airstackPayloadValidated, null, 2)) | ||||
const validatedPayload = await validateFramePayload(payload); | ||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Avoid redundant payload validation in The payload has already been validated before calling Consider removing the redundant validation: - const validatedPayload = await validateFramePayload(payload); 📝 Committable suggestion
Suggested change
|
||||
|
||||
await client | ||||
.insert(interactionTable) | ||||
.values({ | ||||
frame: frame.id, | ||||
fid: airstackPayloadValidated.message.data.fid.toString(), | ||||
buttonIndex: | ||||
airstackPayloadValidated.message.data.frameActionBody.buttonIndex.toString(), | ||||
inputText: airstackPayloadValidated.message.data.frameActionBody.inputText || undefined, | ||||
state: airstackPayloadValidated.message.data.frameActionBody.state || undefined, | ||||
transactionHash: | ||||
airstackPayloadValidated.message.data.frameActionBody.transactionId || undefined, | ||||
castFid: airstackPayloadValidated.message.data.frameActionBody.castId.fid.toString(), | ||||
castHash: airstackPayloadValidated.message.data.frameActionBody.castId.hash, | ||||
fid: validatedPayload.interactor.fid.toString(), | ||||
buttonIndex: validatedPayload.tapped_button.index.toString(), | ||||
inputText: validatedPayload.input?.text || undefined, | ||||
state: validatedPayload.state?.serialized || undefined, | ||||
transactionHash: validatedPayload.transactionId || undefined, | ||||
castFid: validatedPayload.cast.fid.toString(), | ||||
castHash: validatedPayload.cast.hash, | ||||
createdAt: new Date(), | ||||
}) | ||||
.run() | ||||
} | ||||
.run(); | ||||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -42,4 +42,4 @@ export default async function ShowPage({ params }: { params: { frameId: string } | |
</div> | ||
</div> | ||
) | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,3 @@ | ||
|
||
import { Progress } from '@/components/shadcn/Progress' | ||
|
||
const FUNNY_MESSAGES = [ | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🛠️ Refactor suggestion
Consider
import type
for type-only importsStatic analysis hints suggest that some imports are only used as types. Using
import type
can help reduce bundle size by ensuring that these are removed during transpilation.Apply this fix where applicable:
Verify that changing to
import type
does not affect runtime code, as it should only be used when imports are exclusively for types.🧰 Tools
🪛 Biome