Skip to content
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

feat: farcaster frame #707

Draft
wants to merge 4 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 4 additions & 176 deletions functions/_middleware.ts
Original file line number Diff line number Diff line change
@@ -1,177 +1,5 @@
/* eslint max-classes-per-file: "off" */
import { firefoxMiddleware } from './middlewares/firefox'
import { pathMiddleware } from './middlewares/path'
import { staticMiddleware } from './middlewares/static'

class ContentModifier {
private newContent: string

constructor(newContent: string) {
this.newContent = newContent
}

element(element: Element) {
element.setInnerContent(this.newContent)
}
}

class AttributeModifier {
private attributeName: string

private newContent: string

constructor(attributeName: string, newContent: string) {
this.attributeName = attributeName
this.newContent = newContent
}

element(element: Element) {
element.setAttribute(this.attributeName, this.newContent)
}
}

class ScriptWriter {
private src: string

constructor(src: string) {
this.src = src
}

element(element: Element) {
element.append(`<script src="${this.src}"></script>`, { html: true })
}
}

// exception for static files
const staticHandler: PagesFunction = async ({ request, next, env }) => {
const url = new URL(request.url)
const paths = url.pathname.split('/')

if (paths[paths.length - 1].match(/^.*\.(png|xml|ico|json|webmanifest|txt|svg|map|js)$/i)) {
return env.ASSETS.fetch(request)
}

return next()
}

const firefoxRewrite: PagesFunction = async ({ request, next }) => {
const userAgent = request.headers.get('user-agent')?.toLowerCase()

if (userAgent) {
if (
userAgent.indexOf('gecko/20100101') !== -1 &&
// firefox
userAgent.indexOf('firefox/') !== -1
) {
return new HTMLRewriter()
.on('head', new ScriptWriter('/_next/static/chunks/initialise-metamask.js'))
.transform(await next())
}
if (
userAgent.indexOf('webview metamaskmobile') !== -1 &&
userAgent.indexOf('applewebkit') !== -1
) {
return new HTMLRewriter()
.on('head', new ScriptWriter('/_next/static/chunks/initialise-metamask-ios.js'))
.transform(await next())
}
}

return next()
}

const baseOgImageUrl = 'https://ens-og-image.ens-cf.workers.dev'

const pathRewriter: PagesFunction = async ({ request, next }) => {
const url = new URL(request.url)
const paths = url.pathname.split('/')

const nextWithUpdate = () => next(new Request(url.toString(), request))

if (paths[1].match(/^0x[a-fA-F0-9]{40}$/)) {
const address = paths[1]
url.pathname = '/address'
url.searchParams.set('address', address)

const ogImageUrl = `${baseOgImageUrl}/address/${address}`

const newTitle = `${address.slice(0, 7)}...${address.slice(-5)} on ENS`
const newDescription = `${address}'s profile on the Ethereum Name Service`

return new HTMLRewriter()
.on('title', new ContentModifier(newTitle))
.on('meta[name="description"]', new AttributeModifier('content', newDescription))
.on('meta[property="og:image"]', new AttributeModifier('content', ogImageUrl))
.on('meta[property="og:title"]', new AttributeModifier('content', newTitle))
.on('meta[property="og:description"]', new AttributeModifier('content', newDescription))
.on('meta[property="twitter:image"]', new AttributeModifier('content', ogImageUrl))
.on('meta[property="twitter:title"]', new AttributeModifier('content', newTitle))
.on('meta[property="twitter:description"]', new AttributeModifier('content', newDescription))
.transform(await nextWithUpdate())
}

if (paths[1] === 'my' && paths[2] === 'profile') {
url.pathname = '/profile'
url.searchParams.set('connected', 'true')
return nextWithUpdate()
}

if (paths[1] === 'names' && !!paths[2]) {
url.pathname = '/my/names'
url.searchParams.set('address', paths[2])
return nextWithUpdate()
}

if (paths[1] === 'legacyFavourites') {
url.pathname = '/legacyfavourites'
return nextWithUpdate()
}

if (paths[1].match(/\./g) || paths[1] === 'tld') {
const isTLD = paths[1] === 'tld'
url.pathname = `/${(isTLD ? paths[3] : paths[2]) || 'profile'}`
url.searchParams.set('name', isTLD ? paths[2] : paths[1])

if (url.pathname === '/expired-profile') {
url.pathname = '/profile'
url.searchParams.set('expired', 'true')
return nextWithUpdate()
}

if (url.pathname === '/profile') {
const decodedName = decodeURIComponent(isTLD ? paths[2] : paths[1])
let newTitle = 'Invalid Name - ENS'
let newDescription = 'An error occurred'
let normalisedName: string | null = null
try {
const { normalize } = await import('viem/ens')
normalisedName = normalize(decodedName)
newTitle = `${normalisedName} on ENS`
newDescription = `${normalisedName}'s profile on the Ethereum Name Service`
} catch {
console.error('Name could not be normalised')
}

const ogImageUrl = normalisedName
? `${baseOgImageUrl}/name/${normalisedName}`
: `${baseOgImageUrl}/name/`

return new HTMLRewriter()
.on('title', new ContentModifier(newTitle))
.on('meta[name="description"]', new AttributeModifier('content', newDescription))
.on('meta[property="og:image"]', new AttributeModifier('content', ogImageUrl))
.on('meta[property="og:title"]', new AttributeModifier('content', newTitle))
.on('meta[property="og:description"]', new AttributeModifier('content', newDescription))
.on('meta[property="twitter:image"]', new AttributeModifier('content', ogImageUrl))
.on('meta[property="twitter:title"]', new AttributeModifier('content', newTitle))
.on(
'meta[property="twitter:description"]',
new AttributeModifier('content', newDescription),
)
.transform(await nextWithUpdate())
}

return nextWithUpdate()
}

return next()
}

