Skip to content

Commit

Permalink
Merge pull request #386 from bmgalego/client-farcaster
Browse files Browse the repository at this point in the history
feat: Farcaster Client
  • Loading branch information
ponderingdemocritus authored Nov 25, 2024
2 parents 3fbad4b + 6db8eac commit 0355ab6
Show file tree
Hide file tree
Showing 14 changed files with 1,200 additions and 0 deletions.
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

0 comments on commit 0355ab6

Please sign in to comment.