From f873cb4078b69f0b2d4cc89b6514ef0b805c483f Mon Sep 17 00:00:00 2001 From: Complexlity Date: Fri, 9 Aug 2024 18:54:09 +0100 Subject: [PATCH] wip: airstack --- .gitignore | 1 + data.json | 34 ++++++++++ package.json | 1 + pnpm-lock.yaml | 20 ++++++ src/index.ts | 5 +- src/playground.ts | 40 ++++++------ src/services/airstack/index.ts | 83 ++++++++++++++++++++---- src/services/airstack/types.ts | 42 ++++++++++++ src/services/airstack/utils.ts | 115 +++++++++++++++++++++++++++++++++ src/services/neynar/index.ts | 5 +- 10 files changed, 306 insertions(+), 40 deletions(-) create mode 100644 data.json create mode 100644 src/services/airstack/types.ts create mode 100644 src/services/airstack/utils.ts diff --git a/.gitignore b/.gitignore index f06235c..5b605fe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ node_modules dist +testPlayground.ts diff --git a/data.json b/data.json new file mode 100644 index 0000000..341252e --- /dev/null +++ b/data.json @@ -0,0 +1,34 @@ +{ + "Socials": { + "Social": [ + { + "userId": "11244", + "userAddressDetails": { + "addresses": [ + "0x92622c2d23fdf169f312c61aa47eba16b4c12e0b" + ], + "blockchain": "ethereum" + }, + "profileDisplayName": "BFG 🎩↑🌱", + "profileName": "bfg", + "userAddress": "0x92622c2d23fdf169f312c61aa47eba16b4c12e0b", + "connectedAddresses": [ + { + "address": "0xa2746b2a56f9886925c03af1d1e10b8b3dfbbe29", + "blockchain": "ethereum" + } + ], + "followerCount": 9084, + "followingCount": 930, + "profileImage": "https://res.cloudinary.com/merkle-manufactory/image/fetch/c_fill,f_gif,w_112,h_112/https://imagedelivery.net/BXluQx4ige9GuW0Ia56BHw/9834b977-ec60-45b4-16ba-533ad68b8200/original", + "isFarcasterPowerUser": true + } + ] + }, + "Following": { + "Following": null + }, + "Followedby": { + "Following": null + } +} \ No newline at end of file diff --git a/package.json b/package.json index 08ce0e3..95ad1a4 100644 --- a/package.json +++ b/package.json @@ -24,6 +24,7 @@ "vitest": "^2.0.5" }, "dependencies": { + "@airstack/node": "^0.0.7", "axios": "^1.7.3", "ky": "^1.5.0" } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 1a37247..718ac1c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -5,6 +5,9 @@ settings: excludeLinksFromLockfile: false dependencies: + '@airstack/node': + specifier: ^0.0.7 + version: 0.0.7 axios: specifier: ^1.7.3 version: 1.7.3 @@ -31,6 +34,14 @@ devDependencies: packages: + /@airstack/node@0.0.7: + resolution: {integrity: sha512-BATIO8FbSlJz8pkTW/Iwp6vty7l/DPYcvAce8TjZbBX8R3iAsIN9V+ffY7R2duGrrRlQJRg9zIr+G6JKG0HPow==} + engines: {node: '>=14.17.0'} + dependencies: + '@types/node': 14.14.30 + graphql: 16.7.1 + dev: false + /@ampproject/remapping@2.3.0: resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} engines: {node: '>=6.0.0'} @@ -888,6 +899,10 @@ packages: resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} dev: true + /@types/node@14.14.30: + resolution: {integrity: sha512-gUWhy8s45fQp4PqqKecsnOkdW0kt1IaKjgOIR3HPokkzTmQj9ji2wWFID5THu1MKrtO+d4s2lVrlEhXUsPXSvg==} + dev: false + /@types/node@22.1.0: resolution: {integrity: sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==} dependencies: @@ -1493,6 +1508,11 @@ packages: resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} dev: true + /graphql@16.7.1: + resolution: {integrity: sha512-DRYR9tf+UGU0KOsMcKAlXeFfX89UiiIZ0dRU3mR0yJfu6OjZqUcp68NnFLnqQU5RexygFoDy1EW+ccOYcPfmHg==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + dev: false + /has-flag@3.0.0: resolution: {integrity: sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==} engines: {node: '>=4'} diff --git a/src/index.ts b/src/index.ts index 8953b29..dc30229 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,6 +1,5 @@ import { User, Cast } from "./playground"; import { services, TService, Service } from "./services"; -import { neynarService } from "./services/neynar"; type Config = { hubUrl?: string; neynarApiKey?: string; @@ -24,9 +23,9 @@ class uniFarcasterSdk { //TODO: Make more composable private createService(service?: TService): Service { if (service === "neynar" && this.neynarApiKey) { - return new neynarService(this.neynarApiKey); + return new services.neynar(this.neynarApiKey); } else if (service === "airstack" && this.airstackApiKey) { - return services.airstack.init(this.airstackApiKey); + return new services.airstack(this.airstackApiKey); } else { return services.hub; } diff --git a/src/playground.ts b/src/playground.ts index c60b741..82e42e7 100644 --- a/src/playground.ts +++ b/src/playground.ts @@ -1,25 +1,18 @@ -// const uniFarcasterSdk: any = {}; +const uniFarcasterSdk: any = {}; -import uniFarcasterSdk from "."; const sdk = new uniFarcasterSdk({ hubUrl: "string | hasDefaultValue", - neynarApiKey: "NEYNAR_API_DOCS", - airstackApiKey: "string | undefined", - activeService: "neynar", + neynarApiKey: "NEYNAR_API_KEY", + airstackApiKey: "AIRSTACK_API_KEY", + activeService: "airstack", }); -// const user = await sdk.getUserByFid(11244, 213144); -// // console.log(user); -const cast = await sdk.getCastByUrl( - "https://warpcast.com/timdaub.eth/0x9d6f3c51", - 213144 -); -console.log(cast); -// console.log(user); -// const user2: User = sdk.getUserByUsername("complexlity", 213144); -// const cast: Cast = sdk.getCastByHash("0xa0bc828", 213144); -// const cast2: Cast = sdk.getCastByUrl("https://warpcast.com/0xa38dj", 213144); + +const user: User = sdk.getUserByUsername("complexlity", 213144); +const user2: User = sdk.getUserByFid(11244, 213144); +const cast: Cast = sdk.getCastByHash("0xa0bc828", 213144); +const cast2: Cast = sdk.getCastByUrl("https://warpcast.com/0xa38dj", 213144); export type User = { fid: number; @@ -32,7 +25,7 @@ export type User = { followerCount: number; followingCount: number; powerBadge?: boolean; - viewerContext: { + viewerContext?: { following: boolean; followedBy: boolean; }; @@ -48,10 +41,13 @@ export type Cast = { liked: boolean; recasted: boolean; }; + text: string; + embeds: any[]; + channel: string | null; }; -//Optional: Expose neynar and airstack for direct query when extra infor needed -// const endpoint = "/user?limit=2"; -// const query = `SOME_GRAPHQL_QUERY`; -// sdk.neynar(endpoint); -// sdk.airstack(query); +// Optional: Expose neynar and airstack for direct query when extra infor needed +const endpoint = "/user?limit=2"; +const query = `SOME_GRAPHQL_QUERY`; +sdk.neynar(endpoint); +sdk.airstack(query); diff --git a/src/services/airstack/index.ts b/src/services/airstack/index.ts index 94e3823..93856cb 100644 --- a/src/services/airstack/index.ts +++ b/src/services/airstack/index.ts @@ -1,30 +1,85 @@ import { Service } from ".."; import { User, Cast } from "../../playground"; +import { init, fetchQuery } from "@airstack/node"; +import { + castByHashQuery, + castByUrlQuery, + userByFidQuery, + userByUsernameQuery, +} from "./utils"; +import fs from "fs"; +import { AirstackUserFetchResult } from "./types"; -export const airstackService: Service = { - name: "airstack", +export class airstackService { + private apiKey: string; + constructor(apiKey: string) { + this.apiKey = apiKey; + init(this.apiKey); + } + + private getUserFromAirstackSociaslResult(data: AirstackUserFetchResult["Socials"]) { + const userDetails = data.Social[0]; + const userAddresses = this.getUserAddresses(userDetails.connectedAddresses); + const convertedUser: User = { + fid: Number(userDetails.userId), + username: userDetails.profileName, + displayName: userDetails.profileDisplayName, + pfpUrl: userDetails.profileImage, + followerCount: userDetails.followerCount, + followingCount: userDetails.followingCount, + powerBadge: userDetails.isFarcasterPowerUser, + bio: userDetails.profileBio, + ethAddresses: [...userAddresses.ethAddresses, userDetails.userAddress], + solAddresses: [...userAddresses.solAddresses], + }; + return convertedUser; + } + + private getUserAddresses( + userAddresses: { address: string; blockchain: string }[] + ) { + const ethAddresses: string[] = []; + const solAddresses: string[] = []; + for (const address of userAddresses) { + if (address.blockchain === "ethereum") { + ethAddresses.push(address.address); + } + if (address.blockchain === "solana") { + solAddresses.push(address.address); + } + } + return { ethAddresses, solAddresses }; + } async getUserByFid(fid: number, viewerFid: number): Promise { - return new Promise((resolve, reject) => { - reject("Not implemented"); - }); - }, + const query = userByFidQuery(fid, viewerFid); + const { data, error } = await fetchQuery(query); + const returnedData = data as AirstackUserFetchResult; + const user = this.getUserFromAirstackSociaslResult(returnedData.Socials); + const viewerContext = { + following: !!returnedData.Following.Following, + followedBy: !!returnedData.Followedby.Following, + } + user.viewerContext = viewerContext; + return user; + } - async getUserByUsername(username: string, viewerFid: number): Promise { - return new Promise((resolve, reject) => { - reject("Not implemented"); - }); - }, + async getUserByUsername(username: string): Promise { + const query = userByUsernameQuery(username); + const { data, error } = await fetchQuery(query); + const returnedData = data as Omit; + return this.getUserFromAirstackSociaslResult(returnedData.Socials); + } async getCastByHash(hash: string, viewerFid: number): Promise { return new Promise((resolve, reject) => { reject("Not implemented"); }); - }, + } async getCastByUrl(url: string, viewerFid: number): Promise { return new Promise((resolve, reject) => { reject("Not implemented"); }); - }, -}; + } +} diff --git a/src/services/airstack/types.ts b/src/services/airstack/types.ts new file mode 100644 index 0000000..0a8b9b9 --- /dev/null +++ b/src/services/airstack/types.ts @@ -0,0 +1,42 @@ +// ------------------------------------ + +// GENERATED TYPES FOR AIRSTACK QUERIES + +// ------------------------------------ + +export interface AirstackUserFetchResult { + Socials: Socials; + Following: Follow; + Followedby: Follow; +} + +export interface Follow { + Following: any; +} + +export interface Socials { + Social: Social[]; +} + +export interface Social { + userId: string; + userAddress: string; + profileDisplayName: string; + profileName: string; + connectedAddresses: ConnectedAddress[]; + followerCount: number; + followingCount: number; + profileImage: string; + profileBio: string; + isFarcasterPowerUser: boolean; +} + +export interface ConnectedAddress { + address: string; + blockchain: string; +} + +export interface UserAddressDetails { + addresses: string[]; + blockchain: string; +} diff --git a/src/services/airstack/utils.ts b/src/services/airstack/utils.ts new file mode 100644 index 0000000..645e863 --- /dev/null +++ b/src/services/airstack/utils.ts @@ -0,0 +1,115 @@ +const socialReturnedQuery = ` + userId + userAddress + profileBio + profileDisplayName + profileName + connectedAddresses { + address + blockchain + } + followerCount + followingCount + profileImage + isFarcasterPowerUser`; + +export const userByFidQuery = (fid: number, viewerFid: number) => `query MyQuery { + Socials(input: {filter: {userId: {_eq: "${fid}"}}, blockchain: ethereum}) { + Social { + ${socialReturnedQuery} + } + } + Following: SocialFollowings( + input: {filter: {dappName: {_eq: farcaster}, followingProfileId: {_eq: "${fid}"}, followerProfileId: {_eq: "${viewerFid}"}}, blockchain: ALL} + ) { + Following { + followerProfileId + } + } + Followedby: SocialFollowings( + input: {filter: {dappName: {_eq: farcaster}, followingProfileId: {_eq: "${viewerFid}"}, followerProfileId: {_eq: "${fid}"}}, blockchain: ALL} + ) { + Following { + followerProfileId + } + } +}`; + +export const userByUsernameQuery = (username: string) => + `query MyQuery { + Socials( + input: {filter: {profileName: {_eq: "${username}"}}, blockchain: ethereum} + ) { + Social { + ${socialReturnedQuery} + } + } +}`; + +export const castByHashQuery = (castHash: string, viewerFid: number) => + ` +query FetchCastAuthorLikedByAndReactedBy { + FarcasterCasts( + input: {filter: {hash: {_eq: "${castHash}"}}, blockchain: ALL} + ) { + Cast { + castedBy { + ${socialReturnedQuery} + } + } + } + LikedBy: FarcasterReactions( + input: {filter: {reactedBy: {_eq: "fc_fid:${viewerFid}"}, castHash: {_eq: "${castHash}"}, criteria: liked}, blockchain: ALL, limit: 1} + ) { + Reaction { + reactedBy { + userId + } + } + } + RecastedBy: FarcasterReactions( + input: {filter: {reactedBy: {_eq: "fc_fid:${viewerFid}"}, castHash: {_eq: "${castHash}"}, criteria: recasted}, blockchain: ALL, limit: 1} + ) { + Reaction { + reactedBy { + userId + } + } + } +`; +export const castByUrlQuery = (castUrl: string, viewerFid: number) => + ` +query FetchCastAuthorLikedByAndReactedBy { + FarcasterCasts( + input: {filter: {url: {_eq: "${castUrl}"}}, blockchain: ALL} + ) { + Cast { + embeds + text + channel { + name + } + castedBy { + + } + } + } + LikedBy: FarcasterReactions( + input: {filter: {reactedBy: {_eq: "fc_fid:${viewerFid}"}, castHash: {_eq: "${castUrl}"}, criteria: liked}, blockchain: ALL, limit: 1} + ) { + Reaction { + reactedBy { + userId + } + } + } + RecastedBy: FarcasterReactions( + input: {filter: {reactedBy: {_eq: "fc_fid:${viewerFid}"}, castHash: {_eq: "${castUrl}"}, criteria: recasted}, blockchain: ALL, limit: 1} + ) { + Reaction { + reactedBy { + userId + } + } + } +`; \ No newline at end of file diff --git a/src/services/neynar/index.ts b/src/services/neynar/index.ts index 7217c87..5ea2a6d 100644 --- a/src/services/neynar/index.ts +++ b/src/services/neynar/index.ts @@ -59,7 +59,10 @@ export class neynarService { viewerContext: { liked: cast.viewer_context.liked, recasted: cast.viewer_context.recasted, - } + }, + text: cast.text, + embeds: cast.embeds, + channel: cast.channel } } async getUserByFid(fid: number, viewerFid: number = 1): Promise {