From 244b541ceab852a806a4df21118b50c469ad408d Mon Sep 17 00:00:00 2001 From: Aditi Srinivasan Date: Mon, 23 Sep 2024 20:59:17 -0400 Subject: [PATCH 1/9] wip --- packages/core/src/protobufs/generated/message.ts | 7 +++++++ packages/hub-nodejs/src/generated/message.ts | 7 +++++++ packages/hub-web/src/generated/message.ts | 7 +++++++ protobufs/schemas/message.proto | 1 + 4 files changed, 22 insertions(+) diff --git a/packages/core/src/protobufs/generated/message.ts b/packages/core/src/protobufs/generated/message.ts index 3bc697599b..fb3b7ad783 100644 --- a/packages/core/src/protobufs/generated/message.ts +++ b/packages/core/src/protobufs/generated/message.ts @@ -242,6 +242,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 { @@ -264,6 +266,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"); } @@ -283,6 +288,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"); } diff --git a/packages/hub-nodejs/src/generated/message.ts b/packages/hub-nodejs/src/generated/message.ts index 3bc697599b..fb3b7ad783 100644 --- a/packages/hub-nodejs/src/generated/message.ts +++ b/packages/hub-nodejs/src/generated/message.ts @@ -242,6 +242,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 { @@ -264,6 +266,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"); } @@ -283,6 +288,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"); } diff --git a/packages/hub-web/src/generated/message.ts b/packages/hub-web/src/generated/message.ts index 3bc697599b..fb3b7ad783 100644 --- a/packages/hub-web/src/generated/message.ts +++ b/packages/hub-web/src/generated/message.ts @@ -242,6 +242,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 { @@ -264,6 +266,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"); } @@ -283,6 +288,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"); } diff --git a/protobufs/schemas/message.proto b/protobufs/schemas/message.proto index b5f10d44f5..1ab1651345 100644 --- a/protobufs/schemas/message.proto +++ b/protobufs/schemas/message.proto @@ -97,6 +97,7 @@ enum UserDataType { USER_DATA_TYPE_BIO = 3; // Bio for the user USER_DATA_TYPE_URL = 5; // URL of the user USER_DATA_TYPE_USERNAME = 6; // Preferred Name for the user + USER_DATA_TYPE_LOCATION = 7; // Current location for the user } message Embed { From a0cea3e8c27cc4aedad24de1b5fbe2acdf1cb3dd Mon Sep 17 00:00:00 2001 From: Aditi Srinivasan Date: Tue, 1 Oct 2024 11:30:28 -0400 Subject: [PATCH 2/9] add validations --- packages/core/src/validations.ts | 55 ++++++++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/packages/core/src/validations.ts b/packages/core/src/validations.ts index c9fa002514..ad6659b126 100644 --- a/packages/core/src/validations.ts +++ b/packages/core/src/validations.ts @@ -110,6 +110,61 @@ export const validateMessageHash = (hash?: Uint8Array): HubResult => return ok(hash); }; +const validateLatitudeLongitude = (value: string) => { + const [_, decimals] = value.split("."); + if (decimals?.length !== 2) { + return err(new HubError("bad_request.validation_failure", "Wrong precision for latitude/longitude")); + } + + return ok(value); +}; + +export const validateUserLocation = (location: string) => { + if (!location.startsWith("geo:")) { + return err(new HubError("bad_request.validation_failure", "Invalid Geo URI")); + } + + const geoString = location.substring(4); + + // Split on the semicolon to handle any optional parameters + const [coords, ...params] = geoString.split(";"); + + if (params.length > 0) { + return err(new HubError("bad_request.validation_failure", "Extra parameters in Geo URI not supported")); + } + + if (coords === undefined) { + return err(new HubError("bad_request.validation_failure", "Invalid Geo URI. No coordinates provided")); + } + + // Split the coordinates (latitude, longitude, altitude) + const coordParts = coords.split(","); + + if (coordParts === undefined || coordParts.length < 2) { + return err(new HubError("bad_request.validation_failure", "Invalid coordinates in Geo URI")); + } + + if (coordParts[0] === undefined) { + return err(new HubError("bad_request.validation_failure", "Geo URI missing latitude")); + } + + const latitude = validateLatitudeLongitude(coordParts[0]); + if (latitude.isErr()) { + return err(latitude.error); + } + + if (coordParts[1] === undefined) { + return err(new HubError("bad_request.validation_failure", "Geo URI missing longitude")); + } + + const longitude = validateLatitudeLongitude(coordParts[1]); + if (longitude.isErr()) { + return err(longitude.error); + } + + return ok(location); +}; + export const validateCastId = (castId?: protobufs.CastId): HubResult => { if (!castId) { return err(new HubError("bad_request.validation_failure", "castId is missing")); From 38ac0befebae00b8934dcd36a8026932c8010044 Mon Sep 17 00:00:00 2001 From: Aditi Srinivasan Date: Tue, 1 Oct 2024 14:27:56 -0400 Subject: [PATCH 3/9] wip --- .../src/rpc/test/userDataService.test.ts | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/apps/hubble/src/rpc/test/userDataService.test.ts b/apps/hubble/src/rpc/test/userDataService.test.ts index 634729645e..944be40705 100644 --- a/apps/hubble/src/rpc/test/userDataService.test.ts +++ b/apps/hubble/src/rpc/test/userDataService.test.ts @@ -7,6 +7,7 @@ import { getInsecureHubRpcClient, HubError, HubRpcClient, + isUserDataAddData, Message, OnChainEvent, UserDataAddMessage, @@ -68,6 +69,8 @@ let addFname: UserDataAddMessage; let ensNameProof: UsernameProofMessage; let addEnsName: UserDataAddMessage; +let locationAdd: UserDataAddMessage; + beforeAll(async () => { const signerKey = (await signer.getSignerKey())._unsafeUnwrap(); custodySignerKey = (await custodySigner.getSignerKey())._unsafeUnwrap(); @@ -99,6 +102,20 @@ beforeAll(async () => { { transient: { signer } }, ); + locationAdd = await Factories.UserDataAddMessage.create( + { + data: { + fid, + userDataBody: { + type: UserDataType.LOCATION, + value: "geo:23.45,-167.78", + }, + timestamp: pfpAdd.data.timestamp + 2, + }, + }, + { transient: { signer } }, + ); + const custodySignerAddress = bytesToHexString(custodySignerKey)._unsafeUnwrap(); jest.spyOn(publicClient, "getEnsAddress").mockImplementation(() => { @@ -169,6 +186,10 @@ describe("getUserData", () => { expect(await engine.mergeMessage(addEnsName)).toBeInstanceOf(Ok); const ensNameData = await client.getUserData(UserDataRequest.create({ fid, userDataType: UserDataType.USERNAME })); expect(Message.toJSON(ensNameData._unsafeUnwrap())).toEqual(Message.toJSON(addEnsName)); + + expect(await engine.mergeMessage(locationAdd)).toBeInstanceOf(Ok); + const location = await client.getUserData(UserDataRequest.create({ fid, userDataType: UserDataType.LOCATION })); + expect(Message.toJSON(location._unsafeUnwrap())).toEqual(Message.toJSON(locationAdd)); }); test("fails when user data is missing", async () => { From 35da33736931f94b4eb021e4bfd197a45f7c3dee Mon Sep 17 00:00:00 2001 From: Aditi Srinivasan Date: Fri, 11 Oct 2024 15:44:40 -0400 Subject: [PATCH 4/9] location is working end to end --- .../src/rpc/test/userDataService.test.ts | 26 ++++++++ packages/core/src/validations.ts | 66 +++++++++++++++---- 2 files changed, 78 insertions(+), 14 deletions(-) diff --git a/apps/hubble/src/rpc/test/userDataService.test.ts b/apps/hubble/src/rpc/test/userDataService.test.ts index 944be40705..ec7f3b5eb3 100644 --- a/apps/hubble/src/rpc/test/userDataService.test.ts +++ b/apps/hubble/src/rpc/test/userDataService.test.ts @@ -8,6 +8,7 @@ import { HubError, HubRpcClient, isUserDataAddData, + makeUserDataAdd, Message, OnChainEvent, UserDataAddMessage, @@ -192,6 +193,31 @@ describe("getUserData", () => { expect(Message.toJSON(location._unsafeUnwrap())).toEqual(Message.toJSON(locationAdd)); }); + test("user location data can be cleared", async () => { + expect(await engine.mergeMessage(locationAdd)).toBeInstanceOf(Ok); + const location = await client.getUserData(UserDataRequest.create({ fid, userDataType: UserDataType.LOCATION })); + expect(Message.toJSON(location._unsafeUnwrap())).toEqual(Message.toJSON(locationAdd)); + makeUserDataAdd; + const emptyLocationAdd = await Factories.UserDataAddMessage.create( + { + data: { + fid, + userDataBody: { + type: UserDataType.LOCATION, + value: "", + }, + timestamp: locationAdd.data.timestamp + 1, + }, + }, + { transient: { signer } }, + ); + expect(await engine.mergeMessage(emptyLocationAdd)).toBeInstanceOf(Ok); + const emptyLocation = await client.getUserData( + UserDataRequest.create({ fid, userDataType: UserDataType.LOCATION }), + ); + expect(Message.toJSON(emptyLocation._unsafeUnwrap())).toEqual(Message.toJSON(emptyLocationAdd)); + }); + test("fails when user data is missing", async () => { const pfp = await client.getUserData(UserDataRequest.create({ fid, userDataType: UserDataType.PFP })); expect(pfp._unsafeUnwrapErr().errCode).toEqual("not_found"); diff --git a/packages/core/src/validations.ts b/packages/core/src/validations.ts index ad6659b126..42d860da31 100644 --- a/packages/core/src/validations.ts +++ b/packages/core/src/validations.ts @@ -110,33 +110,64 @@ export const validateMessageHash = (hash?: Uint8Array): HubResult => return ok(hash); }; -const validateLatitudeLongitude = (value: string) => { +const validateNumber = (value: string) => { + const number = parseFloat(value); + if (Number.isNaN(number)) { + return err(new HubError("bad_request.validation_failure", "Latitude or longitude is not a valid number")); + } + return ok(number); +}; + +const validateDecimalPlaces = (value: string) => { const [_, decimals] = value.split("."); - if (decimals?.length !== 2) { + if (decimals === undefined || decimals.length !== 2) { return err(new HubError("bad_request.validation_failure", "Wrong precision for latitude/longitude")); } return ok(value); }; -export const validateUserLocation = (location: string) => { - if (!location.startsWith("geo:")) { - return err(new HubError("bad_request.validation_failure", "Invalid Geo URI")); +const validateLatitude = (value: string) => { + const number = validateNumber(value); + + if (number.isErr()) { + return err(number.error); + } + + if (number.value < -90 || number.value > 90) { + return err(new HubError("bad_request.validation_failure", "Latitude value outside valid range")); } - const geoString = location.substring(4); + return validateDecimalPlaces(value); +}; - // Split on the semicolon to handle any optional parameters - const [coords, ...params] = geoString.split(";"); +const validateLongitude = (value: string) => { + const number = validateNumber(value); - if (params.length > 0) { - return err(new HubError("bad_request.validation_failure", "Extra parameters in Geo URI not supported")); + if (number.isErr()) { + return err(number.error); } - if (coords === undefined) { - return err(new HubError("bad_request.validation_failure", "Invalid Geo URI. No coordinates provided")); + if (number.value < -180 || number.value > 180) { + return err(new HubError("bad_request.validation_failure", "Longitude value outside valid range")); } + return validateDecimalPlaces(value); +}; + +// Expected format is [geo:,}] +export const validateUserLocation = (location: string) => { + if (location === "") { + // This is to support clearing location + return ok(location); + } + + if (!location.startsWith("geo:")) { + return err(new HubError("bad_request.validation_failure", "Invalid Geo URI")); + } + + const coords = location.substring(4); + // Split the coordinates (latitude, longitude, altitude) const coordParts = coords.split(","); @@ -148,7 +179,7 @@ export const validateUserLocation = (location: string) => { return err(new HubError("bad_request.validation_failure", "Geo URI missing latitude")); } - const latitude = validateLatitudeLongitude(coordParts[0]); + const latitude = validateLatitude(coordParts[0]); if (latitude.isErr()) { return err(latitude.error); } @@ -157,7 +188,7 @@ export const validateUserLocation = (location: string) => { return err(new HubError("bad_request.validation_failure", "Geo URI missing longitude")); } - const longitude = validateLatitudeLongitude(coordParts[1]); + const longitude = validateLongitude(coordParts[1]); if (longitude.isErr()) { return err(longitude.error); } @@ -999,6 +1030,13 @@ export const validateUserDataAddBody = (body: protobufs.UserDataBody): HubResult } break; } + case protobufs.UserDataType.LOCATION: { + const validatedUserLocation = validateUserLocation(value); + if (validatedUserLocation.isErr()) { + return err(validatedUserLocation.error); + } + break; + } default: return err(new HubError("bad_request.validation_failure", "invalid user data type")); } From f0becdf36afc9dd99411b0359a19892bdfa2261f Mon Sep 17 00:00:00 2001 From: Aditi Srinivasan Date: Fri, 11 Oct 2024 16:49:52 -0400 Subject: [PATCH 5/9] added validation tests --- packages/core/src/validations.test.ts | 112 ++++++++++++++++++++++++++ packages/core/src/validations.ts | 8 +- 2 files changed, 116 insertions(+), 4 deletions(-) diff --git a/packages/core/src/validations.test.ts b/packages/core/src/validations.test.ts index c586e4d58f..f393e9465d 100644 --- a/packages/core/src/validations.test.ts +++ b/packages/core/src/validations.test.ts @@ -1042,6 +1042,22 @@ describe("validateUserDataAddBody", () => { expect(validations.validateUserDataAddBody(body)).toEqual(ok(body)); }); + test("succeeds for empty location", async () => { + const body = Factories.UserDataBody.build({ + type: UserDataType.LOCATION, + value: "", + }); + expect(validations.validateUserDataAddBody(body)).toEqual(ok(body)); + }); + + test("succeeds for valid location", async () => { + const body = Factories.UserDataBody.build({ + type: UserDataType.LOCATION, + value: "geo:12.34,-123.45", + }); + expect(validations.validateUserDataAddBody(body)).toEqual(ok(body)); + }); + describe("fails", () => { let body: protobufs.UserDataBody; let hubErrorMessage: string; @@ -1083,6 +1099,102 @@ describe("validateUserDataAddBody", () => { }); hubErrorMessage = "url value > 256"; }); + + test("when latitude is too low", () => { + body = Factories.UserDataBody.build({ + type: protobufs.UserDataType.LOCATION, + value: "geo:-90.01,12.34", + }); + hubErrorMessage = "Latitude value outside valid range"; + }); + + test("when latitude is too high", () => { + body = Factories.UserDataBody.build({ + type: protobufs.UserDataType.LOCATION, + value: "geo:90.01,12.34", + }); + hubErrorMessage = "Latitude value outside valid range"; + }); + + test("when longitude is too low", () => { + body = Factories.UserDataBody.build({ + type: protobufs.UserDataType.LOCATION, + value: "geo:12.34,-180.01", + }); + hubErrorMessage = "Longitude value outside valid range"; + }); + + test("when longitude is too high", () => { + body = Factories.UserDataBody.build({ + type: protobufs.UserDataType.LOCATION, + value: "geo:12.34,180.01", + }); + hubErrorMessage = "Longitude value outside valid range"; + }); + + test("when latitude has too much precision", () => { + body = Factories.UserDataBody.build({ + type: protobufs.UserDataType.LOCATION, + value: "geo:12.345,12.34", + }); + hubErrorMessage = "Wrong precision for latitude or longitude"; + }); + + test("when latitude has insufficient precision", () => { + body = Factories.UserDataBody.build({ + type: protobufs.UserDataType.LOCATION, + value: "geo:12,12.34", + }); + hubErrorMessage = "Wrong precision for latitude or longitude"; + }); + + test("when longitude has too much precision", () => { + body = Factories.UserDataBody.build({ + type: protobufs.UserDataType.LOCATION, + value: "geo:12.34,12.345", + }); + hubErrorMessage = "Wrong precision for latitude or longitude"; + }); + + test("when longitude has insufficient precision", () => { + body = Factories.UserDataBody.build({ + type: protobufs.UserDataType.LOCATION, + value: "geo:12.345,12.34", + }); + hubErrorMessage = "Wrong precision for latitude or longitude"; + }); + + test("when latitude is an invalid number", () => { + body = Factories.UserDataBody.build({ + type: protobufs.UserDataType.LOCATION, + value: "geo:xx,12.34", + }); + hubErrorMessage = "Latitude or longitude is not a valid number"; + }); + + test("when longitude is an invalid number", () => { + body = Factories.UserDataBody.build({ + type: protobufs.UserDataType.LOCATION, + value: "geo:12.34,xx", + }); + hubErrorMessage = "Latitude or longitude is not a valid number"; + }); + + test("when location is missing geo prefix", () => { + body = Factories.UserDataBody.build({ + type: protobufs.UserDataType.LOCATION, + value: "12.34,12.34", + }); + hubErrorMessage = "Location missing geo: prefix"; + }); + + test("when location is missing a coordinate", () => { + body = Factories.UserDataBody.build({ + type: protobufs.UserDataType.LOCATION, + value: "geo:12.34", + }); + hubErrorMessage = "Invalid coordinates in Geo URI"; + }); }); }); diff --git a/packages/core/src/validations.ts b/packages/core/src/validations.ts index 42d860da31..4d3ffd8c76 100644 --- a/packages/core/src/validations.ts +++ b/packages/core/src/validations.ts @@ -121,7 +121,7 @@ const validateNumber = (value: string) => { const validateDecimalPlaces = (value: string) => { const [_, decimals] = value.split("."); if (decimals === undefined || decimals.length !== 2) { - return err(new HubError("bad_request.validation_failure", "Wrong precision for latitude/longitude")); + return err(new HubError("bad_request.validation_failure", "Wrong precision for latitude or longitude")); } return ok(value); @@ -163,7 +163,7 @@ export const validateUserLocation = (location: string) => { } if (!location.startsWith("geo:")) { - return err(new HubError("bad_request.validation_failure", "Invalid Geo URI")); + return err(new HubError("bad_request.validation_failure", "Location missing geo: prefix")); } const coords = location.substring(4); @@ -176,7 +176,7 @@ export const validateUserLocation = (location: string) => { } if (coordParts[0] === undefined) { - return err(new HubError("bad_request.validation_failure", "Geo URI missing latitude")); + return err(new HubError("bad_request.validation_failure", "Location missing latitude")); } const latitude = validateLatitude(coordParts[0]); @@ -185,7 +185,7 @@ export const validateUserLocation = (location: string) => { } if (coordParts[1] === undefined) { - return err(new HubError("bad_request.validation_failure", "Geo URI missing longitude")); + return err(new HubError("bad_request.validation_failure", "Location missing longitude")); } const longitude = validateLongitude(coordParts[1]); From 9fca7aa4e47e51e79d4e4afbb21a6cbe7f50f0d4 Mon Sep 17 00:00:00 2001 From: Aditi Srinivasan Date: Fri, 11 Oct 2024 17:01:55 -0400 Subject: [PATCH 6/9] fix up tests --- packages/core/src/validations.test.ts | 14 +++++++++++--- packages/core/src/validations.ts | 2 +- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/packages/core/src/validations.test.ts b/packages/core/src/validations.test.ts index f393e9465d..d1dd9bb82a 100644 --- a/packages/core/src/validations.test.ts +++ b/packages/core/src/validations.test.ts @@ -1159,7 +1159,7 @@ describe("validateUserDataAddBody", () => { test("when longitude has insufficient precision", () => { body = Factories.UserDataBody.build({ type: protobufs.UserDataType.LOCATION, - value: "geo:12.345,12.34", + value: "geo:12.34,12", }); hubErrorMessage = "Wrong precision for latitude or longitude"; }); @@ -1188,12 +1188,20 @@ describe("validateUserDataAddBody", () => { hubErrorMessage = "Location missing geo: prefix"; }); + test("when location is missing both coordinates", () => { + body = Factories.UserDataBody.build({ + type: protobufs.UserDataType.LOCATION, + value: "geo:", + }); + hubErrorMessage = "Location contains invalid coordinates"; + }); + test("when location is missing a coordinate", () => { body = Factories.UserDataBody.build({ type: protobufs.UserDataType.LOCATION, - value: "geo:12.34", + value: "geo:12.34,", }); - hubErrorMessage = "Invalid coordinates in Geo URI"; + hubErrorMessage = "Latitude or longitude is not a valid number"; }); }); }); diff --git a/packages/core/src/validations.ts b/packages/core/src/validations.ts index 4d3ffd8c76..f5d9f82bf2 100644 --- a/packages/core/src/validations.ts +++ b/packages/core/src/validations.ts @@ -172,7 +172,7 @@ export const validateUserLocation = (location: string) => { const coordParts = coords.split(","); if (coordParts === undefined || coordParts.length < 2) { - return err(new HubError("bad_request.validation_failure", "Invalid coordinates in Geo URI")); + return err(new HubError("bad_request.validation_failure", "Location contains invalid coordinates")); } if (coordParts[0] === undefined) { From c477670a8c6fda3ff9751843a673546eca9a9243 Mon Sep 17 00:00:00 2001 From: Aditi Srinivasan Date: Fri, 11 Oct 2024 17:03:19 -0400 Subject: [PATCH 7/9] changeset --- .changeset/serious-bulldogs-sort.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .changeset/serious-bulldogs-sort.md diff --git a/.changeset/serious-bulldogs-sort.md b/.changeset/serious-bulldogs-sort.md new file mode 100644 index 0000000000..6c45823776 --- /dev/null +++ b/.changeset/serious-bulldogs-sort.md @@ -0,0 +1,8 @@ +--- +"@farcaster/hub-nodejs": patch +"@farcaster/hub-web": patch +"@farcaster/core": patch +"@farcaster/hubble": patch +--- + +feat: add user location to the protocol From 598206cbce96b59049a025854d491ac9386cc14e Mon Sep 17 00:00:00 2001 From: Aditi Srinivasan Date: Fri, 11 Oct 2024 17:35:40 -0400 Subject: [PATCH 8/9] add a regex match --- packages/core/src/validations.test.ts | 26 +++++++++++++++++--------- packages/core/src/validations.ts | 12 +++++++----- 2 files changed, 24 insertions(+), 14 deletions(-) diff --git a/packages/core/src/validations.test.ts b/packages/core/src/validations.test.ts index d1dd9bb82a..c868b7f9be 100644 --- a/packages/core/src/validations.test.ts +++ b/packages/core/src/validations.test.ts @@ -1137,7 +1137,7 @@ describe("validateUserDataAddBody", () => { type: protobufs.UserDataType.LOCATION, value: "geo:12.345,12.34", }); - hubErrorMessage = "Wrong precision for latitude or longitude"; + hubErrorMessage = "Invalid location string"; }); test("when latitude has insufficient precision", () => { @@ -1145,7 +1145,7 @@ describe("validateUserDataAddBody", () => { type: protobufs.UserDataType.LOCATION, value: "geo:12,12.34", }); - hubErrorMessage = "Wrong precision for latitude or longitude"; + hubErrorMessage = "Invalid location string"; }); test("when longitude has too much precision", () => { @@ -1153,7 +1153,7 @@ describe("validateUserDataAddBody", () => { type: protobufs.UserDataType.LOCATION, value: "geo:12.34,12.345", }); - hubErrorMessage = "Wrong precision for latitude or longitude"; + hubErrorMessage = "Invalid location string"; }); test("when longitude has insufficient precision", () => { @@ -1161,7 +1161,7 @@ describe("validateUserDataAddBody", () => { type: protobufs.UserDataType.LOCATION, value: "geo:12.34,12", }); - hubErrorMessage = "Wrong precision for latitude or longitude"; + hubErrorMessage = "Invalid location string"; }); test("when latitude is an invalid number", () => { @@ -1169,7 +1169,7 @@ describe("validateUserDataAddBody", () => { type: protobufs.UserDataType.LOCATION, value: "geo:xx,12.34", }); - hubErrorMessage = "Latitude or longitude is not a valid number"; + hubErrorMessage = "Invalid location string"; }); test("when longitude is an invalid number", () => { @@ -1177,7 +1177,7 @@ describe("validateUserDataAddBody", () => { type: protobufs.UserDataType.LOCATION, value: "geo:12.34,xx", }); - hubErrorMessage = "Latitude or longitude is not a valid number"; + hubErrorMessage = "Invalid location string"; }); test("when location is missing geo prefix", () => { @@ -1185,7 +1185,7 @@ describe("validateUserDataAddBody", () => { type: protobufs.UserDataType.LOCATION, value: "12.34,12.34", }); - hubErrorMessage = "Location missing geo: prefix"; + hubErrorMessage = "Invalid location string"; }); test("when location is missing both coordinates", () => { @@ -1193,7 +1193,7 @@ describe("validateUserDataAddBody", () => { type: protobufs.UserDataType.LOCATION, value: "geo:", }); - hubErrorMessage = "Location contains invalid coordinates"; + hubErrorMessage = "Invalid location string"; }); test("when location is missing a coordinate", () => { @@ -1201,7 +1201,15 @@ describe("validateUserDataAddBody", () => { type: protobufs.UserDataType.LOCATION, value: "geo:12.34,", }); - hubErrorMessage = "Latitude or longitude is not a valid number"; + hubErrorMessage = "Invalid location string"; + }); + + test("when location contains a space", () => { + body = Factories.UserDataBody.build({ + type: protobufs.UserDataType.LOCATION, + value: "geo:12.34, 12.34", + }); + hubErrorMessage = "Invalid location string"; }); }); }); diff --git a/packages/core/src/validations.ts b/packages/core/src/validations.ts index f5d9f82bf2..f6b470caf4 100644 --- a/packages/core/src/validations.ts +++ b/packages/core/src/validations.ts @@ -113,7 +113,7 @@ export const validateMessageHash = (hash?: Uint8Array): HubResult => const validateNumber = (value: string) => { const number = parseFloat(value); if (Number.isNaN(number)) { - return err(new HubError("bad_request.validation_failure", "Latitude or longitude is not a valid number")); + return err(undefined); } return ok(number); }; @@ -131,7 +131,7 @@ const validateLatitude = (value: string) => { const number = validateNumber(value); if (number.isErr()) { - return err(number.error); + return err(new HubError("bad_request.validation_failure", "Latitude is not a valid number")); } if (number.value < -90 || number.value > 90) { @@ -145,7 +145,7 @@ const validateLongitude = (value: string) => { const number = validateNumber(value); if (number.isErr()) { - return err(number.error); + return err(new HubError("bad_request.validation_failure", "Longitude is not a valid number")); } if (number.value < -180 || number.value > 180) { @@ -162,8 +162,10 @@ export const validateUserLocation = (location: string) => { return ok(location); } - if (!location.startsWith("geo:")) { - return err(new HubError("bad_request.validation_failure", "Location missing geo: prefix")); + const result = location.match(/^geo:-?\d{1,2}\.\d{2},-?\d{1,3}\.\d{2}$/); + + if (result === null || result.length !== 1 || result[0] !== location) { + return err(new HubError("bad_request.validation_failure", "Invalid location string")); } const coords = location.substring(4); From ebe9b0bfd5a82806262d7ab3fa829068b48d444a Mon Sep 17 00:00:00 2001 From: Aditi Srinivasan Date: Fri, 11 Oct 2024 17:47:49 -0400 Subject: [PATCH 9/9] changed to regex validation and added tests --- packages/core/src/validations.test.ts | 10 +++++++- packages/core/src/validations.ts | 35 +++++++-------------------- 2 files changed, 18 insertions(+), 27 deletions(-) diff --git a/packages/core/src/validations.test.ts b/packages/core/src/validations.test.ts index c868b7f9be..2e0cc6fe5d 100644 --- a/packages/core/src/validations.test.ts +++ b/packages/core/src/validations.test.ts @@ -1050,7 +1050,7 @@ describe("validateUserDataAddBody", () => { expect(validations.validateUserDataAddBody(body)).toEqual(ok(body)); }); - test("succeeds for valid location", async () => { + test("succeeds for valid location: negative longitude", async () => { const body = Factories.UserDataBody.build({ type: UserDataType.LOCATION, value: "geo:12.34,-123.45", @@ -1058,6 +1058,14 @@ describe("validateUserDataAddBody", () => { expect(validations.validateUserDataAddBody(body)).toEqual(ok(body)); }); + test("succeeds for valid location: negative latitude", async () => { + const body = Factories.UserDataBody.build({ + type: UserDataType.LOCATION, + value: "geo:-12.34,123.45", + }); + expect(validations.validateUserDataAddBody(body)).toEqual(ok(body)); + }); + describe("fails", () => { let body: protobufs.UserDataBody; let hubErrorMessage: string; diff --git a/packages/core/src/validations.ts b/packages/core/src/validations.ts index f6b470caf4..edf7535e61 100644 --- a/packages/core/src/validations.ts +++ b/packages/core/src/validations.ts @@ -118,15 +118,6 @@ const validateNumber = (value: string) => { return ok(number); }; -const validateDecimalPlaces = (value: string) => { - const [_, decimals] = value.split("."); - if (decimals === undefined || decimals.length !== 2) { - return err(new HubError("bad_request.validation_failure", "Wrong precision for latitude or longitude")); - } - - return ok(value); -}; - const validateLatitude = (value: string) => { const number = validateNumber(value); @@ -138,7 +129,7 @@ const validateLatitude = (value: string) => { return err(new HubError("bad_request.validation_failure", "Latitude value outside valid range")); } - return validateDecimalPlaces(value); + return ok(value); }; const validateLongitude = (value: string) => { @@ -152,7 +143,7 @@ const validateLongitude = (value: string) => { return err(new HubError("bad_request.validation_failure", "Longitude value outside valid range")); } - return validateDecimalPlaces(value); + return ok(value); }; // Expected format is [geo:,}] @@ -162,35 +153,27 @@ export const validateUserLocation = (location: string) => { return ok(location); } - const result = location.match(/^geo:-?\d{1,2}\.\d{2},-?\d{1,3}\.\d{2}$/); + // Match any <=2 digit number with 2dp for latitude and <=3 digit number with 3dp for longitude. Perform validation on ranges in code after parsing because doing this in regex is pretty cumbersome. + const result = location.match(/^geo:(-?\d{1,2}\.\d{2}),(-?\d{1,3}\.\d{2})$/); - if (result === null || result.length !== 1 || result[0] !== location) { + if (result === null || result[0] !== location) { return err(new HubError("bad_request.validation_failure", "Invalid location string")); } - const coords = location.substring(4); - - // Split the coordinates (latitude, longitude, altitude) - const coordParts = coords.split(","); - - if (coordParts === undefined || coordParts.length < 2) { - return err(new HubError("bad_request.validation_failure", "Location contains invalid coordinates")); - } - - if (coordParts[0] === undefined) { + if (result[1] === undefined) { return err(new HubError("bad_request.validation_failure", "Location missing latitude")); } - const latitude = validateLatitude(coordParts[0]); + const latitude = validateLatitude(result[1]); if (latitude.isErr()) { return err(latitude.error); } - if (coordParts[1] === undefined) { + if (result[2] === undefined) { return err(new HubError("bad_request.validation_failure", "Location missing longitude")); } - const longitude = validateLongitude(coordParts[1]); + const longitude = validateLongitude(result[2]); if (longitude.isErr()) { return err(longitude.error); }