export const onRequest = [staticHandler, firefoxRewrite, pathRewriter]
export const onRequest = [staticMiddleware, firefoxMiddleware, pathMiddleware]
27 changes: 27 additions & 0 deletions functions/middlewares/firefox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { ScriptWriter } from '../modifier/ScriptWriter'

export const firefoxMiddleware: PagesFunction = async ({ request, next }) => {
const userAgent = request.headers.get('user-agent')?.toLowerCase()

if (userAgent) {
if (
userAgent.indexOf('gecko/20100101') !== -1 &&
// firefox
userAgent.indexOf('firefox/') !== -1
) {
return new HTMLRewriter()
.on('head', new ScriptWriter('/_next/static/chunks/initialise-metamask.js'))
.transform(await next())
}
if (
userAgent.indexOf('webview metamaskmobile') !== -1 &&
userAgent.indexOf('applewebkit') !== -1
) {
return new HTMLRewriter()
.on('head', new ScriptWriter('/_next/static/chunks/initialise-metamask-ios.js'))
.transform(await next())
}
}

return next()
}
52 changes: 52 additions & 0 deletions functions/middlewares/path.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import { addressRewriter } from '../rewriter/address'
import { profileRewriter } from '../rewriter/profile'

export const pathMiddleware: PagesFunction = async ({ request, next }) => {
const url = new URL(request.url)
const paths = url.pathname.split('/')

const nextWithUpdate = () => next(new Request(url.toString(), request))

if (paths[1].match(/^0x[a-fA-F0-9]{40}$/)) {
const rewriter = await addressRewriter({ request, paths, url })
return rewriter.transform(await nextWithUpdate())
}

if (paths[1] === 'my' && paths[2] === 'profile') {
url.pathname = '/profile'
url.searchParams.set('connected', 'true')
return nextWithUpdate()
}

if (paths[1] === 'names' && !!paths[2]) {
url.pathname = '/my/names'
url.searchParams.set('address', paths[2])
return nextWithUpdate()
}

if (paths[1] === 'legacyFavourites') {
url.pathname = '/legacyfavourites'
return nextWithUpdate()
}

if (paths[1].match(/\./g) || paths[1] === 'tld') {
const isTld = paths[1] === 'tld'
url.pathname = `/${(isTld ? paths[3] : paths[2]) || 'profile'}`
url.searchParams.set('name', isTld ? paths[2] : paths[1])

if (url.pathname === '/expired-profile') {
url.pathname = '/profile'
url.searchParams.set('expired', 'true')
return nextWithUpdate()
}

if (url.pathname === '/profile') {
const rewriter = await profileRewriter({ isTld, paths, request })
return rewriter.transform(await nextWithUpdate())
}

return nextWithUpdate()
}

return next()
}
10 changes: 10 additions & 0 deletions functions/middlewares/static.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export const staticMiddleware: PagesFunction = async ({ request, next, env }) => {
const url = new URL(request.url)
const paths = url.pathname.split('/')

if (paths[paths.length - 1].match(/^.*\.(png|xml|ico|json|webmanifest|txt|svg|map|js)$/i)) {
return env.ASSETS.fetch(request)
}

return next()
}
14 changes: 14 additions & 0 deletions functions/modifier/AttributeModifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export class AttributeModifier {
private attributeName: string

private newContent: string

constructor(attributeName: string, newContent: string) {
this.attributeName = attributeName
this.newContent = newContent
}

element(element: Element) {
element.setAttribute(this.attributeName, this.newContent)
}
}
11 changes: 11 additions & 0 deletions functions/modifier/ContentModifier.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export class ContentModifier {
private newContent: string

constructor(newContent: string) {
this.newContent = newContent
}

element(element: Element) {
element.setInnerContent(this.newContent)
}
}
23 changes: 23 additions & 0 deletions functions/modifier/ElementsCreator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
export type ElementCreation = {
tagName: string
attributes: Record<string, string>
}

export class ElementsCreator {
private elements: ElementCreation[]

constructor(elements: ElementCreation[]) {
this.elements = elements
}

element(element: Element) {
for (const { tagName, attributes } of this.elements) {
element.append(
`<${tagName} ${Object.entries(attributes)
.map(([key, value]) => `${key}="${value}"`)
.join(' ')}></${tagName}>`,
{ html: true },
)
}
}
}
11 changes: 11 additions & 0 deletions functions/modifier/ScriptWriter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export class ScriptWriter {
private src: string

constructor(src: string) {
this.src = src
}

element(element: Element) {
element.append(`<script src="${this.src}"></script>`, { html: true })
}
}
Loading
Loading