Skip to content

Commit e0a666c

Browse files
authored
Add PluralKit style command (limited) handler an proxy recognition (#550)
### Description Fixes #427 Fixes #425 (you can use shorthands, instead of `/usepmp`) #### PluralKit Compatibility and Per-Message Profile Enhancements - Added limited compatibility for PluralKit-style (`pk;member`) commands, allowing users to interact with PMPs using familiar PK syntax. - Introduced shorthand syntax for sending messages with a Persona, such as `✨:test`, and an option to enable/disable this in settings. - Added a new settings section (`PKCompatSettings`) in the Persona profile page to toggle PK command compatibility and shorthand proxying. #### Command Handling Improvements - Updated `/addpmp` and `/usepmp` command parsing to use regular expressions, fixing syntax issues and clarifying usage. The `/usepmp` command now has a new syntax and properly resets room associations when requested. - Added a new `/pmpproxy` command to associate a proxy with a profile, and integrated feedback for unsupported flags in `/usepmp`. #### Data Model and Utility Updates - Expanded the per-message profile data model to support compatibility versions and proxy associations, including new type definitions and parsing utilities. These changes collectively improve the user experience for those using per-message profiles and PluralKit-style workflows, and provide more flexible and robust command handling. #### Type of change - [x] Bug fix (non-breaking change which fixes an issue) - [x] New feature (non-breaking change which adds functionality) - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) - [ ] This change requires a documentation update ### Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my own code - [x] I have commented my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings ### AI disclosure: - [ ] Partially AI assisted (clarify which code was AI assisted and briefly explain what it does). - [ ] Fully AI generated (explain what all the generated code does in moderate detail). no AI was used in the creation of this PR
2 parents a912387 + 0c7911c commit e0a666c

13 files changed

Lines changed: 739 additions & 34 deletions
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
default: minor
3+
---
4+
5+
added a limited compatibility with `pk;member` commands

.changeset/add_pmp_shorthands.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
default: minor
3+
---
4+
5+
added option to use shorthands to send a message with a Persona, for example `✨:test`
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
default: patch
3+
---
4+
5+
rephrased the command describtion for `/usepmp` and made `/usepmp reset` actually reset the room association of the pmp

.changeset/fix_pmp_commands.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
default: patch
3+
---
4+
5+
fixed the syntax issues regarding `/addpmp` and `usepmp` (note that the syntax for `/usepmp` has changed)

src/app/features/room/RoomInput.tsx

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
useEffect,
88
useRef,
99
useState,
10+
useMemo,
1011
} from 'react';
1112
import { useAtom, useAtomValue } from 'jotai';
1213
import { isKeyHotkey } from 'is-hotkey';
@@ -144,6 +145,8 @@ import {
144145
import { Microphone, Stop } from '@phosphor-icons/react';
145146
import { getSupportedAudioExtension } from '$plugins/voice-recorder-kit/supportedCodec';
146147
import { sanitizeCustomHtml } from '$utils/sanitize';
148+
import { PKitCommandMessageHandler } from '$plugins/pluralkit-handler/PKitCommandMessageHandler';
149+
import { PKitProxyMessageHandler } from '$plugins/pluralkit-handler/PKitProxyMessageHandler';
147150
import { SchedulePickerDialog } from './schedule-send';
148151
import * as css from './schedule-send/SchedulePickerDialog.css';
149152
import {
@@ -249,6 +252,20 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
249252
const [hideActivity] = useSetting(settingsAtom, 'hideActivity');
250253
const [mentionInReplies] = useSetting(settingsAtom, 'mentionInReplies');
251254
const commands = useCommands(mx, room);
255+
/**
256+
* handle pluralkit-style messages
257+
*/
258+
const pluralkitCmdMessageHandler = useMemo(
259+
() => new PKitCommandMessageHandler(mx, room),
260+
[mx, room]
261+
);
262+
const pluralkitProxyMessageHandler = useMemo(() => new PKitProxyMessageHandler(mx), [mx]);
263+
useEffect(() => {
264+
pluralkitProxyMessageHandler.init();
265+
}, [pluralkitProxyMessageHandler]);
266+
267+
const [pkCompatEnable] = useSetting(settingsAtom, 'pkCompat');
268+
const [pmpProxyingEnable] = useSetting(settingsAtom, 'pmpProxying');
252269
const emojiBtnRef = useRef<HTMLButtonElement>(null);
253270
const micBtnRef = useRef<HTMLButtonElement>(null);
254271
const roomToParents = useAtomValue(roomToParentsAtom);
@@ -707,6 +724,13 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
707724
return;
708725
}
709726

727+
// check if its a pk command
728+
if (pkCompatEnable && PKitCommandMessageHandler.isPKCommand(plainText)) {
729+
pluralkitCmdMessageHandler.handleMessage(plainText);
730+
resetEditor(editor); // clear the editor
731+
return; // don't do anything besides handling the command
732+
}
733+
710734
if (commandName) {
711735
plainText = trimCommand(commandName, plainText);
712736
customHtml = trimCommand(commandName, customHtml);
@@ -763,8 +787,13 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
763787
* This allows the server to apply the correct profile-based transformations (e.g. font size adjustments) when processing the message,
764788
* and also allows clients to display an accurate preview of how the message will look with the profile applied while it's being composed.
765789
*/
766-
const perMessageProfile = await getCurrentlyUsedPerMessageProfileForRoom(mx, roomId);
790+
const perMessageProfile =
791+
pmpProxyingEnable && pluralkitProxyMessageHandler.isAProxiedMessage(plainText)
792+
? await pluralkitProxyMessageHandler.getPmpBasedOnMessage(plainText)
793+
: await getCurrentlyUsedPerMessageProfileForRoom(mx, roomId);
767794

795+
if (pmpProxyingEnable && pluralkitProxyMessageHandler.isAProxiedMessage(plainText))
796+
plainText = pluralkitProxyMessageHandler.stripProxyFromMessage(plainText) ?? plainText;
768797
if (perMessageProfile) {
769798
content['com.beeper.per_message_profile'] = convertPerMessageProfileToBeeperFormat(
770799
perMessageProfile,
@@ -894,19 +923,23 @@ export const RoomInput = forwardRef<HTMLDivElement, RoomInputProps>(
894923
}, [
895924
editor,
896925
replyEvent,
897-
isMarkdown,
898-
canSendReaction,
899926
mx,
900927
roomId,
928+
isMarkdown,
929+
canSendReaction,
930+
pkCompatEnable,
901931
replyDraft,
902932
silentReply,
933+
pmpProxyingEnable,
934+
pluralkitProxyMessageHandler,
903935
scheduledTime,
904936
editingScheduledDelayId,
905937
nicknames,
938+
room,
906939
handleQuickReact,
940+
pluralkitCmdMessageHandler,
907941
commands,
908942
sendTypingStatus,
909-
room,
910943
queryClient,
911944
threadRootId,
912945
setReplyDraft,
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
import { SequenceCard } from '$components/sequence-card';
2+
import { SettingTile } from '$components/setting-tile';
3+
import { useSetting } from '$state/hooks/settings';
4+
import { settingsAtom } from '$state/settings';
5+
import { Box, Switch, Text } from 'folds';
6+
import { SequenceCardStyle } from '../styles.css';
7+
8+
export function PKCompatSettings() {
9+
const [usePKCompat, setUsePKCompat] = useSetting(settingsAtom, 'pkCompat');
10+
const [usePmpProxying, setUsePmpProxying] = useSetting(settingsAtom, 'pmpProxying');
11+
12+
return (
13+
<Box direction="Column" gap="100">
14+
<Text size="L400">Limited Compatibility with PluralKit-like functions</Text>
15+
<SequenceCard
16+
className={SequenceCardStyle}
17+
variant="SurfaceVariant"
18+
direction="Column"
19+
gap="100"
20+
>
21+
<SettingTile
22+
title="Enable PK commands"
23+
description="If enabled, it will enable a few pk style commands, currently verry limited"
24+
after={
25+
<Switch
26+
variant="Primary"
27+
value={usePKCompat}
28+
onChange={setUsePKCompat}
29+
title={usePKCompat ? 'disable pk; commands' : 'enable pk; commands'}
30+
/>
31+
}
32+
/>
33+
<SettingTile
34+
title="Enable Shorthands"
35+
description="If enabled, you can use shorthands to use a Persona for one message only (eg. '✨:test')"
36+
after={
37+
<Switch
38+
variant="Primary"
39+
value={usePmpProxying}
40+
onChange={setUsePmpProxying}
41+
title={
42+
usePmpProxying
43+
? 'disable checking typed messages for shorthands'
44+
: 'enable checking typed messages for shorthands'
45+
}
46+
/>
47+
}
48+
/>
49+
</SequenceCard>
50+
</Box>
51+
);
52+
}

src/app/features/settings/Persona/ProfilesPage.tsx

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Page, PageHeader, PageNavContent } from '$components/page';
22
import { Box, IconButton, Icon, Icons, Text } from 'folds';
33
import { PerMessageProfileOverview } from './PerMessageProfileOverview';
4+
import { PKCompatSettings } from './PKCompat';
45

56
type PerMessageProfilePageProps = {
67
requestClose: () => void;
@@ -37,6 +38,7 @@ export function PerMessageProfilePage({ requestClose }: PerMessageProfilePagePro
3738
direction="Column"
3839
shrink="No"
3940
>
41+
<PKCompatSettings />
4042
<PerMessageProfileOverview />
4143
</Box>
4244
</PageNavContent>

src/app/hooks/useCommands.ts

Lines changed: 51 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import { useOpenBugReportModal } from '$state/hooks/bugReportModal';
3232
import { createRoomEncryptionState } from '$components/create-room';
3333
import { parsePronounsInput } from '$utils/pronouns';
3434
import { sendFeedback } from '$utils/sendFeedbackToUser';
35+
import { PKitCommandMessageHandler } from '$plugins/pluralkit-handler/PKitCommandMessageHandler';
3536
import { useRoomNavigate } from './useRoomNavigate';
3637
import { enrichWidgetUrl } from './useRoomWidgets';
3738
import { useUserProfile } from './useUserProfile';
@@ -50,6 +51,9 @@ const FLAG_PAT = String.raw`(?:^|\s)-(\w+)\b`;
5051
const FLAG_REG = new RegExp(FLAG_PAT);
5152
const FLAG_REG_G = new RegExp(FLAG_PAT, 'g');
5253

54+
const ADDPMP_REGEX = /(\w+) (name=)?"?([\w\s]*)"? (avatar=)?([\w.:/]+)/;
55+
const USEPMP_REGEX = /^(\w+)\s*(-g)?(-o)?(-u)?\s*(\d+)?$/;
56+
5357
export const splitPayloadContentAndFlags = (payload: string): [string, string | undefined] => {
5458
const flagMatch = new RegExp(FLAG_REG).exec(payload);
5559

@@ -242,6 +246,7 @@ export enum Command {
242246
AddPerMessageProfileToAccount = 'addpmp',
243247
DeletePerMessageProfileFromAccount = 'delpmp',
244248
UsePerMessageProfile = 'usepmp',
249+
AssociateProxyPerMessageProfile = 'pmpproxy',
245250
Pronoun = 'pronoun',
246251
SPronoun = 'spronoun',
247252
Rainbow = 'rainbow',
@@ -281,6 +286,8 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
281286
const { navigateRoom } = useRoomNavigate();
282287
const [developerTools] = useSetting(settingsAtom, 'developerTools');
283288
const [enableMSC4268CMD] = useSetting(settingsAtom, 'enableMSC4268CMD');
289+
// helper for pkit commands
290+
const pkitcmdHandler = useMemo(() => new PKitCommandMessageHandler(mx, room), [mx, room]);
284291
const profile = useUserProfile(mx.getSafeUserId());
285292
const openBugReport = useOpenBugReportModal();
286293

@@ -494,26 +501,19 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
494501
'Add or update a per message profile to your account. Example: /addpmp profileId name=Profile Name avatar=mxc://xyzabc',
495502
exe: async (payload) => {
496503
// Parse key=value pairs
497-
const parts = payload.split(' ');
498-
let avatarUrl: string | undefined;
499-
let name: string | undefined;
500-
parts.forEach((part, index) => {
501-
const [key, value] = part.split('=');
502-
if (key && value) {
503-
if (key === 'name' || key === 'avatar') {
504-
if (key === 'name') {
505-
name = parts
506-
.slice(index)
507-
.map((p) => p.split('=')[1])
508-
.join(' ');
509-
return;
510-
}
511-
if (key === 'avatar') avatarUrl = value;
512-
}
513-
}
514-
});
504+
const args = ADDPMP_REGEX.exec(payload);
505+
if (!args) {
506+
sendFeedback(`invalid payload`, room, mx.getSafeUserId());
507+
return;
508+
}
509+
const avatarUrl: string | undefined = args[5];
510+
const name: string | undefined = args[3];
511+
const profileId = args[1];
515512

516-
const profileId = parts[0]; // profileId is positional (before any key=)
513+
if (!avatarUrl || !name || !profileId) {
514+
sendFeedback(`invalid payload`, room, mx.getSafeUserId());
515+
return;
516+
}
517517

518518
const pmp: PerMessageProfile = {
519519
id: profileId,
@@ -567,13 +567,28 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
567567
[Command.UsePerMessageProfile]: {
568568
name: Command.UsePerMessageProfile,
569569
description:
570-
'Use a per message profile for this room once, or until reset. Example: /usepmp profileId [once,reset,or duration like 1h30m]',
570+
'Use a per message profile for this room once, or until reset. Example: /usepmp (profileId,reset) [-o,-u,-g] [ts]',
571571
exe: async (payload) => {
572-
// this command doesn't need to do anything, the composer will pick it up and apply the profile to the message being composed
573-
const profileId: string = splitWithSpace(payload)[0];
574-
const durationStr: string | undefined = splitWithSpace(payload)[1];
575-
let validUntil: number | undefined;
576-
if (durationStr === 'reset') {
572+
const args = USEPMP_REGEX.exec(payload);
573+
if (!args) {
574+
sendFeedback(`invalid payload`, room, mx.getSafeUserId());
575+
return;
576+
}
577+
const profileId = args[1];
578+
const globalFlag = args[2] !== undefined;
579+
const onceFlag = args[3] !== undefined;
580+
// const untilFlag = args[4] !== undefined;
581+
const validUntil = Number.parseInt(args[5], 10);
582+
if (onceFlag || globalFlag) {
583+
sendFeedback(
584+
'Currently not implemented, consider using shorthands, with /pmpproxy id ✨:text',
585+
room,
586+
mx.getSafeUserId()
587+
);
588+
return;
589+
}
590+
591+
if (profileId.normalize() === 'reset') {
577592
setCurrentlyUsedPerMessageProfileIdForRoom(mx, room.roomId, undefined, undefined, true)
578593
.then(() => {
579594
sendFeedback('Per message profile reset for this room.', room, mx.getSafeUserId());
@@ -591,7 +606,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
591606
.then(() => {
592607
sendFeedback(
593608
`Per message profile "${profileId}" will be used for messages in this room for the until ${
594-
durationStr ?? 'reset'
609+
validUntil ?? 'reset'
595610
}. Use \`/usepmp reset\` to reset it at any time.`,
596611
room,
597612
mx.getSafeUserId()
@@ -606,6 +621,15 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
606621
});
607622
},
608623
},
624+
[Command.AssociateProxyPerMessageProfile]: {
625+
name: Command.AssociateProxyPerMessageProfile,
626+
description: 'Associate proxy with a profile. Example /pmpproxy id ✨:text',
627+
exe: async (payload) => {
628+
const pid: string = splitWithSpace(payload)[0];
629+
const proxy: string = splitWithSpace(payload)[1];
630+
pkitcmdHandler.handleMessage(`pk;member "${pid}" proxy ${proxy}`, true);
631+
},
632+
},
609633
[Command.MyRoomAvatar]: {
610634
name: Command.MyRoomAvatar,
611635
description: 'Change profile picture in current room. Example /myroomavatar mxc://xyzabc',
@@ -1552,6 +1576,7 @@ export const useCommands = (mx: MatrixClient, room: Room): CommandRecord => {
15521576
room,
15531577
profile.displayName,
15541578
profile.avatarUrl,
1579+
pkitcmdHandler,
15551580
developerTools,
15561581
enableMSC4268CMD,
15571582
openBugReport,

0 commit comments

Comments
 (0)