diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..f7e70b5 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,12 @@ +{ + "parser": "@typescript-eslint/parser", + "plugins": ["@typescript-eslint"], + "env": { + "node": true + }, + "extends": [ + "eslint:recommended", + "plugin:@typescript-eslint/recommended", + "prettier" + ] +} diff --git a/.gitignore b/.gitignore index 598f67e..cde652c 100644 --- a/.gitignore +++ b/.gitignore @@ -14,11 +14,14 @@ yarn-error.log* pnpm-debug.log* lerna-debug.log* +*.js + # sharp issue: https://sharp.pixelplumbing.com/install#cross-platform package-lock.json node_modules dist +built dist-ssr *.local diff --git a/api/frame.js b/api/frame.js deleted file mode 100644 index b98067c..0000000 --- a/api/frame.js +++ /dev/null @@ -1,30 +0,0 @@ -import frames from "../src/frames"; -import { URLSearchParamsToObject } from '../modules/utils'; - -export default async (req, context) => { - const url = new URL(req.url); - const params = URLSearchParamsToObject(url.searchParams); - const targetFrameSrc = frames[params.targetFrameName]; - - if (targetFrameSrc.image) { - const image = `${process.env.URL}${targetFrameSrc.image}` - return new Response(image, - { - status: 200, - headers: { 'Content-Type': 'image/png' }, - } - ); - } else if (targetFrameSrc.build) { - const markup = await targetFrameSrc.build(params); - return new Response(markup, - { - status: 200, - headers: { 'Content-Type': 'text/html; charset=utf-8' }, - } - ); - } -} - -export const config = { - path: "/frame" -}; diff --git a/api/index.js b/api/index.ts similarity index 85% rename from api/index.js rename to api/index.ts index 1a6fae0..179c7e9 100644 --- a/api/index.js +++ b/api/index.ts @@ -1,11 +1,11 @@ -import { getFrameMessage } from "frames.js" -import landingPage from '../src/landing-page'; -import { parseRequest, objectToURLSearchParams } from '../modules/utils'; -import buildButtons from '../modules/buildButtons'; -import buildInputs from '../modules/buildInputs'; -import getTargetFrame from '../modules/getTargetFrame'; +import { getFrameMessage } from "frames.js"; +import landingPage from '../src/landing-page.js'; +import { parseRequest, objectToURLSearchParams } from '../modules/utils.js'; +import buildButtons from '../modules/buildButtons.js'; +import buildInputs from '../modules/buildInputs.js'; +import getTargetFrame from '../modules/getTargetFrame.js'; -export default async (req, context) => { +export default async (req) => { try { const requestURL = new URL(req.url); const payload = await parseRequest(req); @@ -40,7 +40,7 @@ const respondWithRedirect = (redirectURL) => { { status: 302, headers: { - 'Location': internalRedirectURL, + 'Location': internalRedirectURL.toString(), }, } ); @@ -48,6 +48,7 @@ const respondWithRedirect = (redirectURL) => { const respondWithFrame = async (targetFrame, frameMessage) => { const searchParams = { + t: new Date().valueOf(), targetFrameName: targetFrame.name, frameMessage } diff --git a/api/og-image.js b/api/og-image.ts similarity index 68% rename from api/og-image.js rename to api/og-image.ts index 232fbde..e88337b 100644 --- a/api/og-image.js +++ b/api/og-image.ts @@ -1,22 +1,22 @@ import satori from "satori"; import sharp from "sharp"; import { html } from "satori-html"; -import fonts from "../src/fonts"; -import frames from "../src/frames"; -import { URLSearchParamsToObject } from '../modules/utils'; +import fonts from "../src/fonts.js"; +import frames from "../src/frames/index.js"; +import { URLSearchParamsToObject } from '../modules/utils.js'; -export default async (req, context) => { +export default async (req) => { const url = new URL(req.url); const params = URLSearchParamsToObject(url.searchParams); - const targetFrame = frames[params.targetFrameName]; - const markup = await targetFrame.build(params.frameMessage); + const targetFrame = frames[params['targetFrameName']]; + const markup = await targetFrame.build(params['frameMessage']); const svg = await satori( html(markup), { width: 1200, height: 630, - fonts + fonts: fonts, } ); const svgBuffer = Buffer.from(svg); diff --git a/api/redirect.js b/api/redirect.ts similarity index 100% rename from api/redirect.js rename to api/redirect.ts diff --git a/modules/antitheft.js b/modules/antitheft.ts similarity index 79% rename from modules/antitheft.js rename to modules/antitheft.ts index d196a32..c286bd4 100644 --- a/modules/antitheft.js +++ b/modules/antitheft.ts @@ -1,10 +1,9 @@ import { getStore } from '@netlify/blobs'; -import frame from '../api/frame'; // Utility functions to abstract the fetching and setting operations const fetchData = async (key) => { const store = getStore('antiTheft'); - let data = await store.get(key, 'json') || []; + let data = await store.get(key, { type: 'json' } ) || []; if (!data.length) { data = process.env[key] ? JSON.parse(process.env[key]) : []; } @@ -25,7 +24,7 @@ const setBoundCasts = (castHashes) => setData('BOUND_CAST_HASHES', castHashes); // Modified functions to use the updated getters and setters const addToList = async (getter, setter, item) => { - let list = await getter(); + const list = await getter(); if (!list.includes(item)) { list.push(item); await setter(list); @@ -33,7 +32,7 @@ const addToList = async (getter, setter, item) => { }; const removeFromList = async (getter, setter, item) => { - let list = await getter(); + const list = await getter(); const index = list.indexOf(item); if (index > -1) { list.splice(index, 1); @@ -52,10 +51,8 @@ const removeBoundCast = (castHash) => removeFromList(getBoundCasts, setBoundCast // 2. The castHash is in boundCasts. // 3. Both boundCasts & boundAccounts are empty. const isFrameStolen = async (frameMessage) => { - console.log('isFrameStolen', frameMessage); const { castId, requestURL } = frameMessage; if (!castId || !requestURL) { - console.log('isFrameStolen:quickExit', castId, requestURL); return false; } @@ -67,11 +64,23 @@ const isFrameStolen = async (frameMessage) => { const isCastAllowed = boundCasts.includes(castHash) || boundCasts.length === 0; const isFirstParty = requestURL ? requestURL.indexOf(process.env.URL) > -1 : true; - console.log('isAuthorAllowed', isAuthorAllowed, castAuthorID, boundAccounts); - console.log('isCastAllowed', isCastAllowed, castHash, boundCasts); - console.log('isFirstParty', isFirstParty); + const isStolen = !isFirstParty || !isAuthorAllowed || !isCastAllowed; - return !isFirstParty || !isAuthorAllowed || !isCastAllowed; + // record the theft + if (isStolen) { + const store = getStore('stolenFrames'); + const stolenFrame = await store.get(castHash, { type: 'json' }) || { + castHash, + castAuthorID, + views: 0, + firstView: new Date().toUTCString(), + }; + stolenFrame.view++; + stolenFrame.lastView = new Date().toUTCString(); + store.setJSON(castHash, stolenFrame); + } + + return isStolen; }; export { diff --git a/modules/buildButtons.js b/modules/buildButtons.ts similarity index 100% rename from modules/buildButtons.js rename to modules/buildButtons.ts diff --git a/modules/buildInputs.js b/modules/buildInputs.ts similarity index 72% rename from modules/buildInputs.js rename to modules/buildInputs.ts index 45a8d97..338d83c 100644 --- a/modules/buildInputs.js +++ b/modules/buildInputs.ts @@ -1,8 +1,8 @@ -import buildTextInput from "./buildTextInput"; +import buildTextInput from "./buildTextInput.js"; export default (inputs) => { return inputs - .map((input, index) => { + .map((input) => { switch (input.type) { case 'text': return buildTextInput(input); diff --git a/modules/buildTextInput.js b/modules/buildTextInput.ts similarity index 100% rename from modules/buildTextInput.js rename to modules/buildTextInput.ts diff --git a/modules/getTargetFrame.js b/modules/getTargetFrame.ts similarity index 87% rename from modules/getTargetFrame.js rename to modules/getTargetFrame.ts index 90a8907..62b8750 100644 --- a/modules/getTargetFrame.js +++ b/modules/getTargetFrame.ts @@ -1,6 +1,6 @@ const DEFAULT_FRAME = 'poster'; -import frames from '../src/frames'; -import { isFrameStolen } from './antitheft'; +import frames from '../src/frames/index.js'; +import { isFrameStolen } from './antitheft.js'; export default async (frameMessage) => { const frameIsStolen = await isFrameStolen(frameMessage); diff --git a/modules/sanitize.js b/modules/sanitize.ts similarity index 100% rename from modules/sanitize.js rename to modules/sanitize.ts diff --git a/modules/utils.js b/modules/utils.ts similarity index 95% rename from modules/utils.js rename to modules/utils.ts index 945b27e..68fc35a 100644 --- a/modules/utils.js +++ b/modules/utils.ts @@ -53,6 +53,7 @@ const URLSearchParamsToObject = (searchParams) => { const obj = {}; for (const [key, value] of searchParams.entries()) { + // eslint-disable-next-line no-useless-escape const keys = key.split(/[\[\]]/g).filter(k => k); let currentObj = obj; @@ -81,7 +82,7 @@ const URLSearchParamsToObject = (searchParams) => { const loadFont = async (fileName) => { try { - const filePath = path.join(__dirname, '../src', 'fonts', fileName); + const filePath = path.join(__dirname, '../../public', 'fonts', fileName); const fontData = await fs.readFile(filePath); return fontData; } catch (error) { diff --git a/netlify.toml b/netlify.toml index 77b9c69..3e822a7 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,8 +1,8 @@ [build] - functions = "/api" - publish = "/public" + publish = "public/" [functions] + directory = "api/" node_bundler = "esbuild" external_node_modules = ["sharp"] included_files = ["node_modules/sharp/**/*"] \ No newline at end of file diff --git a/package.json b/package.json index 05b86db..697174c 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,24 @@ { "name": "simplest-frame", "type": "module", - "version": "0.8.1", + "version": "0.8.2", "dependencies": { "@netlify/blobs": "^6.4.2", "dompurify": "^3.0.8", - "frames.js": "^0.1.1", + "frames.js": "^0.5.0", "jsdom": "^24.0.0", - "satori": "^0.10.11", + "satori": "^0.10.13", "satori-html": "^0.3.2", "sharp": "^0.32.6" + }, + "devDependencies": { + "@types/node": "^20.11.17", + "@typescript-eslint/eslint-plugin": "^6.21.0", + "@typescript-eslint/parser": "^6.21.0", + "eslint": "^8.56.0", + "eslint-config-prettier": "^9.1.0", + "eslint-plugin-prettier": "^5.1.3", + "prettier": "^3.2.5", + "typescript": "^5.3.3" } } diff --git a/src/fonts/Redaction-Regular.otf b/public/fonts/Redaction-Regular.otf similarity index 100% rename from src/fonts/Redaction-Regular.otf rename to public/fonts/Redaction-Regular.otf diff --git a/public/fonts/Redaction100-Regular.otf b/public/fonts/Redaction100-Regular.otf new file mode 100644 index 0000000..036b694 Binary files /dev/null and b/public/fonts/Redaction100-Regular.otf differ diff --git a/public/fonts/Redaction_100-Regular.woff2 b/public/fonts/Redaction_100-Regular.woff2 new file mode 100644 index 0000000..cdd97e8 Binary files /dev/null and b/public/fonts/Redaction_100-Regular.woff2 differ diff --git a/src/data/cast.js b/src/data/cast.ts similarity index 86% rename from src/data/cast.js rename to src/data/cast.ts index 542843c..7f93c68 100644 --- a/src/data/cast.js +++ b/src/data/cast.ts @@ -1,4 +1,4 @@ -import { streamToString } from '../../modules/utils'; +import { streamToString } from '../../modules/utils.js'; const getCast = async(castHash) => { const request = await fetch(`https://protocol.wield.co/farcaster/v2/cast?hash=${castHash}`, { diff --git a/src/data/count.js b/src/data/count.ts similarity index 100% rename from src/data/count.js rename to src/data/count.ts diff --git a/src/data/framer.js b/src/data/framer.ts similarity index 84% rename from src/data/framer.js rename to src/data/framer.ts index 2c62b39..9c24cc2 100644 --- a/src/data/framer.js +++ b/src/data/framer.ts @@ -1,6 +1,6 @@ import { getStore } from '@netlify/blobs'; -import { getUsername } from './username'; -import sanitize from '../../modules/sanitize'; +import { getUsername } from './username.js'; +import sanitize from '../../modules/sanitize.js'; const getFramer = async() => { const store = getStore('gameState'); diff --git a/src/data/username.js b/src/data/username.ts similarity index 86% rename from src/data/username.js rename to src/data/username.ts index 6c12d3b..7e7d163 100644 --- a/src/data/username.js +++ b/src/data/username.ts @@ -1,4 +1,4 @@ -import { streamToString } from '../../modules/utils'; +import { streamToString } from '../../modules/utils.js'; const getUsername = async(fid) => { const request = await fetch(`https://protocol.wield.co/farcaster/v2/user?fid=${fid}`, { diff --git a/src/fonts.ts b/src/fonts.ts new file mode 100644 index 0000000..5103df1 --- /dev/null +++ b/src/fonts.ts @@ -0,0 +1,19 @@ +import { Font, FontStyle, FontWeight } from 'satori'; +import { loadFont } from '../modules/utils.js'; + +const fonts: Font[] = [ + { + name: 'Redaction', + data: await loadFont('Redaction-Regular.otf'), + weight: 400 as FontWeight, // Explicitly casting as Weight type + style: 'normal' as FontStyle, // Explicitly casting as FontStyle type + }, + { + name: 'Redaction-100', + data: await loadFont('Redaction100-Regular.otf'), + weight: 400 as FontWeight, // Explicitly casting as Weight type + style: 'normal' as FontStyle, // Explicitly casting as FontStyle type + } +]; + +export default fonts; diff --git a/src/fonts/index.js b/src/fonts/index.js deleted file mode 100644 index 3d779bb..0000000 --- a/src/fonts/index.js +++ /dev/null @@ -1,11 +0,0 @@ -import { loadFont } from '../../modules/utils'; - -export default [ - { - name: 'Redaction', - data: await loadFont('Redaction-Regular.otf'), - previewPath: '/fonts/Redaction-Regular.woff2', - weight: 400, - style: 'normal', - } -]; \ No newline at end of file diff --git a/src/frames/count.js b/src/frames/count.ts similarity index 72% rename from src/frames/count.js rename to src/frames/count.ts index 6a6f6bc..89e59e3 100644 --- a/src/frames/count.js +++ b/src/frames/count.ts @@ -1,6 +1,6 @@ -import mainLayout from '../layouts/main'; -import { getFramer, setFramer } from '../data/framer'; -import { getCount, incrementCount } from '../data/count'; +import mainLayout from '../layouts/main.js'; +import { getFramer, setFramer } from '../data/framer.js'; +import { getCount, incrementCount } from '../data/count.js'; const build = async (frameMessage) => { let count = await getCount(); @@ -11,10 +11,9 @@ const build = async (frameMessage) => { await setFramer(frameMessage.requesterFid, tauntInput); } - const { username, taunt } = await getFramer() || ''; + const { username, taunt } = await getFramer() || {}; - let tauntOutput; - tauntOutput = taunt ? ` + const tauntOutput = taunt ? `
"${taunt}"
@@ -23,8 +22,8 @@ const build = async (frameMessage) => { const html = String.raw; const frameHTML = html` -
- i've been framed ${count || 0} times +
+ i've been framed ${count || 0} times
last framed by @${username || ''} diff --git a/src/frames/credits.js b/src/frames/credits.ts similarity index 100% rename from src/frames/credits.js rename to src/frames/credits.ts diff --git a/src/frames/index.js b/src/frames/index.js deleted file mode 100644 index 9c671c8..0000000 --- a/src/frames/index.js +++ /dev/null @@ -1,11 +0,0 @@ -import poster from "./poster"; -import count from "./count"; -import credits from "./credits"; -import stolen from "./stolen"; - -export default { - count, - poster, - credits, - stolen, -} diff --git a/src/frames/index.ts b/src/frames/index.ts new file mode 100644 index 0000000..3cbb49c --- /dev/null +++ b/src/frames/index.ts @@ -0,0 +1,11 @@ +import poster from "./poster.js"; +import count from "./count.js"; +import credits from "./credits.js"; +import stolen from "./stolen.js"; + +export default { + count, + poster, + credits, + stolen, +} diff --git a/src/frames/poster.js b/src/frames/poster.ts similarity index 92% rename from src/frames/poster.js rename to src/frames/poster.ts index cfec801..719d610 100644 --- a/src/frames/poster.js +++ b/src/frames/poster.ts @@ -1,4 +1,3 @@ -const html = String.raw; export default { name: 'poster', image: `/images/poster-animated.gif`, diff --git a/src/frames/stolen.js b/src/frames/stolen.ts similarity index 100% rename from src/frames/stolen.js rename to src/frames/stolen.ts diff --git a/src/landing-page.js b/src/landing-page.ts similarity index 72% rename from src/landing-page.js rename to src/landing-page.ts index 9e29aa8..0de6f65 100644 --- a/src/landing-page.js +++ b/src/landing-page.ts @@ -1,10 +1,5 @@ -import fonts from './fonts'; - export default async (frameContent) => { - const fontFile = fonts[0].file; // TODO: we'll have more than 1 font at some point - const fontName = fonts[0].name; - const html = String.raw; const markup = html` @@ -12,10 +7,10 @@ export default async (frameContent) => { diff --git a/src/layouts/main.js b/src/layouts/main.ts similarity index 72% rename from src/layouts/main.js rename to src/layouts/main.ts index f9c8958..36d671c 100644 --- a/src/layouts/main.js +++ b/src/layouts/main.ts @@ -1,23 +1,19 @@ -import fonts from '../fonts'; - export default (payload, frameHTML) => { - const fontFile = fonts[0].file; // TODO: eventually we'll have more than one font - const fontName = fonts[0].name; const html = String.raw; const markup = html`