Skip to content

Commit 5f22d86

Browse files
committed
feat(pkit-compat): some steps towards recognizing proxies, not entirely there yet
1 parent 3f706fe commit 5f22d86

7 files changed

Lines changed: 379 additions & 35 deletions

File tree

src/app/features/room/RoomInput.tsx

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,7 +161,8 @@ import {
161161
import { Microphone, Stop } from '@phosphor-icons/react';
162162
import { getSupportedAudioExtension } from '$plugins/voice-recorder-kit/supportedCodec';
163163
import { sanitizeCustomHtml } from '$utils/sanitize';
164-
import { PluralKitCommandMessageHandler } from '$plugins/pluralkit-handler/pluralkitMessageHandler';
164+
import { PKitCommandMessageHandler } from '$plugins/pluralkit-handler/PKitCommandMessageHandler';
165+
import { PKitProxyMessageHandler } from '$plugins/pluralkit-handler/PKitProxyMessageHandler';
165166
import { SchedulePickerDialog } from './schedule-send';
166167
import * as css from './schedule-send/SchedulePickerDialog.css';
167168
import {
@@ -265,9 +266,15 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
265266
* handle pluralkit-style messages
266267
*/
267268
const pluralkitCmdMessageHandler = useMemo(
268-
() => new PluralKitCommandMessageHandler(mx, room),
269+
() => new PKitCommandMessageHandler(mx, room),
269270
[mx, room]
270271
);
272+
const pluralkitProxyMessageHandler = useMemo(() => new PKitProxyMessageHandler(mx), [mx]);
273+
useEffect(() => {
274+
pluralkitProxyMessageHandler.init();
275+
}, [pluralkitProxyMessageHandler]);
276+
277+
const pkCompatEnable = useSetting(settingsAtom, 'pkCompat');
271278
const emojiBtnRef = useRef<HTMLButtonElement>(null);
272279
const micBtnRef = useRef<HTMLButtonElement>(null);
273280
const roomToParents = useAtomValue(roomToParentsAtom);
@@ -718,7 +725,8 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
718725
return;
719726
}
720727

721-
if (PluralKitCommandMessageHandler.isPKCommand(plainText)) {
728+
// check if its a pk command
729+
if (pkCompatEnable && PKitCommandMessageHandler.isPKCommand(plainText)) {
722730
pluralkitCmdMessageHandler.handleMessage(plainText);
723731
}
724732

@@ -778,8 +786,13 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
778786
* This allows the server to apply the correct profile-based transformations (e.g. font size adjustments) when processing the message,
779787
* and also allows clients to display an accurate preview of how the message will look with the profile applied while it's being composed.
780788
*/
781-
const perMessageProfile = await getCurrentlyUsedPerMessageProfileForRoom(mx, roomId);
789+
const perMessageProfile =
790+
pkCompatEnable && pluralkitProxyMessageHandler.isAProxiedMessage(plainText)
791+
? await pluralkitProxyMessageHandler.getPmpBasedOnMessage(plainText)
792+
: await getCurrentlyUsedPerMessageProfileForRoom(mx, roomId);
782793

794+
if (pkCompatEnable && pluralkitProxyMessageHandler.isAProxiedMessage(plainText))
795+
plainText = pluralkitProxyMessageHandler.stripProxyFromMessage(plainText) ?? plainText;
783796
if (perMessageProfile) {
784797
content['com.beeper.per_message_profile'] =
785798
convertPerMessageProfileToBeeperFormat(perMessageProfile);
@@ -909,6 +922,8 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
909922
roomId,
910923
isMarkdown,
911924
canSendReaction,
925+
pkCompatEnable,
926+
pluralkitProxyMessageHandler,
912927
replyDraft,
913928
silentReply,
914929
scheduledTime,

src/app/hooks/usePerMessageProfile.ts

Lines changed: 191 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1+
import { AccountDataCompatVersion, AccountDataEvent } from '$types/matrix/accountData';
12
import { PronounSet } from '$utils/pronouns';
23
import { MatrixClient } from 'matrix-js-sdk';
34

4-
const ACCOUNT_DATA_PREFIX = 'fyi.cisnt.permessageprofile';
5+
const ACCOUNT_DATA_PREFIX = AccountDataEvent.SablePerProfileMessageProfiles;
56

67
/**
78
* a per message profile
@@ -26,6 +27,7 @@ export type PerMessageProfile = {
2627
* @see PronounSet for the format of the pronouns, and how to parse them from a string input
2728
*/
2829
pronouns?: PronounSet[];
30+
compat?: AccountDataCompatVersion;
2931
};
3032

3133
/**
@@ -96,6 +98,7 @@ type PerMessageProfileIndex = {
9698
* a list of all profile ids, used to list all profiles when the user wants to manage them.
9799
*/
98100
profileIds: string[];
101+
compat: AccountDataCompatVersion;
99102
};
100103

101104
/**
@@ -106,6 +109,64 @@ type PerMessageProfileRoomAssociation = {
106109
validUntil?: number;
107110
};
108111

112+
/**
113+
* associating a profile by proxy
114+
* @author Rye
115+
*/
116+
export type PerMessageProfileProxyAssociation = {
117+
/**
118+
* the profile associated with the proxy
119+
*/
120+
profileId: string;
121+
/**
122+
* regex (string representation of it) to handle the proxy
123+
*/
124+
regexString: string;
125+
/**
126+
* optional parameter to save when the proxy was added
127+
*/
128+
setAt?: number;
129+
};
130+
131+
export type InternalPerMessageProfileProxyAssociation = {
132+
/**
133+
* the profile associated with the proxy
134+
*/
135+
profileId: string;
136+
/**
137+
* regex to handle the proxy
138+
*/
139+
regex: RegExp;
140+
/**
141+
* optional parameter to save when the proxy was added
142+
*/
143+
setAt?: number;
144+
};
145+
146+
export function parsePerMessageProfileProxyAssociation(
147+
assoc: PerMessageProfileProxyAssociation
148+
): InternalPerMessageProfileProxyAssociation {
149+
return {
150+
profileId: assoc.profileId,
151+
// we need to remove artifacts from the toString conversion
152+
regex: new RegExp(assoc.regexString.slice(1, -1)),
153+
setAt: assoc.setAt,
154+
} satisfies InternalPerMessageProfileProxyAssociation;
155+
}
156+
157+
type PerMessageProfileProxyAssociationWrapper = {
158+
/**
159+
* the associations saved in the wrapper
160+
*/
161+
associations:
162+
| Map<string, PerMessageProfileProxyAssociation>
163+
| Record<string, PerMessageProfileProxyAssociation>;
164+
/**
165+
* optional parameter to save compatibility information
166+
*/
167+
compat?: AccountDataCompatVersion;
168+
};
169+
109170
/**
110171
* the shape of the account data for room associations, which is a wrapper around a list of associations.
111172
* This is used to store the associations in account data, and allows us to easily add additional fields in the future if needed without breaking the existing data structure.
@@ -120,9 +181,14 @@ type PerMessageProfileRoomAssociationWrapper = {
120181
associations:
121182
| Map<string, PerMessageProfileRoomAssociation>
122183
| Record<string, PerMessageProfileRoomAssociation>;
184+
compat?: AccountDataCompatVersion;
123185
};
124186

125-
// Helper to always get a Map from wrapper
187+
/**
188+
* unwrap a profile-room-associations-wrapper
189+
* @param wrapper the wrapper to unwrap
190+
* @returns unwrapped map for profile-room-associations
191+
*/
126192
function getAssociationsMap(
127193
wrapper?: PerMessageProfileRoomAssociationWrapper
128194
): Map<string, PerMessageProfileRoomAssociation> {
@@ -138,6 +204,33 @@ function associationsMapToObject(
138204
return Object.fromEntries(map);
139205
}
140206

207+
/**
208+
* helper function (similar to getAssociationsMap for Room associations)
209+
* @param wrapper the wrapper to unwrap
210+
* @returns unwrapped map of proxy associations
211+
*/
212+
function getProxyAssociationMap(
213+
wrapper?: PerMessageProfileProxyAssociationWrapper
214+
): Map<string, PerMessageProfileProxyAssociation> {
215+
if (!wrapper?.associations) return new Map();
216+
if (wrapper.associations instanceof Map) return wrapper.associations;
217+
return new Map(Object.entries(wrapper.associations));
218+
}
219+
220+
function proxyAssociationsMapToObject(
221+
map: Map<string, PerMessageProfileProxyAssociation>
222+
): Record<string, PerMessageProfileProxyAssociation> {
223+
return Object.fromEntries(map);
224+
}
225+
226+
/**
227+
* getting a profile from the account data where the profile matches a given id
228+
*
229+
* @export
230+
* @param {MatrixClient} mx the matrix client
231+
* @param {string} id the profile id
232+
* @return {*} {(Promise<PerMessageProfile | undefined>)} the profile, with the profile Id, if it exists
233+
*/
141234
export async function getPerMessageProfileById(
142235
mx: MatrixClient,
143236
id: string
@@ -146,24 +239,47 @@ export async function getPerMessageProfileById(
146239
return profile ? (profile.getContent() as unknown as PerMessageProfile) : undefined;
147240
}
148241

242+
/**
243+
* getting an array of all PerMessageProfile's saved in the account data
244+
*
245+
* @export
246+
* @param {MatrixClient} mx the matrix client
247+
* @return {*} {Promise<PerMessageProfile[]>} a array containing all per-message-profiles saved
248+
*/
149249
export async function getAllPerMessageProfiles(mx: MatrixClient): Promise<PerMessageProfile[]> {
150250
const profileData = mx.getAccountData(`${ACCOUNT_DATA_PREFIX}.index` as any);
151251
const profileIds = (profileData?.getContent() as PerMessageProfileIndex)?.profileIds || [];
152252
const profiles = await Promise.all(profileIds.map((id) => getPerMessageProfileById(mx, id)));
153253
return profiles.filter((profile): profile is PerMessageProfile => profile !== undefined);
154254
}
155255

256+
/**
257+
* add or update a pmp
258+
* @param mx the matrix client
259+
* @param profile the profile to add/update
260+
* @returns void
261+
*/
156262
export function addOrUpdatePerMessageProfile(mx: MatrixClient, profile: PerMessageProfile) {
157263
const profileListIndex = mx.getAccountData(`${ACCOUNT_DATA_PREFIX}.index` as any);
264+
const profileWithCompat = {
265+
...profile,
266+
compat: {
267+
version: 1,
268+
compatDate: '2026-03-26',
269+
} satisfies AccountDataCompatVersion,
270+
} satisfies PerMessageProfile;
158271
if (profileListIndex?.getContent()?.profileIds.includes(profile.id)) {
159272
// profile already exists, just update it
160-
return mx.setAccountData(`${ACCOUNT_DATA_PREFIX}.${profile.id}` as any, profile as any);
273+
return mx.setAccountData(
274+
`${ACCOUNT_DATA_PREFIX}.${profile.id}` as any,
275+
profileWithCompat as any
276+
);
161277
}
162278
// profile doesn't exist, add it to the index and then add the profile data
163279
const newProfileIds = [...(profileListIndex?.getContent()?.profileIds || []), profile.id];
164280
return Promise.all([
165281
mx.setAccountData(`${ACCOUNT_DATA_PREFIX}.index` as any, { profileIds: newProfileIds } as any),
166-
mx.setAccountData(`${ACCOUNT_DATA_PREFIX}.${profile.id}` as any, profile as any),
282+
mx.setAccountData(`${ACCOUNT_DATA_PREFIX}.${profile.id}` as any, profileWithCompat as any),
167283
]);
168284
}
169285

@@ -235,6 +351,77 @@ export async function setCurrentlyUsedPerMessageProfileIdForRoom(
235351
}
236352

237353
/**
354+
*
355+
* @param mx the matrix client
356+
* @param profileId the profile id which the prefix should be attached to
357+
* @param proxy the prefix to use as index
358+
* @param proxyRegExp the regex we can use to match the prefix
359+
* @param reset wheather to delete the prefix
360+
*/
361+
export async function associateProxyWithProfile(
362+
mx: MatrixClient,
363+
profileId: string | undefined,
364+
proxy: string,
365+
proxyRegExp: RegExp,
366+
reset: boolean
367+
) {
368+
const associations = getProxyAssociationMap(
369+
mx.getAccountData(`${ACCOUNT_DATA_PREFIX}.proxyassociation` as any)?.getContent()
370+
);
371+
372+
if (reset) associations.delete(proxy);
373+
374+
if (!profileId) throw new Error('profileId might not be undefined');
375+
if (profileId)
376+
associations.set(proxy, {
377+
profileId,
378+
regexString: proxyRegExp.toString(),
379+
} satisfies PerMessageProfileProxyAssociation);
380+
mx.setAccountData(
381+
`${ACCOUNT_DATA_PREFIX}.proxyassociation` as any,
382+
{ associations: proxyAssociationsMapToObject(associations) } as any
383+
);
384+
}
385+
386+
/**
387+
* get a profile based on a proxy
388+
* @param mx the matrix client
389+
* @param proxy the proxy to look for
390+
* @returns the profile, if any, associated with the prefix
391+
*/
392+
export async function getProfileAssociatedWithProxy(
393+
mx: MatrixClient,
394+
proxy: string
395+
): Promise<PerMessageProfile | undefined> {
396+
const profileId = getAssociationsMap(
397+
mx.getAccountData(`${ACCOUNT_DATA_PREFIX}.proxyassociation` as any)?.getContent()
398+
).get(proxy)?.profileId;
399+
if (!profileId) return undefined;
400+
return getPerMessageProfileById(mx, profileId);
401+
}
402+
403+
/**
404+
*
405+
*
406+
* @export
407+
* @param {MatrixClient} mx the matrix client
408+
* @return {*} {Promise<PerMessageProfileProxyAssociation[]>}
409+
*/
410+
export async function getAllPerMessageProfileProxies(
411+
mx: MatrixClient
412+
): Promise<PerMessageProfileProxyAssociation[]> {
413+
const cont: PerMessageProfileProxyAssociationWrapper | undefined = mx
414+
.getAccountData(`${ACCOUNT_DATA_PREFIX}.proxyassociation` as any)
415+
?.getContent();
416+
if (!cont) return [];
417+
const pmap = getProxyAssociationMap(cont);
418+
const parr = new Array<PerMessageProfileProxyAssociation>();
419+
pmap.values().forEach((v) => parr.push(v));
420+
return parr;
421+
}
422+
423+
/**
424+
*
238425
* drops all room associations for a profile, used when deleting a profile to make sure there are no dangling associations left that point to a non existing profile, which could cause issues when trying to apply the profile to a message in a room that still has an association for the deleted profile.
239426
*
240427
* @param {MatrixClient} mx the matrix client

0 commit comments

Comments
 (0)