diff --git a/apps/scoutgamecron/package.json b/apps/scoutgamecron/package.json index cca2e53535..129a9f65f7 100644 --- a/apps/scoutgamecron/package.json +++ b/apps/scoutgamecron/package.json @@ -16,6 +16,7 @@ "@packages/github": "^0.0.0", "@packages/onchain": "^0.0.0", "@packages/scoutgame": "^0.0.0", + "@packages/utils": "^1.0.0", "web-push": "^3.6.7" }, "devDependencies": { diff --git a/apps/scoutgamecron/src/scripts/createBuilders.ts b/apps/scoutgamecron/src/scripts/createBuilders.ts new file mode 100644 index 0000000000..74b876e7ec --- /dev/null +++ b/apps/scoutgamecron/src/scripts/createBuilders.ts @@ -0,0 +1,107 @@ +import { log } from '@charmverse/core/log'; +import { prisma } from '@charmverse/core/prisma-client' +import { octokit } from '@packages/github/client'; + +import * as http from '@packages/utils/http'; + +export type FarcasterProfile = { + username: string; + fid: number; + display_name: string; + pfp_url: string; + profile: { + bio: { + text: string; + } + } +}; + +const profileApiUrl = 'https://api.neynar.com/v2/farcaster/user/bulk'; + +export async function getFarcasterProfileById(fid: number) { + const {users} = await http + .GET<{users: FarcasterProfile[]}>( + `${profileApiUrl}?fids=${fid}`, + {}, + { + credentials: 'omit', + headers: { + 'X-Api-Key': process.env.NEYNAR_API_KEY as string + } + } + ); + return users[0] || null; +} + + +const builderWaitlistLogins: string[] = [] + +async function createBuilders() { + for (const login of builderWaitlistLogins) { + const waitlistSlot = await prisma.connectWaitlistSlot.findFirst({ + where: { + OR: [ + { + githubLogin: login, + }, + { + githubLogin: login.toLowerCase(), + } + ] + } + }) + const fid = waitlistSlot?.fid; + + if (!waitlistSlot || !fid) { + log.warn(`No waitlist slot or fid found for ${login}`) + continue + } + + if (waitlistSlot && fid) { + try { + const githubUser = await octokit.rest.users.getByUsername({ username: login }) + const profile = await getFarcasterProfileById(fid) + if (!profile) { + log.info(`No profile found for ${login}`) + continue + } + const displayName = profile.display_name; + const username = profile.username; + const avatarUrl = profile.pfp_url; + const bio = profile.profile.bio.text; + if (!username) { + log.info(`No username found for ${login} with fid ${fid}`) + continue + } + const builder = await prisma.scout.upsert({ + where: { + username, + }, + update: {}, + create: { + displayName, + username, + avatar: avatarUrl, + bio, + builderStatus: "applied", + farcasterId: fid, + farcasterName: displayName, + githubUser: { + create: { + id: githubUser.data.id, + login, + displayName: githubUser.data.name, + email: githubUser.data.email, + } + } + } + }) + log.info(`Created builder for ${login}`, { builderId: builder.id }) + } catch (error) { + log.error(`Error creating builder for ${login}`, { error }) + } + } + } +} + +createBuilders() \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9c2e8d3c2a..f9ec5f10ea 100644 --- a/package-lock.json +++ b/package-lock.json @@ -542,6 +542,7 @@ "@packages/github": "^0.0.0", "@packages/onchain": "^0.0.0", "@packages/scoutgame": "^0.0.0", + "@packages/utils": "^1.0.0", "web-push": "^3.6.7" }, "devDependencies": { @@ -113610,6 +113611,7 @@ "@packages/github": "^0.0.0", "@packages/onchain": "^0.0.0", "@packages/scoutgame": "^0.0.0", + "@packages/utils": "^1.0.0", "@swc/core": "^1.7.26", "@types/web-push": "^3.6.3", "web-push": "^3.6.7" diff --git a/packages/utils/package.json b/packages/utils/package.json index 33323399a6..958f4738d9 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -11,6 +11,7 @@ "./numbers": "./src/numbers.ts", "./react": "./src/react.ts", "./types": "./src/types.ts", - "./dates": "./src/dates.ts" + "./dates": "./src/dates.ts", + "./http": "./src/http.ts" } -} +} \ No newline at end of file diff --git a/packages/utils/src/fetch.ts b/packages/utils/src/fetch.ts new file mode 100644 index 0000000000..8c23619aa5 --- /dev/null +++ b/packages/utils/src/fetch.ts @@ -0,0 +1,33 @@ +type RequestInit = Parameters[1]; + +export async function transformResponse(response: Response) { + const contentType = response.headers.get('content-type'); + + if (response.status >= 400) { + // necessary to capture the regular response for embedded blocks + if (contentType?.includes('application/json')) { + try { + const jsonResponse = await response.json(); + return Promise.reject({ status: response.status, ...jsonResponse }); + } catch (error) { + // not valid JSON, content-type is lying! + } + } + // Note: 401 if user is logged out + return response.text().then((text) => Promise.reject({ status: response.status, message: text })); + } + + if (contentType?.includes('application/json')) { + return response.json(); + } else if (contentType?.includes('application/octet-stream')) { + return response.blob(); + } + return response.text().then((_response) => { + // since we expect JSON, dont return the true value for 200 response + return _response === 'OK' ? null : _response; + }); +} + +export default function fetchWrapper(resource: string, init?: RequestInit): Promise { + return fetch(resource, init).then(transformResponse) as Promise; +} diff --git a/packages/utils/src/http.ts b/packages/utils/src/http.ts new file mode 100644 index 0000000000..0626d569ff --- /dev/null +++ b/packages/utils/src/http.ts @@ -0,0 +1,95 @@ +import fetch from './fetch'; + +type Params = { [key: string]: any }; + +export function GET( + _requestUrl: string, + data: Params = {}, + { headers = {}, credentials = 'include' }: { credentials?: RequestCredentials; headers?: any } = {} +): Promise { + const requestUrl = _appendQuery(_requestUrl, data); + return fetch(requestUrl, { + method: 'GET', + headers: new Headers({ + Accept: 'application/json', + ...headers + }), + credentials + }); +} + +export function DELETE( + _requestUrl: string, + data: Params = {}, + { headers = {} }: { headers?: any } = {} +): Promise { + const requestUrl = _appendQuery(_requestUrl, data); + return fetch(requestUrl, { + method: 'DELETE', + headers: new Headers({ + Accept: 'application/json', + 'Content-Type': 'application/json', + ...headers + }), + credentials: 'include' + }); +} + +export function POST( + requestURL: string, + data: Params | string = {}, + { + headers = {}, + noHeaders, + skipStringifying, + credentials = 'include', + query + }: { + credentials?: RequestCredentials; + headers?: any; + noHeaders?: boolean; + skipStringifying?: boolean; + query?: any; + } = {} +): Promise { + const urlWithQuery = query ? _appendQuery(requestURL, query || {}) : requestURL; + + return fetch(urlWithQuery, { + body: !skipStringifying ? JSON.stringify(data) : (data as string), + method: 'POST', + headers: noHeaders + ? undefined + : new Headers({ + Accept: 'application/json', + 'Content-Type': 'application/json', + ...headers + }), + credentials + }); +} + +export function PUT(requestURL: string, data: Params = {}, { headers = {} }: { headers?: any } = {}): Promise { + return fetch(requestURL, { + body: JSON.stringify(data), + method: 'PUT', + headers: new Headers({ + Accept: 'application/json', + 'Content-Type': 'application/json', + ...headers + }), + credentials: 'include' + }); +} + +function _appendQuery(path: string, data: Params) { + const queryString = Object.keys(data) + .filter((key) => !!data[key]) + .map((key) => { + const value = data[key]; + return Array.isArray(value) + ? `${value.map((v: string) => `${key}=${v}`).join('&')}` + : `${key}=${encodeURIComponent(value)}`; + }) + .join('&'); + return `${path}${queryString ? `?${queryString}` : ''}`; +}