diff --git a/lib/types/farcaster-types.js b/lib/types/farcaster-types.js index 8dcecf9b3..f8ab1a0c8 100644 --- a/lib/types/farcaster-types.js +++ b/lib/types/farcaster-types.js @@ -1,15 +1,29 @@ // @flow // This is a message that the rendered webpage // (landing/connect-farcaster.react.js) uses to communicate back // to the React Native WebView that is rendering it // (native/components/farcaster-web-view.react.js) export type FarcasterWebViewMessage = | { +type: 'farcaster_url', +url: string, } | { +type: 'farcaster_data', +fid: string, }; + +export type FarcasterUser = { + +fid: number, + +viewerContext: { + +following: boolean, + }, + ... +}; + +export type FarcasterChannel = { + +id: string, + +name: string, + ... +}; diff --git a/lib/utils/neynar-client.js b/lib/utils/neynar-client.js index bc52a9f11..546b73123 100644 --- a/lib/utils/neynar-client.js +++ b/lib/utils/neynar-client.js @@ -1,158 +1,148 @@ // @flow import invariant from 'invariant'; import { getMessageForException } from './errors.js'; - -type FarcasterUser = { - +fid: number, - +viewerContext: { - +following: boolean, - }, - ... -}; +import type { + FarcasterChannel, + FarcasterUser, +} from '../types/farcaster-types.js'; type FetchFollowersResponse = { +result: { +users: $ReadOnlyArray, +next: { +cursor: ?string, }, }, }; -type FarcasterChannel = { - +id: string, - +name: string, - ... -}; - type FetchFollowedFarcasterChannelsResponse = { +channels: $ReadOnlyArray, +next: { +cursor: ?string, }, }; const neynarBaseURL = 'https://api.neynar.com/'; const neynarURLs = { '1': `${neynarBaseURL}v1/farcaster/`, '2': `${neynarBaseURL}v2/farcaster/`, }; function getNeynarURL( apiVersion: string, apiCall: string, params: { [string]: string }, ): string { const neynarURL = neynarURLs[apiVersion]; invariant( neynarURL, `could not find Neynar URL for apiVersion ${apiVersion}`, ); return `${neynarURL}${apiCall}?${new URLSearchParams(params).toString()}`; } const fetchFollowerLimit = 150; const fetchFollowedChannelsLimit = 100; class NeynarClient { apiKey: string; constructor(apiKey: string) { this.apiKey = apiKey; } // We're using the term "friend" for a bidirectional follow async fetchFriendFIDs(fid: string): Promise { const fids = []; let paginationCursor = null; do { const params: { [string]: string } = { fid, viewerFid: fid, limit: fetchFollowerLimit.toString(), ...(paginationCursor ? { cursor: paginationCursor } : null), }; const url = getNeynarURL('1', 'followers', params); try { const response = await fetch(url, { method: 'GET', headers: { Accept: 'application/json', api_key: this.apiKey, }, }); const json: FetchFollowersResponse = await response.json(); const { users } = json.result; for (const user of users) { if (user.viewerContext.following) { fids.push(user.fid.toString()); } } paginationCursor = json.result.next.cursor; } catch (error) { console.log( 'Failed to fetch friend FIDs:', getMessageForException(error) ?? 'unknown', ); throw error; } } while (paginationCursor); return fids; } async fetchFollowedFarcasterChannels( fid: string, ): Promise { const farcasterChannels = []; let paginationCursor = null; do { const params: { [string]: string } = { fid, limit: fetchFollowedChannelsLimit.toString(), ...(paginationCursor ? { cursor: paginationCursor } : null), }; const url = getNeynarURL('2', 'user/channels', params); try { const response = await fetch(url, { method: 'GET', headers: { Accept: 'application/json', api_key: this.apiKey, }, }); const json: FetchFollowedFarcasterChannelsResponse = await response.json(); const { channels, next } = json; channels.forEach(channel => { farcasterChannels.push(channel); }); paginationCursor = next.cursor; } catch (error) { console.log( 'Failed to fetch followed Farcaster channels:', getMessageForException(error) ?? 'unknown', ); throw error; } } while (paginationCursor); return farcasterChannels; } } export { NeynarClient };