diff --git a/.changeset/cool-ties-accept.md b/.changeset/cool-ties-accept.md new file mode 100644 index 00000000..ba2df775 --- /dev/null +++ b/.changeset/cool-ties-accept.md @@ -0,0 +1,5 @@ +--- +"frames.js": patch +--- + +feat: support location user data type diff --git a/packages/frames.js/src/farcaster/generated/message.ts b/packages/frames.js/src/farcaster/generated/message.ts index 1f5da55b..b188dbf5 100644 --- a/packages/frames.js/src/farcaster/generated/message.ts +++ b/packages/frames.js/src/farcaster/generated/message.ts @@ -110,6 +110,8 @@ export enum MessageType { USERNAME_PROOF = 12, /** FRAME_ACTION - A Farcaster Frame action */ FRAME_ACTION = 13, + /** LINK_COMPACT_STATE - Link Compaction State Message */ + LINK_COMPACT_STATE = 14, } export function messageTypeFromJSON(object: any): MessageType { @@ -150,6 +152,9 @@ export function messageTypeFromJSON(object: any): MessageType { case 13: case "MESSAGE_TYPE_FRAME_ACTION": return MessageType.FRAME_ACTION; + case 14: + case "MESSAGE_TYPE_LINK_COMPACT_STATE": + return MessageType.LINK_COMPACT_STATE; default: throw new tsProtoGlobalThis.Error( "Unrecognized enum value " + object + " for enum MessageType" @@ -183,6 +188,8 @@ export function messageTypeToJSON(object: MessageType): string { return "MESSAGE_TYPE_USERNAME_PROOF"; case MessageType.FRAME_ACTION: return "MESSAGE_TYPE_FRAME_ACTION"; + case MessageType.LINK_COMPACT_STATE: + return "MESSAGE_TYPE_LINK_COMPACT_STATE"; default: throw new tsProtoGlobalThis.Error( "Unrecognized enum value " + object + " for enum MessageType" @@ -252,6 +259,8 @@ export enum UserDataType { URL = 5, /** USERNAME - Preferred Name for the user */ USERNAME = 6, + /** LOCATION - Current location for the user */ + LOCATION = 7, } export function userDataTypeFromJSON(object: any): UserDataType { @@ -274,6 +283,9 @@ export function userDataTypeFromJSON(object: any): UserDataType { case 6: case "USER_DATA_TYPE_USERNAME": return UserDataType.USERNAME; + case 7: + case "USER_DATA_TYPE_LOCATION": + return UserDataType.LOCATION; default: throw new tsProtoGlobalThis.Error( "Unrecognized enum value " + object + " for enum UserDataType" @@ -295,6 +307,8 @@ export function userDataTypeToJSON(object: UserDataType): string { return "USER_DATA_TYPE_URL"; case UserDataType.USERNAME: return "USER_DATA_TYPE_USERNAME"; + case UserDataType.LOCATION: + return "USER_DATA_TYPE_LOCATION"; default: throw new tsProtoGlobalThis.Error( "Unrecognized enum value " + object + " for enum UserDataType" @@ -302,6 +316,40 @@ export function userDataTypeToJSON(object: UserDataType): string { } } +/** Type of cast */ +export enum CastType { + CAST = 0, + LONG_CAST = 1, +} + +export function castTypeFromJSON(object: any): CastType { + switch (object) { + case 0: + case "CAST": + return CastType.CAST; + case 1: + case "LONG_CAST": + return CastType.LONG_CAST; + default: + throw new tsProtoGlobalThis.Error( + "Unrecognized enum value " + object + " for enum CastType" + ); + } +} + +export function castTypeToJSON(object: CastType): string { + switch (object) { + case CastType.CAST: + return "CAST"; + case CastType.LONG_CAST: + return "LONG_CAST"; + default: + throw new tsProtoGlobalThis.Error( + "Unrecognized enum value " + object + " for enum CastType" + ); + } +} + /** Type of Reaction */ export enum ReactionType { NONE = 0, @@ -423,6 +471,8 @@ export interface MessageData { linkBody?: LinkBody | undefined; usernameProofBody?: UserNameProof | undefined; frameActionBody?: FrameActionBody | undefined; + /** Compaction messages */ + linkCompactStateBody?: LinkCompactStateBody | undefined; } /** Adds metadata about a user */ @@ -454,6 +504,8 @@ export interface CastAddBody { mentionsPositions: number[]; /** URLs or cast ids to be embedded in the cast */ embeds: Embed[]; + /** Type of cast */ + type: CastType; } /** Removes an existing Cast */ @@ -514,6 +566,13 @@ export interface LinkBody { targetFid?: number | undefined; } +/** A Compaction message for the Link Store */ +export interface LinkCompactStateBody { + /** Type of link, <= 8 characters */ + type: string; + targetFids: number[]; +} + /** A Farcaster Frame action */ export interface FrameActionBody { /** URL of the Frame triggering the action */ @@ -726,6 +785,7 @@ function createBaseMessageData(): MessageData { linkBody: undefined, usernameProofBody: undefined, frameActionBody: undefined, + linkCompactStateBody: undefined, }; } @@ -797,6 +857,12 @@ export const MessageData = { writer.uint32(130).fork() ).ldelim(); } + if (message.linkCompactStateBody !== undefined) { + LinkCompactStateBody.encode( + message.linkCompactStateBody, + writer.uint32(138).fork() + ).ldelim(); + } return writer; }, @@ -912,6 +978,16 @@ export const MessageData = { reader.uint32() ); continue; + case 17: + if (tag != 138) { + break; + } + + message.linkCompactStateBody = LinkCompactStateBody.decode( + reader, + reader.uint32() + ); + continue; } if ((tag & 7) == 4 || tag == 0) { break; @@ -956,6 +1032,9 @@ export const MessageData = { frameActionBody: isSet(object.frameActionBody) ? FrameActionBody.fromJSON(object.frameActionBody) : undefined, + linkCompactStateBody: isSet(object.linkCompactStateBody) + ? LinkCompactStateBody.fromJSON(object.linkCompactStateBody) + : undefined, }; }, @@ -1003,6 +1082,10 @@ export const MessageData = { (obj.frameActionBody = message.frameActionBody ? FrameActionBody.toJSON(message.frameActionBody) : undefined); + message.linkCompactStateBody !== undefined && + (obj.linkCompactStateBody = message.linkCompactStateBody + ? LinkCompactStateBody.toJSON(message.linkCompactStateBody) + : undefined); return obj; }, @@ -1059,6 +1142,11 @@ export const MessageData = { object.frameActionBody !== undefined && object.frameActionBody !== null ? FrameActionBody.fromPartial(object.frameActionBody) : undefined; + message.linkCompactStateBody = + object.linkCompactStateBody !== undefined && + object.linkCompactStateBody !== null + ? LinkCompactStateBody.fromPartial(object.linkCompactStateBody) + : undefined; return message; }, }; @@ -1227,6 +1315,7 @@ function createBaseCastAddBody(): CastAddBody { text: "", mentionsPositions: [], embeds: [], + type: 0, }; } @@ -1260,6 +1349,9 @@ export const CastAddBody = { for (const v of message.embeds) { Embed.encode(v!, writer.uint32(50).fork()).ldelim(); } + if (message.type !== 0) { + writer.uint32(64).int32(message.type); + } return writer; }, @@ -1338,6 +1430,13 @@ export const CastAddBody = { message.embeds.push(Embed.decode(reader, reader.uint32())); continue; + case 8: + if (tag != 64) { + break; + } + + message.type = reader.int32() as any; + continue; } if ((tag & 7) == 4 || tag == 0) { break; @@ -1366,6 +1465,7 @@ export const CastAddBody = { embeds: Array.isArray(object?.embeds) ? object.embeds.map((e: any) => Embed.fromJSON(e)) : [], + type: isSet(object.type) ? castTypeFromJSON(object.type) : 0, }; }, @@ -1399,6 +1499,7 @@ export const CastAddBody = { } else { obj.embeds = []; } + message.type !== undefined && (obj.type = castTypeToJSON(message.type)); return obj; }, @@ -1420,6 +1521,7 @@ export const CastAddBody = { message.text = object.text ?? ""; message.mentionsPositions = object.mentionsPositions?.map((e) => e) || []; message.embeds = object.embeds?.map((e) => Embed.fromPartial(e)) || []; + message.type = object.type ?? 0; return message; }, }; @@ -2018,6 +2120,105 @@ export const LinkBody = { }, }; +function createBaseLinkCompactStateBody(): LinkCompactStateBody { + return { type: "", targetFids: [] }; +} + +export const LinkCompactStateBody = { + encode( + message: LinkCompactStateBody, + writer: _m0.Writer = _m0.Writer.create() + ): _m0.Writer { + if (message.type !== "") { + writer.uint32(10).string(message.type); + } + writer.uint32(18).fork(); + for (const v of message.targetFids) { + writer.uint64(v); + } + writer.ldelim(); + return writer; + }, + + decode( + input: _m0.Reader | Uint8Array, + length?: number + ): LinkCompactStateBody { + const reader = + input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseLinkCompactStateBody(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag != 10) { + break; + } + + message.type = reader.string(); + continue; + case 2: + if (tag == 16) { + message.targetFids.push(longToNumber(reader.uint64() as Long)); + continue; + } + + if (tag == 18) { + const end2 = reader.uint32() + reader.pos; + while (reader.pos < end2) { + message.targetFids.push(longToNumber(reader.uint64() as Long)); + } + + continue; + } + + break; + } + if ((tag & 7) == 4 || tag == 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): LinkCompactStateBody { + return { + type: isSet(object.type) ? String(object.type) : "", + targetFids: Array.isArray(object?.targetFids) + ? object.targetFids.map((e: any) => Number(e)) + : [], + }; + }, + + toJSON(message: LinkCompactStateBody): unknown { + const obj: any = {}; + message.type !== undefined && (obj.type = message.type); + if (message.targetFids) { + obj.targetFids = message.targetFids.map((e) => Math.round(e)); + } else { + obj.targetFids = []; + } + return obj; + }, + + create, I>>( + base?: I + ): LinkCompactStateBody { + return LinkCompactStateBody.fromPartial(base ?? {}); + }, + + fromPartial, I>>( + object: I + ): LinkCompactStateBody { + const message = createBaseLinkCompactStateBody(); + message.type = object.type ?? ""; + message.targetFids = object.targetFids?.map((e) => e) || []; + return message; + }, +}; + function createBaseFrameActionBody(): FrameActionBody { return { url: new Uint8Array(), @@ -2256,12 +2457,12 @@ type Builtin = type DeepPartial = T extends Builtin ? T : T extends Array - ? Array> - : T extends ReadonlyArray - ? ReadonlyArray> - : T extends {} - ? { [K in keyof T]?: DeepPartial } - : Partial; + ? Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : T extends {} + ? { [K in keyof T]?: DeepPartial } + : Partial; type KeysOfUnion = T extends T ? keyof T : never; type Exact = P extends Builtin diff --git a/packages/frames.js/src/getUserDataForFid.test.ts b/packages/frames.js/src/getUserDataForFid.test.ts index ed633968..921adb05 100644 --- a/packages/frames.js/src/getUserDataForFid.test.ts +++ b/packages/frames.js/src/getUserDataForFid.test.ts @@ -159,4 +159,55 @@ describe("getUserDataForFid", () => { 'Failed to parse response body as JSON because server hub returned response with status "504" and body "Gateway Timeout"' ); }); + + it("warns if message could not be parsed", async () => { + nock(DEFAULT_HUB_API_URL) + .get("/v1/userDataByFid?fid=1214") + .reply(200, { + messages: [ + { + data: { + type: "MESSAGE_TYPE_USER_DATA_ADD", + fid: 1214, + timestamp: 69403426, + network: "FARCASTER_NETWORK_MAINNET", + userDataBody: { + type: "USER_DATA_TYPE_SOMETHING_UNKNOWN", + value: + "https://lh3.googleusercontent.com/-S5cdhOpZtJ_Qzg9iPWELEsRTkIsZ7qGYmVlwEORgFB00WWAtZGefRnS4Bjcz5ah40WVOOWeYfU5pP9Eekikb3cLMW2mZQOMQHlWhg", + }, + }, + hash: "0x465e44c9d8b4f6189d40b79029168a1dc0b50ea5", + hashScheme: "HASH_SCHEME_BLAKE3", + signature: + "x4J7Lo4FM7wutYYomV7ItFSOlo3Rca4s+BQ5rQK0dvRpIrsCCX7BU5fnkX3UfseXTyh4kJOVYmeEhLeeA27oAw==", + signatureScheme: "SIGNATURE_SCHEME_ED25519", + signer: + "0xf23a5c7b9f067c621a989a585b78daf7b2a9debe9d54325ef95b0878f44204c6", + }, + ], + }); + + const consoleWarnSpy = jest.spyOn(console, "warn").mockImplementation(); + + const result = await getUserDataForFid({ fid: 1214 }); + + expect(consoleWarnSpy).toHaveBeenCalled(); + expect(consoleWarnSpy).toHaveBeenCalledWith( + "Failed to parse user data message for fid 1214", + expect.any(Object), + expect.any(Error) + ); + + expect(result).toBeDefined(); + if (!result) { + throw new Error("Result is null"); + } + + const allValuesUndefined = Object.values(result).every( + (value) => value === undefined + ); + + expect(allValuesUndefined).toBe(true); + }); }); diff --git a/packages/frames.js/src/getUserDataForFid.ts b/packages/frames.js/src/getUserDataForFid.ts index ba81b58f..9d0af3b4 100644 --- a/packages/frames.js/src/getUserDataForFid.ts +++ b/packages/frames.js/src/getUserDataForFid.ts @@ -6,7 +6,7 @@ import type { HubHttpUrlOptions, UserDataReturnType } from "./types"; * Returns the latest user data for a given Farcaster users Fid if available. */ export async function getUserDataForFid< - Options extends HubHttpUrlOptions | undefined, + Options extends HubHttpUrlOptions | undefined >({ fid, options = {}, @@ -34,7 +34,9 @@ export async function getUserDataForFid< .catch(async () => { // body has not been throw new Error( - `Failed to parse response body as JSON because server hub returned response with status "${userDataResponse.status}" and body "${await userDataResponse.clone().text()}"` + `Failed to parse response body as JSON because server hub returned response with status "${ + userDataResponse.status + }" and body "${await userDataResponse.clone().text()}"` ); })) as { messages?: Record[] }; @@ -42,24 +44,33 @@ export async function getUserDataForFid< const valuesByType = messages.reduce< Partial> >((acc, messageJson) => { - const message = Message.fromJSON(messageJson); + try { + const message = Message.fromJSON(messageJson); - if (message.data?.type !== MessageType.USER_DATA_ADD) { - return acc; - } + if (message.data?.type !== MessageType.USER_DATA_ADD) { + return acc; + } - if (!message.data.userDataBody) { - return acc; - } + if (!message.data.userDataBody) { + return acc; + } - const timestamp = message.data.timestamp; - const { type, value } = message.data.userDataBody; - const foundValue = acc[type]; + const timestamp = message.data.timestamp; + const { type, value } = message.data.userDataBody; + const foundValue = acc[type]; - if (foundValue && foundValue.timestamp < timestamp) { - acc[type] = { value, timestamp }; - } else { - acc[type] = { value, timestamp }; + if (foundValue && foundValue.timestamp < timestamp) { + acc[type] = { value, timestamp }; + } else { + acc[type] = { value, timestamp }; + } + } catch (error) { + // eslint-disable-next-line no-console -- provide feedback to user + console.warn( + `Failed to parse user data message for fid ${fid}`, + messageJson, + error + ); } return acc; @@ -70,6 +81,7 @@ export async function getUserDataForFid< displayName: valuesByType[UserDataType.DISPLAY]?.value, username: valuesByType[UserDataType.USERNAME]?.value, bio: valuesByType[UserDataType.BIO]?.value, + location: valuesByType[UserDataType.LOCATION]?.value, }; } diff --git a/packages/frames.js/src/types.ts b/packages/frames.js/src/types.ts index 9c1ded37..b07bb075 100644 --- a/packages/frames.js/src/types.ts +++ b/packages/frames.js/src/types.ts @@ -44,8 +44,11 @@ type FrameOptionalStringKeys = | keyof OpenFramesProperties | "frames.js:version" | "frames.js:debug-info:image"; -type FrameOptionalActionButtonTypeKeys = - `fc:frame:button:${1 | 2 | 3 | 4}:action`; +type FrameOptionalActionButtonTypeKeys = `fc:frame:button:${ + | 1 + | 2 + | 3 + | 4}:action`; type FrameOptionalButtonStringKeys = | `fc:frame:button:${1 | 2 | 3 | 4}` | `fc:frame:button:${1 | 2 | 3 | 4}:target` @@ -59,8 +62,8 @@ type MapFrameOptionalKeyToValueType = K extends FrameOptionalStringKeys ? string | undefined : K extends FrameOptionalActionButtonTypeKeys - ? ActionButtonType | undefined - : string | undefined; + ? ActionButtonType | undefined + : string | undefined; type FrameRequiredProperties = { "fc:frame": FrameVersion; @@ -153,7 +156,7 @@ export type FrameButtonsType = | [FrameButton, FrameButton, FrameButton, FrameButton]; export type AddressReturnType< - Options extends { fallbackToCustodyAddress?: boolean } | undefined, + Options extends { fallbackToCustodyAddress?: boolean } | undefined > = Options extends { fallbackToCustodyAddress: true } ? `0x${string}` : `0x${string}` | null; @@ -214,6 +217,7 @@ export type UserDataReturnType = { username?: string; bio?: string; profileImage?: string; + location?: string; } | null; export type FrameActionDataParsedAndHubContext = FrameActionDataParsed &