diff --git a/.changeset/update_msc4461_support_to_v3.md b/.changeset/update_msc4461_support_to_v3.md new file mode 100644 index 0000000000..37b5ba3f39 --- /dev/null +++ b/.changeset/update_msc4461_support_to_v3.md @@ -0,0 +1,5 @@ +--- +default: minor +--- + +Update MSC4461 (Persona) support to v3 diff --git a/src/app/features/room/composerMessage.test.ts b/src/app/features/room/composerMessage.test.ts index 70b90b7d3d..db3b75b15b 100644 --- a/src/app/features/room/composerMessage.test.ts +++ b/src/app/features/room/composerMessage.test.ts @@ -41,7 +41,6 @@ const mx = { const profile = (id: string, displayname: string): PerMessageProfileMsc4461 => ({ id, displayname, - trigger: { prefix: [] }, }); /** Mirrors a command selected from autocomplete in the engine-neutral document. */ @@ -179,7 +178,7 @@ describe('buildOutgoingMessage', () => { }); it('strips a pluralkit proxy wrapper and lets its profile win', async () => { - const proxied = { ...profile('proxy', 'Proxied'), trigger: { prefix: ['A: '] } }; + const proxied = { ...profile('proxy', 'Proxied'), triggers: [{ prefix: 'A: ' }] }; profiles.account = proxied; const result = await build('A: hello there', { diff --git a/src/app/features/room/persona-picker/PersonaPicker.test.tsx b/src/app/features/room/persona-picker/PersonaPicker.test.tsx index 37ff256003..3ea3c9aafc 100644 --- a/src/app/features/room/persona-picker/PersonaPicker.test.tsx +++ b/src/app/features/room/persona-picker/PersonaPicker.test.tsx @@ -135,8 +135,8 @@ function deferred() { } const profiles: PerMessageProfileMsc4461[] = [ - { id: 'first', displayname: 'First', trigger: { prefix: [] } }, - { id: 'second', displayname: 'Second', trigger: { prefix: [] } }, + { id: 'first', displayname: 'First' }, + { id: 'second', displayname: 'Second' }, ]; type TestMatrixClient = MatrixClient & { emitAccountData: (type: string) => void }; @@ -198,12 +198,12 @@ describe('PersonaPicker async flows', () => { view.rerender({ mx: secondClient }); await act(async () => { - secondFetch.resolve([{ id: 'new', displayname: 'New', trigger: { prefix: [] } }]); + secondFetch.resolve([{ id: 'new', displayname: 'New' }]); await secondFetch.promise; }); await waitFor(() => expect(view.result.current.profiles?.[0]?.id).toBe('new')); await act(async () => { - firstFetch.resolve([{ id: 'old', displayname: 'Old', trigger: { prefix: [] } }]); + firstFetch.resolve([{ id: 'old', displayname: 'Old' }]); await firstFetch.promise; }); diff --git a/src/app/features/settings/Persona/PerMessageProfileEditor.test.tsx b/src/app/features/settings/Persona/PerMessageProfileEditor.test.tsx index 37ee53708a..79a5a7d649 100644 --- a/src/app/features/settings/Persona/PerMessageProfileEditor.test.tsx +++ b/src/app/features/settings/Persona/PerMessageProfileEditor.test.tsx @@ -34,7 +34,7 @@ describe('PerMessageProfileEditor', () => { mx={{} as MatrixClient} profileId="old-id" displayName="New Profile" - shorthands={{ prefix: [] }} + shorthands={[]} /> ); diff --git a/src/app/features/settings/Persona/PerMessageProfileEditor.tsx b/src/app/features/settings/Persona/PerMessageProfileEditor.tsx index 039aa3dfed..f95470d61a 100644 --- a/src/app/features/settings/Persona/PerMessageProfileEditor.tsx +++ b/src/app/features/settings/Persona/PerMessageProfileEditor.tsx @@ -24,20 +24,17 @@ import { SettingTile } from '$components/setting-tile'; import { AsyncStatus, useAsyncCallback } from '$hooks/useAsyncCallback'; import { NameColorEditor } from '../account/NameColorEditor'; import { - MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME, - MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_SUFFIX_PROPERTY_NAME, MATRIX_UNSTABLE_COLORS, MATRIX_UNSTABLE_PROFILE_PRONOUNS_PROPERTY_NAME, } from '$unstable/prefixes'; import { accessibleColor } from '$plugins/color'; import { ThemeKind } from '$hooks/useTheme'; -type Shorthand = { prefix?: string; suffix?: string }; -type ShorthandRow = Shorthand & { id: string }; +type ShorthandRow = ProfileTrigger & { id: string }; type ShorthandListItemProps = ShorthandRow & { onDelete: (shorthandId: string) => void; - onChange: (shorthandId: string, shorthand: Shorthand) => void; + onChange: (shorthandId: string, shorthand: ProfileTrigger) => void; }; function ShorthandListItem({ id, prefix, suffix, onDelete, onChange }: ShorthandListItemProps) { const [newPrefix, setNewPrefix] = useState(prefix); @@ -136,46 +133,17 @@ function ShorthandListItem({ id, prefix, suffix, onDelete, onChange }: Shorthand ); } -function triggersToShorthandRows(trigger: ProfileTrigger): ShorthandRow[] { - const prefixes: ShorthandRow[] = trigger.prefix.map((str) => { - return { prefix: str, id: nanoid() }; - }); - const suffixes: ShorthandRow[] | undefined = trigger['net.f0rest.suffix']?.map((str) => { - return { suffix: str, id: nanoid() }; - }); - const circumfixes: ShorthandRow[] | undefined = trigger['net.f0rest.circumfix']?.map( - ({ prefix, suffix }) => { - return { prefix: prefix, suffix: suffix, id: nanoid() }; - } - ); - - return prefixes.concat(suffixes ?? [], circumfixes ?? []); +function triggersToShorthandRows(triggers: ProfileTrigger[]): ShorthandRow[] { + return triggers.map(({ prefix, suffix, keep_trigger }) => ({ + prefix, + suffix, + keep_trigger, + id: nanoid(), + })); } -function shorthandRowsToTriggers(rows: ShorthandRow[]): ProfileTrigger { - const prefix: string[] = []; - const suffix: string[] = []; - const circumfix: { prefix: string; suffix: string }[] = []; - - rows.forEach((row) => { - if (row.prefix && row.suffix) { - circumfix.push({ prefix: row.prefix, suffix: row.suffix }); - } else if (row.prefix) { - prefix.push(row.prefix); - } else if (row.suffix) { - suffix.push(row.suffix); - } - }); - - return { - prefix, - ...(suffix.length > 0 - ? { [MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_SUFFIX_PROPERTY_NAME]: suffix } - : {}), - ...(circumfix.length > 0 - ? { [MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME]: circumfix } - : {}), - }; +function shorthandRowsToTriggers(rows: ShorthandRow[]): ProfileTrigger[] { + return rows.map(({ prefix, suffix, keep_trigger }) => ({ prefix, suffix, keep_trigger })); } /** * the props we use for the per-message profile editor, which is used to edit a per-message profile. This is used in the settings page when the user wants to edit a profile. @@ -188,7 +156,7 @@ export type PerMessageProfileEditorProps = { pronouns?: PronounSet[]; nameColorLightTheme?: string; nameColorDarkTheme?: string; - shorthands?: ProfileTrigger; + shorthands?: ProfileTrigger[]; onDelete?: (profileId: string) => void; }; @@ -255,7 +223,7 @@ export function PerMessageProfileEditor({ setNewShorthands((s) => s?.filter((shorthand) => shorthand.id !== id)); }; - const handleSaveShorthand = (oldId: string, shorthand: Shorthand) => { + const handleSaveShorthand = (oldId: string, shorthand: ProfileTrigger) => { setNewShorthands((rows = []) => { const index = rows.findIndex((row) => row.id === oldId); if (index === -1) return rows; @@ -358,7 +326,7 @@ export function PerMessageProfileEditor({ displayname: newDisplayName, avatar_url: avatarMxc, [MATRIX_UNSTABLE_PROFILE_PRONOUNS_PROPERTY_NAME]: newPronouns, - trigger: shorthandRowsToTriggers(newShorthands ?? []), + triggers: shorthandRowsToTriggers(newShorthands ?? []), [MATRIX_UNSTABLE_COLORS]: { on_light: newNameColorLight ?? undefined, on_dark: newNameColorDark ?? undefined, diff --git a/src/app/features/settings/Persona/PerMessageProfileOverview.tsx b/src/app/features/settings/Persona/PerMessageProfileOverview.tsx index 1bb3bce9c8..e0b9cab40c 100644 --- a/src/app/features/settings/Persona/PerMessageProfileOverview.tsx +++ b/src/app/features/settings/Persona/PerMessageProfileOverview.tsx @@ -78,7 +78,7 @@ export function PerMessageProfileOverview({ const newProfile: PerMessageProfileMsc4461 = { id: generateShortId(5), displayname: 'New Profile', - trigger: { prefix: [] }, + triggers: [], }; await addOrUpdatePerMessageProfile(mx, newProfile); onCreateProfile(newProfile); diff --git a/src/app/features/settings/Persona/ProfilesPage.tsx b/src/app/features/settings/Persona/ProfilesPage.tsx index 60512ac983..93746e430b 100644 --- a/src/app/features/settings/Persona/ProfilesPage.tsx +++ b/src/app/features/settings/Persona/ProfilesPage.tsx @@ -31,7 +31,7 @@ export function PerMessageProfilePage({ requestBack, requestClose }: PerMessageP pronouns={editingProfile['io.fsky.nyx.pronouns']} nameColorLightTheme={editingProfile['eu.she-a.color']?.on_light} nameColorDarkTheme={editingProfile['eu.she-a.color']?.on_dark} - shorthands={editingProfile.trigger} + shorthands={editingProfile.triggers ?? []} requestClose={handleEditorClose} /> ); diff --git a/src/app/hooks/commands/pmp.ts b/src/app/hooks/commands/pmp.ts index 7af7046dfc..70f0837de1 100644 --- a/src/app/hooks/commands/pmp.ts +++ b/src/app/hooks/commands/pmp.ts @@ -39,7 +39,6 @@ export const createPmpCommands = (ctx: CommandContext): Partial = id: profileId, displayname: name || '', avatar_url, - trigger: { prefix: [] }, }; await addOrUpdatePerMessageProfile(mx, pmp) .then(() => { diff --git a/src/app/hooks/usePerMessageProfile.test.ts b/src/app/hooks/usePerMessageProfile.test.ts index 3eecb286e5..21391f6f57 100644 --- a/src/app/hooks/usePerMessageProfile.test.ts +++ b/src/app/hooks/usePerMessageProfile.test.ts @@ -4,6 +4,7 @@ import type { MatrixClient } from '$types/matrix-sdk'; import { MATRIX_SABLE_UNSTABLE_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME, MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME, + MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME_V2, } from '$unstable/prefixes'; import { addOrUpdatePerMessageProfile, @@ -12,7 +13,7 @@ import { renamePerMessageProfile, type PerMessageProfileMsc4461, } from './usePerMessageProfile'; -import type { PersonaCatalogContent } from '$app/persona/catalog'; +import type { PersonaCatalogContent, PersonaV2 } from '$app/persona/catalog'; import { projectPersona } from '$app/persona/projection'; import { resolvePersonaProxy } from '$app/persona/proxy'; import { resolvePersona } from '$app/persona/selection'; @@ -47,15 +48,24 @@ function createMatrixClient(profiles: PerMessageProfileMsc4461[] = []) { const profile = (id: string): PerMessageProfileMsc4461 => ({ id, displayname: `Profile ${id}`, - trigger: { prefix: [] }, + triggers: [], +}); + +const profileV2 = (id: string): PersonaV2 => ({ + id, + displayname: `Profile ${id}`, + trigger: { + prefix: [], + }, }); describe('profile persistence', () => { it('normalizes the previously nested MSC4461 account-data payload', async () => { const { accountData, mx } = createMatrixClient(); - accountData.set(MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME, { + accountData.delete(MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME); + accountData.set(MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME_V2, { type: 'm.per_message_profiles', - content: { profiles: [profile('first')] }, + content: { profiles: [profileV2('first')] }, }); await expect(getAllPerMessageProfiles(mx)).resolves.toEqual([profile('first')]); @@ -83,11 +93,7 @@ describe('profile persistence', () => { id: 'legacy', displayname: 'Legacy', avatar_url: 'mxc://example.org/avatar', - trigger: { - prefix: [], - 'net.f0rest.suffix': [], - 'net.f0rest.circumfix': [], - }, + triggers: [], }, ]); expect( @@ -98,6 +104,41 @@ describe('profile persistence', () => { ).toBeUndefined(); }); + it('migrates MSC4461 catalog from v2 to v3', async () => { + const { accountData, mx } = createMatrixClient(); + accountData.delete(MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME); + accountData.set(MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME_V2, { + profiles: [ + { + id: 'v2id', + displayname: 'V2 Persona', + 'com.example.unknown-third-party': 'foobar', + trigger: { + prefix: ['a: '], + 'net.f0rest.suffix': [' :b'], + 'net.f0rest.circumfix': [{ prefix: '[', suffix: ']' }], + }, + }, + ], + }); + + const catalog = { + profiles: [ + { + id: 'v2id', + displayname: 'V2 Persona', + 'com.example.unknown-third-party': 'foobar', + triggers: [{ prefix: '[', suffix: ']' }, { prefix: 'a: ' }, { suffix: ' :b' }], + }, + ], + }; + + await expect(getAllPerMessageProfiles(mx)).resolves.toEqual(catalog.profiles); + expect( + accountData.get(MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME) + ).toEqual(catalog); + }); + it('cleans up an empty legacy index', async () => { const { accountData, mx } = createMatrixClient(); accountData.delete(MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME); @@ -116,13 +157,17 @@ describe('profile persistence', () => { }); }); - it('filters malformed catalog and legacy profile entries', async () => { + it('filters malformed legacy profile entries', async () => { const { accountData, mx } = createMatrixClient(); - accountData.set(MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME, { - profiles: [profile('valid'), { id: 'missing-trigger', displayname: 'Invalid' }], + accountData.delete(MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME); + accountData.set(MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME_V2, { + profiles: [profileV2('valid'), { id: 'missing-trigger', displayname: 'Invalid' }], }); await expect(getAllPerMessageProfiles(mx)).resolves.toEqual([profile('valid')]); + }); + it('filters malformed catalog v2 profile entries', async () => { + const { accountData, mx } = createMatrixClient(); accountData.delete(MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME); accountData.set(`${MATRIX_SABLE_UNSTABLE_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME}.index`, { profileIds: ['valid', 'invalid', 1], @@ -138,7 +183,7 @@ describe('profile persistence', () => { { id: 'valid', displayname: 'Valid', - trigger: { prefix: [], 'net.f0rest.suffix': [], 'net.f0rest.circumfix': [] }, + triggers: [], }, ]); }); @@ -226,8 +271,8 @@ describe('profile persistence', () => { describe('persona resolution', () => { const personas = [ - { ...profile('first'), trigger: { prefix: ['first: '] } }, - { ...profile('second'), trigger: { prefix: ['second: '] } }, + { ...profile('first'), triggers: [{ prefix: 'first: ' }] }, + { ...profile('second'), triggers: [{ prefix: 'second: ' }] }, ]; it('applies precedence and ignores expired selections', () => { @@ -258,10 +303,10 @@ describe('persona resolution', () => { }); it('strips suffix and circumfix triggers', () => { - const suffix = { ...personas[0]!, trigger: { prefix: [], 'net.f0rest.suffix': [' -a'] } }; + const suffix = { ...personas[0]!, triggers: [{ suffix: ' -a' }] }; const circumfix = { ...personas[1]!, - trigger: { prefix: [], 'net.f0rest.circumfix': [{ prefix: '[', suffix: ']' }] }, + triggers: [{ prefix: '[', suffix: ']' }], }; expect(resolvePersonaProxy([suffix], 'hello -a')).toEqual({ persona: suffix, body: 'hello' }); diff --git a/src/app/persona/catalog.test.ts b/src/app/persona/catalog.test.ts index be4d5cd1bb..2bc2e164de 100644 --- a/src/app/persona/catalog.test.ts +++ b/src/app/persona/catalog.test.ts @@ -4,6 +4,7 @@ import type { MatrixClient } from '$types/matrix-sdk'; import { MATRIX_SABLE_UNSTABLE_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME, MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME, + MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME_V2, } from '$unstable/prefixes'; import { ProfileCatalog } from './catalog'; @@ -32,11 +33,11 @@ function createMatrixClient(accountData: Map, writable = false) } describe('ProfileCatalog', () => { - it('filters personas with malformed optional trigger variants', async () => { + it('filters v2 personas with malformed optional trigger variants', async () => { const { mx } = createMatrixClient( new Map([ [ - MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME, + MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME_V2, { profiles: [ { id: 'valid', displayname: 'Valid', trigger: { prefix: [] } }, @@ -57,7 +58,7 @@ describe('ProfileCatalog', () => { ); await expect(new ProfileCatalog(mx).list({ migrate: false })).resolves.toEqual([ - { id: 'valid', displayname: 'Valid', trigger: { prefix: [] } }, + { id: 'valid', displayname: 'Valid', triggers: [] }, ]); }); @@ -85,7 +86,7 @@ describe('ProfileCatalog', () => { const accountData = new Map([ [ MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME, - { profiles: [{ id: 'old', displayname: 'Old', trigger: { prefix: [] } }] }, + { profiles: [{ id: 'old', displayname: 'Old', triggers: [] }] }, ], [ `${MATRIX_SABLE_UNSTABLE_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME}.globalassociation`, @@ -118,8 +119,8 @@ describe('ProfileCatalog', () => { MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME, { profiles: [ - { id: 'deleted', displayname: 'Deleted', trigger: { prefix: [] } }, - { id: 'kept', displayname: 'Kept', trigger: { prefix: [] } }, + { id: 'deleted', displayname: 'Deleted', triggers: [] }, + { id: 'kept', displayname: 'Kept', triggers: [] }, ], }, ], @@ -161,13 +162,13 @@ describe('ProfileCatalog', () => { const catalog = new ProfileCatalog(mx); await Promise.all([ - catalog.merge({ id: 'first', displayname: 'First', trigger: { prefix: [] } }), - catalog.merge({ id: 'second', displayname: 'Second', trigger: { prefix: [] } }), + catalog.merge({ id: 'first', displayname: 'First', triggers: [] }), + catalog.merge({ id: 'second', displayname: 'Second', triggers: [] }), ]); await expect(catalog.list()).resolves.toEqual([ - { id: 'first', displayname: 'First', trigger: { prefix: [] } }, - { id: 'second', displayname: 'Second', trigger: { prefix: [] } }, + { id: 'first', displayname: 'First', triggers: [] }, + { id: 'second', displayname: 'Second', triggers: [] }, ]); }); diff --git a/src/app/persona/catalog.ts b/src/app/persona/catalog.ts index b31601ac54..5e2fed3cf1 100644 --- a/src/app/persona/catalog.ts +++ b/src/app/persona/catalog.ts @@ -4,11 +4,13 @@ import { CustomAccountDataEvent } from '$types/matrix/accountData'; import type { ColorSet } from '$hooks/useUserProfile'; import { type PronounSet } from '$utils/pronouns'; import { createKeyedQueue } from '$utils/keyedQueue'; +import type { MATRIX_UNSTABLE_PROFILE_PKIT_IMPORT_PROPERTY_NAME } from '$unstable/prefixes'; import { MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME, MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_SUFFIX_PROPERTY_NAME, MATRIX_UNSTABLE_COLORS, MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME, + MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME_V2, MATRIX_UNSTABLE_PROFILE_PRONOUNS_PROPERTY_NAME, } from '$unstable/prefixes'; import type { @@ -16,6 +18,7 @@ import type { PerMessageProfileMsc4461, ProfileTrigger, ResolvedPersonaSelection, + PkitImport, } from './index'; const ACCOUNT_DATA_PREFIX = CustomAccountDataEvent.SablePerProfileMessageProfiles; @@ -32,17 +35,39 @@ type LegacyProfile = { type LegacyProfileIndex = { profileIds: string[]; compat: AccountDataCompatVersion }; +export type ProfileV2Trigger = { + prefix: string[]; + [MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_SUFFIX_PROPERTY_NAME]?: string[]; + [MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME]?: { + prefix: string; + suffix: string; + }[]; +}; + +export type PersonaV2 = { + id: string; + displayname: string; + avatar_url?: string; + [MATRIX_UNSTABLE_PROFILE_PRONOUNS_PROPERTY_NAME]?: PronounSet[]; + [MATRIX_UNSTABLE_COLORS]?: ColorSet; + [MATRIX_UNSTABLE_PROFILE_PKIT_IMPORT_PROPERTY_NAME]?: PkitImport; + trigger: ProfileV2Trigger; + compat?: AccountDataCompatVersion; +}; + export type PerMessageProfileIndexMsc4461 = { type: 'm.per_message_profiles'; content: { profiles: PerMessageProfileMsc4461[] }; }; -export type PersonaCatalogContent = { profiles: Persona[] }; -type InvalidPersonaCatalogContent = { +export type PersonaCatalogContentV2 = { profiles: PersonaV2[] }; +type InvalidPersonaCatalogContentV2 = { type: 'm.per_message_profiles'; - content: PersonaCatalogContent; + content: PersonaCatalogContentV2; }; +export type PersonaCatalogContent = { profiles: Persona[] }; + type ProfileAssociation = { profileId: string; validUntil?: number }; type RoomAssociationWrapper = { associations: Map | Record; @@ -87,6 +112,7 @@ type ProxyAssociationWrapper = { export function isPersonaAccountDataEvent(eventType: string) { return ( eventType === MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME || + eventType === MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME_V2 || eventType.startsWith(`${ACCOUNT_DATA_PREFIX}.`) ); } @@ -95,7 +121,40 @@ function accountData(mx: MatrixClient, eventType: string) { return mx.getAccountData(eventType as Parameters[0]); } -function isCircumfix(value: unknown): value is { prefix: string; suffix: string } { +function isTrigger(value: unknown): value is ProfileTrigger { + let trigger = value as { + prefix?: unknown; + suffix?: unknown; + keep_trigger?: unknown; + }; + + return ( + typeof trigger === 'object' && + trigger !== null && + !Array.isArray(trigger) && + (trigger.prefix === undefined || typeof trigger.prefix === 'string') && + (trigger.suffix === undefined || typeof trigger.suffix === 'string') && + (trigger.keep_trigger === undefined || typeof trigger.keep_trigger == 'boolean') + ); +} + +function isPersona(value: unknown): value is Persona { + const persona = value as { + id?: unknown; + displayname?: unknown; + triggers?: unknown; + }; + const triggers = persona.triggers; + return ( + typeof value === 'object' && + value !== null && + typeof persona.id === 'string' && + typeof persona.displayname === 'string' && + (triggers === undefined || (Array.isArray(triggers) && triggers.every(isTrigger))) + ); +} + +function isCircumfixV2(value: unknown): value is { prefix: string; suffix: string } { return ( typeof value === 'object' && value !== null && @@ -104,7 +163,7 @@ function isCircumfix(value: unknown): value is { prefix: string; suffix: string ); } -function isPersona(value: unknown): value is Persona { +function isPersonaV2(value: unknown): value is PersonaV2 { const persona = value as { id?: unknown; displayname?: unknown; @@ -127,10 +186,35 @@ function isPersona(value: unknown): value is Persona { ))) && (trigger[MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME] === undefined || (Array.isArray(trigger[MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME]) && - trigger[MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME].every(isCircumfix))) + trigger[MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME].every( + isCircumfixV2 + ))) + ); +} + +function isCatalogContentV2(value: unknown): value is PersonaCatalogContentV2 { + return ( + typeof value === 'object' && + value !== null && + 'profiles' in value && + Array.isArray(value.profiles) ); } +function readCatalogV2(mx: MatrixClient): PersonaCatalogContentV2 | undefined { + const content = accountData( + mx, + MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME_V2 + )?.getContent(); + if (isCatalogContentV2(content)) return { profiles: content.profiles.filter(isPersonaV2) }; + + const nested = content as InvalidPersonaCatalogContentV2 | undefined; + if (isCatalogContentV2(nested?.content)) { + return { profiles: nested.content.profiles.filter(isPersona) }; + } + return undefined; +} + function isCatalogContent(value: unknown): value is PersonaCatalogContent { return ( typeof value === 'object' && @@ -140,21 +224,12 @@ function isCatalogContent(value: unknown): value is PersonaCatalogContent { ); } -function readCatalog(mx: MatrixClient): { profiles: Persona[]; nested: boolean } | undefined { +function readCatalog(mx: MatrixClient): { profiles: Persona[] } | undefined { const content = accountData( mx, MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME )?.getContent(); - if (isCatalogContent(content)) - return { profiles: content.profiles.filter(isPersona), nested: false }; - - const nested = content as InvalidPersonaCatalogContent | undefined; - if (isCatalogContent(nested?.content)) { - return { - profiles: nested.content.profiles.filter(isPersona), - nested: nested.type === 'm.per_message_profiles', - }; - } + if (isCatalogContent(content)) return { profiles: content.profiles.filter(isPersona) }; return undefined; } @@ -231,12 +306,32 @@ export function parsePerMessageProfileProxyAssociation( }; } -export function convertPmpToMsc4461(mx: MatrixClient, profile: LegacyProfile): Persona { - const trigger: ProfileTrigger = { - prefix: [], - [MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_SUFFIX_PROPERTY_NAME]: [], - [MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME]: [], +export function convertMsc4461V2ToV3(profile: PersonaV2): Persona { + let { trigger: triggerV2, ...profile_other } = profile; + let triggerV3: ProfileTrigger[] = []; + + for (let { prefix, suffix } of triggerV2[ + MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME + ] ?? []) { + triggerV3.push({ prefix, suffix }); + } + + for (let prefix of triggerV2.prefix ?? []) { + triggerV3.push({ prefix }); + } + + for (let suffix of triggerV2[MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_SUFFIX_PROPERTY_NAME] ?? []) { + triggerV3.push({ suffix }); + } + + return { + ...profile_other, + triggers: triggerV3, }; +} + +export function convertPmpToMsc4461(mx: MatrixClient, profile: LegacyProfile): Persona { + const triggers: ProfileTrigger[] = []; proxyAssociationMap( accountData(mx, `${ACCOUNT_DATA_PREFIX}.proxyassociation`)?.getContent() as | ProxyAssociationWrapper @@ -247,14 +342,9 @@ export function convertPmpToMsc4461(mx: MatrixClient, profile: LegacyProfile): P .forEach(([key, association]) => { const migrated = migratePmpProxyAssociation(key, association); if (!migrated) return; - if (migrated.prefix && !migrated.suffix) trigger.prefix.push(migrated.prefix); - else if (!migrated.prefix && migrated.suffix) - trigger[MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_SUFFIX_PROPERTY_NAME]!.push(migrated.suffix); - else if (migrated.prefix && migrated.suffix) - trigger[MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME]!.push({ - prefix: migrated.prefix, - suffix: migrated.suffix, - }); + if (migrated.prefix || migrated.suffix) { + triggers.push({ prefix: migrated.prefix, suffix: migrated.suffix }); + } }); const persona: Persona = { id: profile.id, @@ -262,7 +352,7 @@ export function convertPmpToMsc4461(mx: MatrixClient, profile: LegacyProfile): P avatar_url: profile.avatarUrl, [MATRIX_UNSTABLE_PROFILE_PRONOUNS_PROPERTY_NAME]: profile.pronouns, [MATRIX_UNSTABLE_COLORS]: profile.colors, - trigger, + triggers, }; if (!profile.avatarUrl) delete persona.avatar_url; if (!profile.pronouns?.length) delete persona[MATRIX_UNSTABLE_PROFILE_PRONOUNS_PROPERTY_NAME]; @@ -276,10 +366,18 @@ export class ProfileCatalog { private async load(migrate: boolean): Promise { const catalog = readCatalog(this.mx); if (catalog) { - if (migrate && catalog.nested) await saveCatalog(this.mx, catalog.profiles); return catalog.profiles; } + const catalogV2 = readCatalogV2(this.mx); + if (catalogV2) { + const profiles = catalogV2.profiles.map(convertMsc4461V2ToV3); + if (migrate) { + await saveCatalog(this.mx, profiles); + } + return profiles; + } + const index = accountData(this.mx, `${ACCOUNT_DATA_PREFIX}.index`); if (!index) return []; const profileIds = (index.getContent() as LegacyProfileIndex | undefined)?.profileIds; diff --git a/src/app/persona/index.ts b/src/app/persona/index.ts index 7b227d341d..bbdbb0c1bd 100644 --- a/src/app/persona/index.ts +++ b/src/app/persona/index.ts @@ -1,8 +1,6 @@ import type { AccountDataCompatVersion } from '$types/matrix/accountData'; import type { PronounSet } from '$utils/pronouns'; import type { - MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME, - MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_SUFFIX_PROPERTY_NAME, MATRIX_UNSTABLE_COLORS, MATRIX_UNSTABLE_PROFILE_PKIT_IMPORT_PROPERTY_NAME, MATRIX_UNSTABLE_PROFILE_PRONOUNS_PROPERTY_NAME, @@ -10,12 +8,9 @@ import type { import type { ColorSet } from '$hooks/useUserProfile'; export type ProfileTrigger = { - prefix: string[]; - [MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_SUFFIX_PROPERTY_NAME]?: string[]; - [MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME]?: { - prefix: string; - suffix: string; - }[]; + prefix?: string; + suffix?: string; + keep_trigger?: boolean; }; export type PkitImport = { @@ -32,7 +27,7 @@ export type PerMessageProfileMsc4461 = { [MATRIX_UNSTABLE_PROFILE_PRONOUNS_PROPERTY_NAME]?: PronounSet[]; [MATRIX_UNSTABLE_COLORS]?: ColorSet; [MATRIX_UNSTABLE_PROFILE_PKIT_IMPORT_PROPERTY_NAME]?: PkitImport; - trigger: ProfileTrigger; + triggers?: ProfileTrigger[]; compat?: AccountDataCompatVersion; }; diff --git a/src/app/persona/pluralkit.test.ts b/src/app/persona/pluralkit.test.ts index 5e3107eb30..8bb4c948c9 100644 --- a/src/app/persona/pluralkit.test.ts +++ b/src/app/persona/pluralkit.test.ts @@ -86,9 +86,11 @@ describe('PluralkitImport', () => { { id: 'foo', displayname: 'Foo Bar', - trigger: { - prefix: ['Foo:'], - }, + triggers: [ + { + prefix: 'Foo:', + }, + ], [MATRIX_UNSTABLE_PROFILE_PKIT_IMPORT_PROPERTY_NAME]: { id: '0', uuid: 'f00b4r' }, }, ]); @@ -99,7 +101,7 @@ describe('PluralkitImport', () => { [ MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME, { - profiles: [{ id: 'valid', displayname: 'Valid', trigger: { prefix: [] } }], + profiles: [{ id: 'valid', displayname: 'Valid', triggers: [] }], }, ], ]), @@ -124,13 +126,15 @@ describe('PluralkitImport', () => { const catalog = new ProfileCatalog(mx); await expect(importPluralkitMembers(mx, catalog, pkData)).resolves.not.toThrow(); await expect(catalog.list()).resolves.toEqual([ - { id: 'valid', displayname: 'Valid', trigger: { prefix: [] } }, + { id: 'valid', displayname: 'Valid', triggers: [] }, { id: 'foo', displayname: 'Foo Bar', - trigger: { - prefix: ['Foo:'], - }, + triggers: [ + { + prefix: 'Foo:', + }, + ], [MATRIX_UNSTABLE_PROFILE_PKIT_IMPORT_PROPERTY_NAME]: { id: '0', uuid: 'f00b4r' }, }, ]); @@ -146,9 +150,11 @@ describe('PluralkitImport', () => { { id: 'foo', displayname: 'Foo Bar', - trigger: { - prefix: ['Foo:'], - }, + triggers: [ + { + prefix: 'Foo:', + }, + ], [MATRIX_UNSTABLE_COLORS]: { on_dark: '#ffffff', on_light: '#000000', @@ -183,9 +189,11 @@ describe('PluralkitImport', () => { { id: 'bar', displayname: 'Bar Bar', - trigger: { - prefix: ['Foo:'], - }, + triggers: [ + { + prefix: 'Foo:', + }, + ], [MATRIX_UNSTABLE_COLORS]: { on_dark: '#ffffff', on_light: '#000000', diff --git a/src/app/persona/projection.ts b/src/app/persona/projection.ts index a657cd5e7f..b7b5ae0beb 100644 --- a/src/app/persona/projection.ts +++ b/src/app/persona/projection.ts @@ -1,11 +1,9 @@ import { - MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME, - MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_SUFFIX_PROPERTY_NAME, MATRIX_UNSTABLE_COLORS, MATRIX_UNSTABLE_PROFILE_PKIT_IMPORT_PROPERTY_NAME, MATRIX_UNSTABLE_PROFILE_PRONOUNS_PROPERTY_NAME, } from '$unstable/prefixes'; -import type { PerMessageProfileBeeperFormat, Persona, ProfileTrigger } from './index'; +import type { PerMessageProfileBeeperFormat, Persona } from './index'; import chroma from 'chroma-js'; import { ThemeKind } from '$hooks/useTheme'; import { accessibleColor } from '$plugins/color'; @@ -47,7 +45,7 @@ export function convertBeeperFormatToOurPerMessageProfile( [MATRIX_UNSTABLE_PROFILE_PRONOUNS_PROPERTY_NAME]: beeperProfile[MATRIX_UNSTABLE_PROFILE_PRONOUNS_PROPERTY_NAME], [MATRIX_UNSTABLE_COLORS]: beeperProfile[MATRIX_UNSTABLE_COLORS], - trigger: { prefix: [] }, + triggers: [], }; } @@ -79,31 +77,16 @@ export function convertPluralkitFormatToOurPerMessageProfile( } // parse proxy tags - const trigger: ProfileTrigger = { - prefix: [], - }; - if (pkitProfile.proxy_tags) { - pkitProfile.proxy_tags.forEach( - ({ prefix, suffix }: { prefix: string | null; suffix: string | null }) => { - if (prefix && suffix) { - (trigger[MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME] ??= []).push({ - prefix, - suffix, - }); - } else if (!prefix && suffix) { - (trigger[MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_SUFFIX_PROPERTY_NAME] ??= []).push(suffix); - } else if (prefix && !suffix) { - trigger.prefix.push(prefix); - } - } - ); - } + const triggers = pkitProfile.proxy_tags?.map(({ prefix, suffix }) => ({ + prefix: prefix ?? undefined, + suffix: suffix ?? undefined, + })); const profile: Persona = { id: pkitProfile.name, displayname: pkitProfile.display_name ?? pkitProfile.name, avatar_url: pkitAvatarUrl, - trigger, + triggers, [MATRIX_UNSTABLE_PROFILE_PRONOUNS_PROPERTY_NAME]: parsePronounsInput(pkitProfile.pronouns), [MATRIX_UNSTABLE_PROFILE_PKIT_IMPORT_PROPERTY_NAME]: { id: pkitProfile.id, diff --git a/src/app/persona/proxy.test.ts b/src/app/persona/proxy.test.ts index cc243c0d71..e95e5bef6c 100644 --- a/src/app/persona/proxy.test.ts +++ b/src/app/persona/proxy.test.ts @@ -7,16 +7,22 @@ describe('resolvePersonaProxy', () => { const persona: Persona = { id: 'persona', displayname: 'Persona', - trigger: { - prefix: ['p: '], - 'net.f0rest.suffix': [' :p'], - 'net.f0rest.circumfix': [{ prefix: '[', suffix: ']' }], - }, + triggers: [ + { prefix: 'r: ' }, + { suffix: ' :r' }, + { prefix: '[', suffix: ']', keep_trigger: false }, + { prefix: 'd: ', keep_trigger: true }, + { suffix: ' :d', keep_trigger: true }, + { prefix: '<', suffix: '>', keep_trigger: true }, + ], }; it('strips prefix, suffix, and circumfix triggers', () => { - expect(resolvePersonaProxy([persona], 'p: hello')?.body).toBe('hello'); - expect(resolvePersonaProxy([persona], 'hello :p')?.body).toBe('hello'); + expect(resolvePersonaProxy([persona], 'r: hello')?.body).toBe('hello'); + expect(resolvePersonaProxy([persona], 'hello :r')?.body).toBe('hello'); expect(resolvePersonaProxy([persona], '[hello]')?.body).toBe('hello'); + expect(resolvePersonaProxy([persona], 'd: hello')?.body).toBe('d: hello'); + expect(resolvePersonaProxy([persona], 'hello :d')?.body).toBe('hello :d'); + expect(resolvePersonaProxy([persona], '')?.body).toBe(''); }); }); diff --git a/src/app/persona/proxy.ts b/src/app/persona/proxy.ts index c6ada03db5..2bfd1befb8 100644 --- a/src/app/persona/proxy.ts +++ b/src/app/persona/proxy.ts @@ -1,28 +1,19 @@ -import { - MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME, - MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_SUFFIX_PROPERTY_NAME, -} from '$unstable/prefixes'; import type { Persona } from './index'; /** Resolves the first matching MSC4461 trigger and strips it. */ export function resolvePersonaProxy(personas: readonly Persona[], body: string) { for (const persona of personas) { - const prefix = persona.trigger.prefix.find((trigger) => body.startsWith(trigger)); - if (prefix !== undefined) return { persona, body: body.slice(prefix.length).trimStart() }; + const trigger = persona.triggers?.find( + ({ prefix, suffix }) => body.startsWith(prefix ?? '') && body.endsWith(suffix ?? '') + ); - const suffix = persona.trigger[ - MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_SUFFIX_PROPERTY_NAME - ]?.find((trigger) => body.endsWith(trigger)); - if (suffix !== undefined) return { persona, body: body.slice(0, -suffix.length).trimEnd() }; + if (trigger) { + if (!trigger.keep_trigger) { + body = body.slice(trigger.prefix?.length ?? 0, body.length - (trigger.suffix?.length ?? 0)); + } - const circumfix = persona.trigger[ - MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME - ]?.find(({ prefix: start, suffix: end }) => body.startsWith(start) && body.endsWith(end)); - if (circumfix !== undefined) - return { - persona, - body: body.slice(circumfix.prefix.length, -circumfix.suffix.length).trim(), - }; + return { persona, body }; + } } return undefined; } diff --git a/src/app/persona/selection.test.ts b/src/app/persona/selection.test.ts index 5e118224be..99a305daf2 100644 --- a/src/app/persona/selection.test.ts +++ b/src/app/persona/selection.test.ts @@ -3,7 +3,7 @@ import { describe, expect, it } from 'vitest'; import type { Persona } from './index'; import { resolvePersona } from './selection'; -const persona = (id: string): Persona => ({ id, displayname: id, trigger: { prefix: [] } }); +const persona = (id: string): Persona => ({ id, displayname: id }); describe('resolvePersona', () => { it('uses proxy, latched, room, then account precedence', () => { diff --git a/src/app/plugins/pluralkit-handler/PKitCommandMessageHandler.ts b/src/app/plugins/pluralkit-handler/PKitCommandMessageHandler.ts index 8315448bbb..10934b4e72 100644 --- a/src/app/plugins/pluralkit-handler/PKitCommandMessageHandler.ts +++ b/src/app/plugins/pluralkit-handler/PKitCommandMessageHandler.ts @@ -1,7 +1,6 @@ import type { PerMessageProfileMsc4461, PerMessageProfileProxyAssociationV2, - ProfileTrigger, } from '$hooks/usePerMessageProfile'; import { addOrUpdatePerMessageProfile, @@ -12,10 +11,6 @@ import { import { sendFeedback } from '$utils/sendFeedbackToUser'; import type { MatrixClient, Room } from '$types/matrix-sdk'; import { generateShortId } from '$utils/shortIdGen'; -import { - MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME, - MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_SUFFIX_PROPERTY_NAME, -} from '$unstable/prefixes'; const pkMemberRenameRegex = /^(pk;member)\s+"?([\w\s]+)"?\s*rename\s+"?([\w\s]+)"?$/; const pkMemberNewRegex = /^(pk;member)\s+new\s+"?([\w\s]+)"?$/; @@ -50,65 +45,6 @@ export function buildProxyRegex({ prefix, suffix }: PerMessageProfileProxyAssoci return new RegExp(`^${pattern}$`); } -export function testTriggers(triggers: ProfileTrigger, input: string): boolean { - const matchesPrefix = triggers.prefix.some((prefix) => input.startsWith(prefix)); - if (matchesPrefix) return true; - - const matchesSuffix = triggers[MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_SUFFIX_PROPERTY_NAME]?.some( - (suffix) => input.startsWith(suffix) - ); - if (matchesSuffix) return true; - - const matchesCircumfix = triggers[ - MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME - ]?.some(({ prefix, suffix }) => { - const casePrefix = prefix ? input.startsWith(prefix) : true; - const caseSuffix = suffix ? input.endsWith(suffix) : true; - - return casePrefix && caseSuffix; - }); - - return !!matchesCircumfix; -} - -export function testProxy( - { prefix, suffix }: PerMessageProfileProxyAssociationV2, - input: string -): boolean { - const matchesPrefix = prefix ? input.startsWith(prefix) : true; - const matchesSuffix = suffix ? input.endsWith(suffix) : true; - - return matchesPrefix && matchesSuffix; -} - -export function stripTrigger(triggers: ProfileTrigger, input: string): string { - const prefix = triggers.prefix.find((value) => input.startsWith(value)); - if (prefix !== undefined) return stripProxy({ prefix }, input); - - const suffix = triggers[MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_SUFFIX_PROPERTY_NAME]?.find( - (value) => input.endsWith(value) - ); - if (suffix !== undefined) return stripProxy({ suffix }, input); - - const circumfix = triggers[MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME]?.find( - (trigger) => input.startsWith(trigger.prefix) && input.endsWith(trigger.suffix) - ); - if (circumfix) return stripProxy(circumfix, input); - - return input; -} - -export function stripProxy( - { prefix, suffix }: { prefix?: string; suffix?: string }, - input: string -): string { - let message = input; - if (prefix) message = message.slice(prefix.length); - if (suffix) message = message.slice(0, message.length - suffix.length); - - return message; -} - /** * a class to use as PluralKit command message handler * @@ -171,7 +107,7 @@ export class PKitCommandMessageHandler { await addOrUpdatePerMessageProfile(this.mx, { id: generatedID, displayname: memberName, - trigger: { prefix: [] }, + triggers: [], }); sendFeedback( `added new member has been created with id: ${generatedID} and name ${memberName}`, @@ -271,19 +207,11 @@ export class PKitCommandMessageHandler { if (pmp && proxyTags) { const { prefix, suffix } = proxyTags; - if (prefix && suffix) { - pmp.trigger[MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME] ??= []; - pmp.trigger[MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME] = pmp.trigger[ - MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME - ].filter((trigger) => trigger.prefix !== prefix && trigger.suffix !== suffix); - } else if (prefix && !suffix) { - pmp.trigger.prefix = pmp.trigger.prefix.filter((trigger) => trigger !== prefix); - } else if (!prefix && suffix) { - pmp.trigger[MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_SUFFIX_PROPERTY_NAME] ??= []; - pmp.trigger[MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_SUFFIX_PROPERTY_NAME] = pmp.trigger[ - MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_SUFFIX_PROPERTY_NAME - ].filter((trigger) => trigger !== suffix); + if (!pmp.triggers) { + pmp.triggers = []; } + pmp.triggers.push({ prefix, suffix }); + await addOrUpdatePerMessageProfile(this.mx, pmp); sendFeedback( @@ -330,18 +258,11 @@ export class PKitCommandMessageHandler { if (pmp) { const { prefix, suffix } = proxyTags; - if (prefix && suffix) { - (pmp.trigger[MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_CIRCUMFIX_PROPERTY_NAME] ??= []).push({ - prefix: prefix, - suffix: suffix, - }); - } else if (!prefix && suffix) { - (pmp.trigger[MATRIX_SABLE_UNSTABLE_MSC4461_TRIGGER_SUFFIX_PROPERTY_NAME] ??= []).push( - suffix - ); - } else if (prefix && !suffix) { - pmp.trigger.prefix.push(prefix); + if (!pmp.triggers) { + pmp.triggers = []; } + pmp.triggers.push({ prefix, suffix }); + await addOrUpdatePerMessageProfile(this.mx, pmp); sendFeedback( `Persona with ${this.useIdInsteadOfNameWherePossible ? 'id' : 'name'} "${name}" (${pmpId}) is now associated with ${matchAgainst}`, diff --git a/src/app/plugins/pluralkit-handler/PKitProxyMessageHandler.test.ts b/src/app/plugins/pluralkit-handler/PKitProxyMessageHandler.test.ts deleted file mode 100644 index 68f859b4c6..0000000000 --- a/src/app/plugins/pluralkit-handler/PKitProxyMessageHandler.test.ts +++ /dev/null @@ -1,65 +0,0 @@ -import type { Mock } from 'vitest'; -import { describe, expect, it, vi, beforeEach } from 'vitest'; - -import { PKitProxyMessageHandler } from './PKitProxyMessageHandler'; -import type { MatrixClient } from '$types/matrix-sdk'; - -// Mock the hook module that provides proxy associations + profile lookup -vi.mock('$hooks/usePerMessageProfile', () => ({ - getAllPerMessageProfiles: vi.fn<() => Promise>(), - getPerMessageProfileById: vi.fn<() => Promise>(), - parsePerMessageProfileProxyAssociation: vi.fn<() => unknown>(), -})); - -const mocked = await import('$hooks/usePerMessageProfile'); - -describe('PKitProxyMessageHandler', () => { - beforeEach(() => { - vi.resetAllMocks(); - }); - - it('returns false for isAProxiedMessage before init', () => { - const handler = new PKitProxyMessageHandler({} as unknown as MatrixClient); - expect(handler.isAProxiedMessage('[test] hi')).toBe(false); - }); - - it('matches a proxied message, returns pmp, and strips content', async () => { - (mocked.getAllPerMessageProfiles as unknown as Mock).mockResolvedValueOnce([ - { - id: 'p1', - name: 'Test', - trigger: { - prefix: [], - 'net.f0rest.circumfix': [{ prefix: '[', suffix: ']' }], - }, - }, - ]); - (mocked.getPerMessageProfileById as unknown as Mock).mockResolvedValueOnce({ - id: 'p1', - name: 'Test', - trigger: { - prefix: [], - 'net.f0rest.circumfix': [{ prefix: '[', suffix: ']' }], - }, - }); - - const handler = new PKitProxyMessageHandler({} as unknown as MatrixClient); - - const pmp = await handler.getPmpBasedOnMessage('[hello]'); - expect(pmp).toEqual({ - id: 'p1', - name: 'Test', - trigger: { - prefix: [], - 'net.f0rest.circumfix': [{ prefix: '[', suffix: ']' }], - }, - }); - - // getPmpBasedOnMessage refreshes/init() so we should be inited now - expect(handler.isAProxiedMessage('[hello]')).toBe(true); - expect(handler.stripProxyFromMessage('[hello]')).toBe('hello'); - - expect(handler.isAProxiedMessage('[hello\nworld]')).toBe(true); - expect(handler.stripProxyFromMessage('[hello\nworld]')).toBe('hello\nworld'); - }); -}); diff --git a/src/app/plugins/pluralkit-handler/PKitProxyMessageHandler.ts b/src/app/plugins/pluralkit-handler/PKitProxyMessageHandler.ts deleted file mode 100644 index bfb3eaa659..0000000000 --- a/src/app/plugins/pluralkit-handler/PKitProxyMessageHandler.ts +++ /dev/null @@ -1,95 +0,0 @@ -import type { PerMessageProfileMsc4461 } from '$hooks/usePerMessageProfile'; -import { getAllPerMessageProfiles, getPerMessageProfileById } from '$hooks/usePerMessageProfile'; -import type { MatrixClient } from '$types/matrix-sdk'; -import { stripTrigger, testTriggers } from './PKitCommandMessageHandler'; - -/** - * proxy message handler - * @author Rye - */ -export class PKitProxyMessageHandler { - /** - * the matrix client we use, we init that in the constructor - * - * @private - * @type {MatrixClient} - * @memberof PKitProxyMessageHandler - */ - private readonly mx: MatrixClient; - - /** - * a list of profiles; is not initialized in the constructor - * @private - * @type {PerMessageProfileMsc4461[]} - * @memberof PKitProxyMessageHandler - */ - private profiles: PerMessageProfileMsc4461[]; - - private succInit: boolean; - - /** - * a pk proxy message handler - * @param mx the matrix client - */ - public constructor(mx: MatrixClient) { - this.mx = mx; - this.profiles = []; - this.succInit = false; - } - - /** - * initialize the handler, as this is not necessarily fast, it shouldn't happen in the constructor - */ - public async init(): Promise { - try { - this.profiles = await getAllPerMessageProfiles(this.mx); - this.succInit = true; - } catch (err) { - this.succInit = false; - throw new Error(`failed to init pmp proxy handler: ${String(err)}`, { - cause: err, - }); - } - } - - /** - * you should probably check this before running `getPmpBasedOnMessage`, as this is faster - * @param message the message to check - */ - public isAProxiedMessage(message: string): boolean { - if (!this.succInit) return false; - return this.profiles.some((profile) => testTriggers(profile.trigger, message)); - } - - /** - * get PmP based on message - * @param message the message to look at - * @returns the matching Per-Message-Profile, if any - */ - public async getPmpBasedOnMessage( - message: string - ): Promise { - // Always refresh so newly-added proxies apply immediately. - await this.init(); - // check if the message matches our formats - const profileId = this.profiles.find((profile) => testTriggers(profile.trigger, message))?.id; - if (!profileId) return undefined; - return getPerMessageProfileById(this.mx, profileId); - } - - /** - * this runs synchronously, so it needs to be inited beforehand - * - * @param {string} message the message you want to extract from - * @return {*} {(string | undefined)} the message without the proxy - * @memberof PKitProxyMessageHandler - */ - public stripProxyFromMessage(message: string): string | undefined { - if (!this.succInit) return undefined; - let m; - this.profiles.forEach((profile) => { - if (testTriggers(profile.trigger, message)) m = stripTrigger(profile.trigger, message); - }); - return m; - } -} diff --git a/src/unstable/prefixes/sable/accountdata.ts b/src/unstable/prefixes/sable/accountdata.ts index 7d33034e4b..2131fc9a8a 100644 --- a/src/unstable/prefixes/sable/accountdata.ts +++ b/src/unstable/prefixes/sable/accountdata.ts @@ -11,8 +11,10 @@ export const MATRIX_SABLE_UNSTABLE_ACCOUNT_SETTINGS_PROPERTY_NAME = 'moe.sable.a */ export const MATRIX_SABLE_UNSTABLE_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME = 'fyi.cisnt.permessageprofile'; -export const MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME = +export const MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME_V2 = 'fi.mau.msc4461.per_message_profiles.v2'; +export const MATRIX_UNSTABLE_MSC4461_ACCOUNT_PER_MESSAGE_PROFILES_PROPERTY_NAME = + 'fi.mau.msc4461.per_message_profiles.v3'; export const MATRIX_SABLE_UNSTABLE_DISMISSED_INVITES = 'moe.sable.dismissed_invites'; export const MATRIX_SABLE_UNSTABLE_ACCOUNT_ADDED_SERVERS_PROPERTY_NAME = 'moe.sable.added_servers';