-
Notifications
You must be signed in to change notification settings - Fork 4.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request #386 from bmgalego/client-farcaster
feat: Farcaster Client
- Loading branch information
Showing
14 changed files
with
1,200 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
{ | ||
"name": "@ai16z/client-farcaster", | ||
"version": "0.0.1", | ||
"main": "dist/index.js", | ||
"type": "module", | ||
"types": "dist/index.d.ts", | ||
"dependencies": { | ||
"@ai16z/eliza": "workspace:*", | ||
"@farcaster/hub-nodejs": "^0.12.7", | ||
"viem": "^2.21.47" | ||
}, | ||
"devDependencies": { | ||
"tsup": "^8.3.5" | ||
}, | ||
"scripts": { | ||
"build": "tsup --format esm --dts", | ||
"dev": "tsup --watch" | ||
}, | ||
"peerDependencies": {} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,79 @@ | ||
import { CastId, FarcasterNetwork, Signer } from "@farcaster/hub-nodejs"; | ||
import { CastType, makeCastAdd } from "@farcaster/hub-nodejs"; | ||
import type { FarcasterClient } from "./client"; | ||
import type { Content, IAgentRuntime, Memory, UUID } from "@ai16z/eliza"; | ||
import type { Cast, Profile } from "./types"; | ||
import { createCastMemory } from "./memory"; | ||
import { splitPostContent } from "./utils"; | ||
|
||
export async function sendCast({ | ||
client, | ||
runtime, | ||
content, | ||
roomId, | ||
inReplyTo, | ||
signer, | ||
profile, | ||
}: { | ||
profile: Profile; | ||
client: FarcasterClient; | ||
runtime: IAgentRuntime; | ||
content: Content; | ||
roomId: UUID; | ||
signer: Signer; | ||
inReplyTo?: CastId; | ||
}): Promise<{ memory: Memory; cast: Cast }[]> { | ||
const chunks = splitPostContent(content.text); | ||
const sent: Cast[] = []; | ||
let parentCastId = inReplyTo; | ||
|
||
for (const chunk of chunks) { | ||
const castAddMessageResult = await makeCastAdd( | ||
{ | ||
text: chunk, | ||
embeds: [], | ||
embedsDeprecated: [], | ||
mentions: [], | ||
mentionsPositions: [], | ||
type: CastType.CAST, // TODO: check CastType.LONG_CAST | ||
parentCastId, | ||
}, | ||
{ | ||
fid: profile.fid, | ||
network: FarcasterNetwork.MAINNET, | ||
}, | ||
signer | ||
); | ||
|
||
if (castAddMessageResult.isErr()) { | ||
throw castAddMessageResult.error; | ||
} | ||
|
||
await client.submitMessage(castAddMessageResult.value); | ||
|
||
const cast = await client.loadCastFromMessage( | ||
castAddMessageResult.value | ||
); | ||
|
||
sent.push(cast); | ||
|
||
parentCastId = { | ||
fid: cast.profile.fid, | ||
hash: cast.message.hash, | ||
}; | ||
|
||
// TODO: check rate limiting | ||
// Wait a bit between tweets to avoid rate limiting issues | ||
// await wait(1000, 2000); | ||
} | ||
|
||
return sent.map((cast) => ({ | ||
cast, | ||
memory: createCastMemory({ | ||
roomId, | ||
agentId: runtime.agentId, | ||
userId: runtime.agentId, | ||
cast, | ||
}), | ||
})); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,193 @@ | ||
import { IAgentRuntime } from "@ai16z/eliza"; | ||
import { | ||
CastAddMessage, | ||
CastId, | ||
FidRequest, | ||
getInsecureHubRpcClient, | ||
getSSLHubRpcClient, | ||
HubRpcClient, | ||
isCastAddMessage, | ||
isUserDataAddMessage, | ||
Message, | ||
MessagesResponse, | ||
} from "@farcaster/hub-nodejs"; | ||
import { Cast, Profile } from "./types"; | ||
import { toHex } from "viem"; | ||
import { populateMentions } from "./utils"; | ||
|
||
export class FarcasterClient { | ||
runtime: IAgentRuntime; | ||
farcaster: HubRpcClient; | ||
|
||
cache: Map<string, any>; | ||
|
||
constructor(opts: { | ||
runtime: IAgentRuntime; | ||
url: string; | ||
ssl: boolean; | ||
cache: Map<string, any>; | ||
}) { | ||
this.cache = opts.cache; | ||
this.runtime = opts.runtime; | ||
this.farcaster = opts.ssl | ||
? getSSLHubRpcClient(opts.url) | ||
: getInsecureHubRpcClient(opts.url); | ||
} | ||
|
||
async submitMessage(cast: Message, retryTimes?: number): Promise<void> { | ||
const result = await this.farcaster.submitMessage(cast); | ||
|
||
if (result.isErr()) { | ||
throw result.error; | ||
} | ||
} | ||
|
||
async loadCastFromMessage(message: CastAddMessage): Promise<Cast> { | ||
const profileMap = {}; | ||
|
||
const profile = await this.getProfile(message.data.fid); | ||
|
||
profileMap[message.data.fid] = profile; | ||
|
||
for (const mentionId of message.data.castAddBody.mentions) { | ||
if (profileMap[mentionId]) continue; | ||
profileMap[mentionId] = await this.getProfile(mentionId); | ||
} | ||
|
||
const text = populateMentions( | ||
message.data.castAddBody.text, | ||
message.data.castAddBody.mentions, | ||
message.data.castAddBody.mentionsPositions, | ||
profileMap | ||
); | ||
|
||
return { | ||
id: toHex(message.hash), | ||
message, | ||
text, | ||
profile, | ||
}; | ||
} | ||
|
||
async getCast(castId: CastId): Promise<Message> { | ||
const castHash = toHex(castId.hash); | ||
|
||
if (this.cache.has(`farcaster/cast/${castHash}`)) { | ||
return this.cache.get(`farcaster/cast/${castHash}`); | ||
} | ||
|
||
const cast = await this.farcaster.getCast(castId); | ||
|
||
if (cast.isErr()) { | ||
throw cast.error; | ||
} | ||
|
||
this.cache.set(`farcaster/cast/${castHash}`, cast); | ||
|
||
return cast.value; | ||
} | ||
|
||
async getCastsByFid(request: FidRequest): Promise<MessagesResponse> { | ||
const cast = await this.farcaster.getCastsByFid(request); | ||
if (cast.isErr()) { | ||
throw cast.error; | ||
} | ||
|
||
cast.value.messages.map((cast) => { | ||
this.cache.set(`farcaster/cast/${toHex(cast.hash)}`, cast); | ||
}); | ||
|
||
return cast.value; | ||
} | ||
|
||
async getMentions(request: FidRequest): Promise<MessagesResponse> { | ||
const cast = await this.farcaster.getCastsByMention(request); | ||
if (cast.isErr()) { | ||
throw cast.error; | ||
} | ||
|
||
cast.value.messages.map((cast) => { | ||
this.cache.set(`farcaster/cast/${toHex(cast.hash)}`, cast); | ||
}); | ||
|
||
return cast.value; | ||
} | ||
|
||
async getProfile(fid: number): Promise<Profile> { | ||
if (this.cache.has(`farcaster/profile/${fid}`)) { | ||
return this.cache.get(`farcaster/profile/${fid}`) as Profile; | ||
} | ||
|
||
const result = await this.farcaster.getUserDataByFid({ | ||
fid: fid, | ||
reverse: true, | ||
}); | ||
|
||
if (result.isErr()) { | ||
throw result.error; | ||
} | ||
|
||
const profile: Profile = { | ||
fid, | ||
name: "", | ||
signer: "0x", | ||
username: "", | ||
}; | ||
|
||
const userDataBodyType = { | ||
1: "pfp", | ||
2: "name", | ||
3: "bio", | ||
5: "url", | ||
6: "username", | ||
// 7: "location", | ||
// 8: "twitter", | ||
// 9: "github", | ||
} as const; | ||
|
||
for (const message of result.value.messages) { | ||
if (isUserDataAddMessage(message)) { | ||
if (message.data.userDataBody.type in userDataBodyType) { | ||
const prop = | ||
userDataBodyType[message.data.userDataBody.type]; | ||
profile[prop] = message.data.userDataBody.value; | ||
} | ||
} | ||
} | ||
|
||
const [lastMessage] = result.value.messages; | ||
|
||
if (lastMessage) { | ||
profile.signer = toHex(lastMessage.signer); | ||
} | ||
|
||
this.cache.set(`farcaster/profile/${fid}`, profile); | ||
|
||
return profile; | ||
} | ||
|
||
async getTimeline(request: FidRequest): Promise<{ | ||
timeline: Cast[]; | ||
nextPageToken?: Uint8Array<ArrayBufferLike> | undefined; | ||
}> { | ||
const timeline: Cast[] = []; | ||
|
||
const results = await this.getCastsByFid(request); | ||
|
||
for (const message of results.messages) { | ||
if (isCastAddMessage(message)) { | ||
this.cache.set( | ||
`farcaster/cast/${toHex(message.hash)}`, | ||
message | ||
); | ||
|
||
timeline.push(await this.loadCastFromMessage(message)); | ||
} | ||
} | ||
|
||
return { | ||
timeline, | ||
nextPageToken: results.nextPageToken, | ||
}; | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,58 @@ | ||
import { Client, IAgentRuntime } from "@ai16z/eliza"; | ||
import { Signer, NobleEd25519Signer } from "@farcaster/hub-nodejs"; | ||
import { Hex, hexToBytes } from "viem"; | ||
import { FarcasterClient } from "./client"; | ||
import { FarcasterPostManager } from "./post"; | ||
import { FarcasterInteractionManager } from "./interactions"; | ||
|
||
export class FarcasterAgentClient implements Client { | ||
client: FarcasterClient; | ||
posts: FarcasterPostManager; | ||
interactions: FarcasterInteractionManager; | ||
|
||
private signer: Signer; | ||
|
||
constructor( | ||
public runtime: IAgentRuntime, | ||
client?: FarcasterClient | ||
) { | ||
const cache = new Map<string, any>(); | ||
|
||
this.signer = new NobleEd25519Signer( | ||
hexToBytes(runtime.getSetting("FARCASTER_PRIVATE_KEY")! as Hex) | ||
); | ||
|
||
this.client = | ||
client ?? | ||
new FarcasterClient({ | ||
runtime, | ||
ssl: true, | ||
url: | ||
runtime.getSetting("FARCASTER_HUB_URL") ?? | ||
"hub.pinata.cloud", | ||
cache, | ||
}); | ||
|
||
this.posts = new FarcasterPostManager( | ||
this.client, | ||
this.runtime, | ||
this.signer, | ||
cache | ||
); | ||
|
||
this.interactions = new FarcasterInteractionManager( | ||
this.client, | ||
this.runtime, | ||
this.signer, | ||
cache | ||
); | ||
} | ||
|
||
async start() { | ||
await Promise.all([this.posts.start(), this.interactions.start()]); | ||
} | ||
|
||
async stop() { | ||
await Promise.all([this.posts.stop(), this.interactions.stop()]); | ||
} | ||
} |
Oops, something went wrong.