diff --git a/lib/shared/threads/farcaster-group-spec.js b/lib/shared/threads/farcaster-group-spec.js new file mode 100644 --- /dev/null +++ b/lib/shared/threads/farcaster-group-spec.js @@ -0,0 +1,15 @@ +// @flow + +import { farcasterThreadProtocol } from './protocols/farcaster-thread-protocol.js'; +import type { ThreadSpec } from './thread-spec.js'; +import type { MemberInfoSansPermissions } from '../../types/minimally-encoded-thread-permissions-types.js'; + +const farcasterGroupSpec: ThreadSpec = Object.freeze( + { + traits: new Set(), + protocol: () => farcasterThreadProtocol, + threadLabel: 'Farcaster Group', + }, +); + +export { farcasterGroupSpec }; diff --git a/lib/shared/threads/farcaster-personal-spec.js b/lib/shared/threads/farcaster-personal-spec.js new file mode 100644 --- /dev/null +++ b/lib/shared/threads/farcaster-personal-spec.js @@ -0,0 +1,14 @@ +// @flow + +import { farcasterThreadProtocol } from './protocols/farcaster-thread-protocol.js'; +import type { ThreadSpec } from './thread-spec.js'; +import type { MemberInfoSansPermissions } from '../../types/minimally-encoded-thread-permissions-types.js'; + +const farcasterPersonalSpec: ThreadSpec = + Object.freeze({ + traits: new Set(['personal']), + protocol: () => farcasterThreadProtocol, + threadLabel: 'Farcaster Personal', + }); + +export { farcasterPersonalSpec }; diff --git a/lib/shared/threads/protocols/dm-thread-protocol.js b/lib/shared/threads/protocols/dm-thread-protocol.js --- a/lib/shared/threads/protocols/dm-thread-protocol.js +++ b/lib/shared/threads/protocols/dm-thread-protocol.js @@ -51,7 +51,7 @@ import { assertWithValidator, pendingThickSidebarURLPrefix, - thickIDRegex, + thickIDRegExp, } from '../../../utils/validation-utils.js'; import { generatePendingThreadColor } from '../../color-utils.js'; import { @@ -890,7 +890,7 @@ }: ThreadJoinPayload); }, - threadIDMatchesProtocol: (threadID: string) => thickIDRegex.test(threadID), + threadIDMatchesProtocol: (threadID: string) => thickIDRegExp.test(threadID), allowsDeletingSidebarSource: false, diff --git a/lib/shared/threads/protocols/farcaster-thread-protocol.js b/lib/shared/threads/protocols/farcaster-thread-protocol.js --- a/lib/shared/threads/protocols/farcaster-thread-protocol.js +++ b/lib/shared/threads/protocols/farcaster-thread-protocol.js @@ -22,7 +22,10 @@ ChangeThreadSettingsPayload, ThreadJoinPayload, } from '../../../types/thread-types.js'; -import { pendingThickSidebarURLPrefix } from '../../../utils/validation-utils.js'; +import { + farcasterThreadIDRegExp, + pendingThickSidebarURLPrefix, +} from '../../../utils/validation-utils.js'; import { messageNotifyTypes } from '../../messages/message-spec.js'; import type { ThreadProtocol } from '../thread-spec.js'; @@ -129,7 +132,7 @@ }, threadIDMatchesProtocol: (threadID: string): boolean => { - return threadID.startsWith('FARCASTER#'); + return farcasterThreadIDRegExp.test(threadID); }, allowsDeletingSidebarSource: false, diff --git a/lib/shared/threads/protocols/keyserver-thread-protocol.js b/lib/shared/threads/protocols/keyserver-thread-protocol.js --- a/lib/shared/threads/protocols/keyserver-thread-protocol.js +++ b/lib/shared/threads/protocols/keyserver-thread-protocol.js @@ -69,9 +69,8 @@ SendMessageError, } from '../../../utils/errors.js'; import { - isSchemaRegExp, pendingSidebarURLPrefix, - thickIDRegex, + isKeyserverThreadID, } from '../../../utils/validation-utils.js'; import { generatePendingThreadColor } from '../../color-utils.js'; import { getNextLocalID } from '../../id-utils.js'; @@ -646,7 +645,7 @@ }, threadIDMatchesProtocol: (threadID: string) => { - return isSchemaRegExp.test(threadID) && !thickIDRegex.test(threadID); + return isKeyserverThreadID(threadID); }, allowsDeletingSidebarSource: true, diff --git a/lib/shared/threads/thread-specs.js b/lib/shared/threads/thread-specs.js --- a/lib/shared/threads/thread-specs.js +++ b/lib/shared/threads/thread-specs.js @@ -6,6 +6,8 @@ import { communityRootSpec } from './community-root-spec.js'; import { communitySecretAnnouncementSubthreadSpec } from './community-secret-announcement-subthread-spec.js'; import { communitySecretSubthreadSpec } from './community-secret-subthread-spec.js'; +import { farcasterGroupSpec } from './farcaster-group-spec.js'; +import { farcasterPersonalSpec } from './farcaster-personal-spec.js'; import { genesisPersonalSpec } from './genesis-personal-spec.js'; import { genesisPrivateSpec } from './genesis-private-spec.js'; import { genesisSpec } from './genesis-spec.js'; @@ -38,6 +40,8 @@ [threadTypes.PERSONAL]: personalSpec, [threadTypes.PRIVATE]: privateSpec, [threadTypes.THICK_SIDEBAR]: thickSidebarSpec, + [threadTypes.FARCASTER_PERSONAL]: farcasterPersonalSpec, + [threadTypes.FARCASTER_GROUP]: farcasterGroupSpec, }); let threadTypesByTrait = null; diff --git a/lib/types/dm-ops.js b/lib/types/dm-ops.js --- a/lib/types/dm-ops.js +++ b/lib/types/dm-ops.js @@ -29,7 +29,7 @@ import { values } from '../utils/objects.js'; import { tColor, - thickIDRegex, + thickIDRegExp, tRegex, tShape, tString, @@ -59,7 +59,7 @@ }); export type DMOperationType = $Values; -const tThickID = tRegex(thickIDRegex); +const tThickID = tRegex(thickIDRegExp); // In CHANGE_THREAD_SETTINGS operation we're generating message IDs // based on the prefix that is tThickID. A message with the generated ID diff --git a/lib/types/message-types.js b/lib/types/message-types.js --- a/lib/types/message-types.js +++ b/lib/types/message-types.js @@ -155,7 +155,7 @@ tNumber, tShape, tUserID, - thickIDRegex, + thickIDRegExp, } from '../utils/validation-utils.js'; const composableMessageTypes = new Set([ @@ -743,5 +743,5 @@ }; export function messageIDIsThick(messageID: string): boolean { - return thickIDRegex.test(messageID); + return thickIDRegExp.test(messageID); } diff --git a/lib/types/thread-types-enum.js b/lib/types/thread-types-enum.js --- a/lib/types/thread-types-enum.js +++ b/lib/types/thread-types-enum.js @@ -68,11 +68,18 @@ | NonSidebarThickThreadType | SidebarThickThreadType; -export type ThreadType = ThinThreadType | ThickThreadType; +export const farcasterThreadTypes = Object.freeze({ + FARCASTER_PERSONAL: 17, + FARCASTER_GROUP: 18, +}); +export type FarcasterThreadType = $Values; + +export type ThreadType = ThinThreadType | ThickThreadType | FarcasterThreadType; export const threadTypes = Object.freeze({ ...thinThreadTypes, ...thickThreadTypes, + ...farcasterThreadTypes, }); export function assertThinThreadType(threadType: number): ThinThreadType { @@ -110,6 +117,19 @@ values(thickThreadTypes), ); +export function assertFarcasterThreadType( + threadType: number, +): FarcasterThreadType { + invariant( + threadType === 17 || threadType === 18, + 'number is not FarcasterThreadType enum', + ); + return threadType; +} +export const farcasterThreadTypeValidator: TRefinement = tNumEnum( + values(farcasterThreadTypes), +); + export function assertThreadType(threadType: number): ThreadType { invariant( threadType === 3 || @@ -125,7 +145,9 @@ threadType === 13 || threadType === 14 || threadType === 15 || - threadType === 16, + threadType === 16 || + threadType === 17 || + threadType === 18, 'number is not ThreadType enum', ); return threadType; diff --git a/lib/utils/validation-utils.js b/lib/utils/validation-utils.js --- a/lib/utils/validation-utils.js +++ b/lib/utils/validation-utils.js @@ -112,13 +112,27 @@ // PersistentStorageUtilities/ThreadOperationsUtilities/ThreadTypeEnum.h const uuidRegex = '[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}'; -const idSchemaRegex = `(?:(?:[0-9]+|${uuidRegex})\\|)?(?:[0-9]+|${uuidRegex})`; -const isSchemaRegExp: RegExp = new RegExp(`^${idSchemaRegex}$`); +const thickIDRegExp: RegExp = new RegExp(`^${uuidRegex}$`); + +const farcasterThreadIDRegex = 'FARCASTER#(?:(?:[0-9a-z]+)|(?:[0-9]+-[0-9]+))'; +const farcasterThreadIDRegExp: RegExp = new RegExp( + `^${farcasterThreadIDRegex}$`, +); + +const idSchemaRegex = `(?:${farcasterThreadIDRegex}|(?:(?:[0-9]+|${uuidRegex})\\|)?(?:[0-9]+|${uuidRegex}))`; +const idSchemaRegExp: RegExp = new RegExp(`^${idSchemaRegex}$`); + +function isKeyserverThreadID(threadID: string): boolean { + return ( + idSchemaRegExp.test(threadID) && + !thickIDRegExp.test(threadID) && + !farcasterThreadIDRegExp.test(threadID) + ); +} const pendingSidebarURLPrefix = 'sidebar'; const pendingThickSidebarURLPrefix = 'dm_sidebar'; const pendingThreadIDRegex = `pending/(type[0-9]+/[0-9]+(\\+[0-9]+)*|(${pendingSidebarURLPrefix}|${pendingThickSidebarURLPrefix})/${idSchemaRegex})`; -const thickIDRegex: RegExp = new RegExp(`^${uuidRegex}$`); const chatNameMaxLength = 191; const chatNameMinLength = 0; @@ -155,9 +169,11 @@ pendingSidebarURLPrefix, pendingThickSidebarURLPrefix, pendingThreadIDRegex, - thickIDRegex, + thickIDRegExp, validChatNameRegex, validChatNameRegexString, chatNameMaxLength, - isSchemaRegExp, + idSchemaRegExp, + farcasterThreadIDRegExp, + isKeyserverThreadID, }; diff --git a/lib/utils/validation-utils.test.js b/lib/utils/validation-utils.test.js --- a/lib/utils/validation-utils.test.js +++ b/lib/utils/validation-utils.test.js @@ -1,8 +1,12 @@ // @flow import { + idSchemaRegExp, tMediaMessagePhoto, tMediaMessageVideo, + isKeyserverThreadID, + thickIDRegExp, + farcasterThreadIDRegExp, tNumEnum, } from './validation-utils.js'; import { threadTypes } from '../types/thread-types-enum.js'; @@ -139,4 +143,149 @@ ).toBe(false); }); }); + + describe('ID regex tests', () => { + describe('Keyserver ID regex', () => { + it('Should match compound IDs with keyserver components', () => { + expect(isKeyserverThreadID('256|123')).toBe(true); + expect(isKeyserverThreadID('123|456')).toBe(true); + expect( + isKeyserverThreadID('550e8400-e29b-41d4-a716-446655440000|456'), + ).toBe(true); + }); + + it('Should not match thick IDs (UUIDs)', () => { + expect( + isKeyserverThreadID('550e8400-e29b-41d4-a716-446655440000'), + ).toBe(false); + }); + + it('Should not match Farcaster IDs', () => { + expect(isKeyserverThreadID('FARCASTER#ef5a742bca')).toBe(false); + expect(isKeyserverThreadID('FARCASTER#12345-635')).toBe(false); + }); + + it('Should not match invalid formats', () => { + expect(isKeyserverThreadID('')).toBe(false); + expect(isKeyserverThreadID('abc')).toBe(false); + expect(isKeyserverThreadID('123abc')).toBe(false); + }); + }); + + describe('Thick ID regex', () => { + it('Should match valid UUIDs', () => { + expect(thickIDRegExp.test('550e8400-e29b-41d4-a716-446655440000')).toBe( + true, + ); + expect(thickIDRegExp.test('6ba7b810-9dad-11d1-80b4-00c04fd430c8')).toBe( + true, + ); + expect(thickIDRegExp.test('12345678-1234-1234-1234-123456789abc')).toBe( + true, + ); + }); + + it('Should not match keyserver IDs', () => { + expect(thickIDRegExp.test('256')).toBe(false); + expect(thickIDRegExp.test('1234567890')).toBe(false); + }); + + it('Should not match Farcaster IDs', () => { + expect(thickIDRegExp.test('FARCASTER#ef5a742bca')).toBe(false); + expect(thickIDRegExp.test('FARCASTER#12345-635')).toBe(false); + }); + + it('Should not match malformed UUIDs', () => { + expect(thickIDRegExp.test('550e8400-e29b-41d4-a716-44665544000')).toBe( + false, + ); + expect( + thickIDRegExp.test('550e8400-e29b-41d4-a716-446655440000-extra'), + ).toBe(false); + expect(thickIDRegExp.test('550e8400-e29b-41d4-a716')).toBe(false); + }); + }); + + describe('Farcaster ID regex', () => { + it('Should match group conversation', () => { + expect(farcasterThreadIDRegExp.test('FARCASTER#ef5a742bca')).toBe(true); + expect(farcasterThreadIDRegExp.test('FARCASTER#abc123')).toBe(true); + expect(farcasterThreadIDRegExp.test('FARCASTER#0123456789abcdef')).toBe( + true, + ); + }); + + it('Should match 1:1 conversation', () => { + expect(farcasterThreadIDRegExp.test('FARCASTER#12345-635')).toBe(true); + expect(farcasterThreadIDRegExp.test('FARCASTER#1-2')).toBe(true); + expect(farcasterThreadIDRegExp.test('FARCASTER#111111-999999')).toBe( + true, + ); + }); + + it('Should not match keyserver IDs', () => { + expect(farcasterThreadIDRegExp.test('256')).toBe(false); + expect(farcasterThreadIDRegExp.test('1234567890')).toBe(false); + }); + + it('Should not match thick IDs', () => { + expect( + farcasterThreadIDRegExp.test('550e8400-e29b-41d4-a716-446655440000'), + ).toBe(false); + }); + + it('Should not match malformed Farcaster IDs', () => { + expect(farcasterThreadIDRegExp.test('farcaster')).toBe(false); + expect(farcasterThreadIDRegExp.test('FARCASTER#')).toBe(false); + expect(farcasterThreadIDRegExp.test('notFARCASTER#12345')).toBe(false); + }); + }); + + describe('idSchemaRegex', () => { + it('Should match keyserver IDs', () => { + expect(idSchemaRegExp.test('256')).toBe(true); + expect(idSchemaRegExp.test('1234567890')).toBe(true); + expect(idSchemaRegExp.test('1')).toBe(true); + }); + + it('Should match thick IDs', () => { + expect( + idSchemaRegExp.test('550e8400-e29b-41d4-a716-446655440000'), + ).toBe(true); + expect( + idSchemaRegExp.test('6ba7b810-9dad-11d1-80b4-00c04fd430c8'), + ).toBe(true); + }); + + it('Should match Farcaster IDs', () => { + expect(idSchemaRegExp.test('FARCASTER#ef5a742bca')).toBe(true); + expect(idSchemaRegExp.test('FARCASTER#12345-635')).toBe(true); + }); + + it('Should match compound IDs with keyserver prefix', () => { + expect( + idSchemaRegExp.test('256|550e8400-e29b-41d4-a716-446655440000'), + ).toBe(true); + }); + + it('Should match compound IDs with UUID prefix', () => { + expect( + idSchemaRegExp.test('550e8400-e29b-41d4-a716-446655440000|256'), + ).toBe(true); + }); + + it('Should not match invalid formats', () => { + expect(idSchemaRegExp.test('')).toBe(false); + expect(idSchemaRegExp.test('invalid')).toBe(false); + expect(idSchemaRegExp.test('123abc')).toBe(false); + expect(idSchemaRegExp.test('farcaster')).toBe(false); + expect(idSchemaRegExp.test('550e8400-e29b-41d4-a716')).toBe(false); + }); + + it('Should not match multiple pipe separators', () => { + expect(idSchemaRegExp.test('256|123|456')).toBe(false); + expect(idSchemaRegExp.test('256||123')).toBe(false); + }); + }); + }); }); diff --git a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h --- a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h +++ b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.h @@ -29,7 +29,6 @@ std::vector processMessagesResults(SQLiteStatementWrapper &preparedSQL) const; - std::string getThickThreadTypesList() const; public: SQLiteQueryExecutor(std::string sqliteFilePath, bool skipMigration = false); diff --git a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp --- a/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp +++ b/native/cpp/CommonCpp/DatabaseManagers/SQLiteQueryExecutor.cpp @@ -188,20 +188,6 @@ } } -std::string SQLiteQueryExecutor::getThickThreadTypesList() const { - std::stringstream resultStream; - for (auto it = THICK_THREAD_TYPES.begin(); it != THICK_THREAD_TYPES.end(); - ++it) { - int typeInt = static_cast(*it); - resultStream << typeInt; - - if (it + 1 != THICK_THREAD_TYPES.end()) { - resultStream << ","; - } - } - return resultStream.str(); -} - std::vector SQLiteQueryExecutor::getInitialMessages() const { static std::string getInitialMessagesSQL = "SELECT " diff --git a/native/cpp/CommonCpp/NativeModules/PersistentStorageUtilities/ThreadOperationsUtilities/ThreadTypeEnum.h b/native/cpp/CommonCpp/NativeModules/PersistentStorageUtilities/ThreadOperationsUtilities/ThreadTypeEnum.h --- a/native/cpp/CommonCpp/NativeModules/PersistentStorageUtilities/ThreadOperationsUtilities/ThreadTypeEnum.h +++ b/native/cpp/CommonCpp/NativeModules/PersistentStorageUtilities/ThreadOperationsUtilities/ThreadTypeEnum.h @@ -22,35 +22,41 @@ PERSONAL = 14, PRIVATE = 15, THICK_SIDEBAR = 16, + FARCASTER_PERSONAL = 17, + FARCASTER_GROUP = 18, }; -const std::vector THICK_THREAD_TYPES{ - ThreadType::LOCAL, - ThreadType::PERSONAL, - ThreadType::PRIVATE, - ThreadType::THICK_SIDEBAR}; - // Regex patterns - should be in sync with lib/utils/validation-utils.js const std::string UUID_REGEX_STRING = "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{" "12}"; -const std::string ID_SCHEMA_REGEX_STRING = "(?:(?:[0-9]+|" + UUID_REGEX_STRING + - ")\\|)?(?:[0-9]+|" + UUID_REGEX_STRING + ")"; - -const std::regex IS_SCHEMA_REGEX("^" + ID_SCHEMA_REGEX_STRING + "$"); +const std::string FARCASTER_ID_REGEX_STRING = + "FARCASTER#(?:(?:[0-9a-z]+)|(?:" + "[0-9]+-[0-9]+))"; +const std::string ID_SCHEMA_REGEX_STRING = "(?:" + FARCASTER_ID_REGEX_STRING + + "|(?:(?:[0-9]+|" + UUID_REGEX_STRING + ")\\|)?(?:[0-9]+|" + + UUID_REGEX_STRING + "))"; + +const std::regex ID_SCHEMA_REGEX("^" + ID_SCHEMA_REGEX_STRING + "$"); const std::regex THICK_ID_REGEX("^" + UUID_REGEX_STRING + "$"); +const std::regex FARCASTER_ID_REGEX("^" + FARCASTER_ID_REGEX_STRING + "$"); // Helper functions for regex testing inline bool isSchemaID(const std::string &threadID) { - return std::regex_match(threadID, IS_SCHEMA_REGEX); + return std::regex_match(threadID, ID_SCHEMA_REGEX); } inline bool isThickID(const std::string &threadID) { return std::regex_match(threadID, THICK_ID_REGEX); } +inline bool isFarcasterID(const std::string &threadID) { + return std::regex_match(threadID, FARCASTER_ID_REGEX); +} + inline bool threadIDMatchesKeyserverProtocol(const std::string &threadID) { - return isSchemaID(threadID) && !isThickID(threadID); + return isSchemaID(threadID) && !isThickID(threadID) && + !isFarcasterID(threadID); } } // namespace comm diff --git a/web/shared-worker/_generated/comm_query_executor.wasm b/web/shared-worker/_generated/comm_query_executor.wasm index 0000000000000000000000000000000000000000..0000000000000000000000000000000000000000 GIT binary patch literal 0 Hc$@