Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Farcaster Client #386

Merged
merged 11 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,12 @@ STARKNET_ADDRESS=
STARKNET_PRIVATE_KEY=
STARKNET_RPC_URL=


# Farcaster
FARCASTER_HUB_URL=
FARCASTER_FID=
FARCASTER_PRIVATE_KEY=

# Coinbase
COINBASE_COMMERCE_KEY= # from coinbase developer portal
COINBASE_API_KEY= # from coinbase developer portal
Expand All @@ -112,6 +118,7 @@ ZEROG_EVM_RPC=
ZEROG_PRIVATE_KEY=
ZEROG_FLOW_ADDRESS=


# Coinbase Commerce
COINBASE_COMMERCE_KEY=

20 changes: 20 additions & 0 deletions packages/client-farcaster/package.json
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": {}
}
79 changes: 79 additions & 0 deletions packages/client-farcaster/src/actions.ts
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,
}),
}));
}
193 changes: 193 additions & 0 deletions packages/client-farcaster/src/client.ts
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,
};
}
}
58 changes: 58 additions & 0 deletions packages/client-farcaster/src/index.ts
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()]);
}
}
Loading