@@ -603,34 +603,40 @@ module.exports = function register(socket, ctx) {
603603 }
604604
605605 // ── Persona detection (#86, #5349) ────────────────────
606- // Pattern: "::PersonaName actual message body" (colon optional after the
607- // name). The leading "::" is a deliberate, unambiguous trigger that does
608- // not conflict with any markdown syntax (unlike ">>", which the markdown
609- // renderer would turn into a nested blockquote). Persona names match
610- // against the sender's own user_personas (case-insensitive). When a
611- // match is found we strip the prefix from the stored content and stamp
612- // persona_id / persona_username / persona_avatar so the outgoing message
613- // displays as the persona while the real user_id (and therefore
614- // moderation) remains intact.
606+ // Pattern: "::PersonaName actual message body"
607+ // The leading "::" is a deliberate, unambiguous trigger that does not
608+ // conflict with any markdown syntax. We iterate over the sender's own
609+ // personas (longest name first to handle prefix ambiguity) and do a
610+ // case-insensitive prefix match — this correctly handles persona names
611+ // that contain spaces, which a single regex with [^\s] cannot do.
615612 let personaId = null ;
616613 let personaUsername = null ;
617614 let personaAvatar = null ;
618615 let finalContent = safeContent ;
619- {
620- const m = safeContent . match ( / ^ : : \s * ( [ ^ \s : ] [ ^ \s : ] { 0 , 31 } ) : ? \s + ( [ \s \S ] + ) $ / ) ;
621- if ( m ) {
622- const candidate = m [ 1 ] . trim ( ) ;
623- const body = m [ 2 ] ;
624- if ( candidate && body && body . trim ( ) . length > 0 ) {
625- const persona = db . prepare (
626- 'SELECT id, name, avatar FROM user_personas WHERE user_id = ? AND name = ? COLLATE NOCASE'
627- ) . get ( socket . user . id , candidate ) ;
628- if ( persona ) {
629- personaId = persona . id ;
630- personaUsername = persona . name ;
631- personaAvatar = persona . avatar || null ;
632- finalContent = body . trim ( ) ;
633- }
616+ if ( safeContent . startsWith ( '::' ) ) {
617+ const userPersonas = db . prepare (
618+ 'SELECT id, name, avatar FROM user_personas WHERE user_id = ?'
619+ ) . all ( socket . user . id ) ;
620+ // Sort longest name first to prefer the most specific match.
621+ userPersonas . sort ( ( a , b ) => b . name . length - a . name . length ) ;
622+ const lower = safeContent . toLowerCase ( ) ;
623+ for ( const persona of userPersonas ) {
624+ // Allow "::Name message" (space separator) or "::Name: message" (colon+space)
625+ const base = '::' + persona . name . toLowerCase ( ) ;
626+ let body = null ;
627+ if ( lower . startsWith ( base + ' ' ) ) {
628+ body = safeContent . slice ( base . length + 1 ) . trim ( ) ;
629+ } else if ( lower . startsWith ( base + ': ' ) ) {
630+ body = safeContent . slice ( base . length + 2 ) . trim ( ) ;
631+ } else if ( lower . startsWith ( base + ':' ) && safeContent . length > base . length + 1 ) {
632+ body = safeContent . slice ( base . length + 1 ) . trim ( ) ;
633+ }
634+ if ( body && body . length > 0 ) {
635+ personaId = persona . id ;
636+ personaUsername = persona . name ;
637+ personaAvatar = persona . avatar || null ;
638+ finalContent = body ;
639+ break ;
634640 }
635641 }
636642 }
0 commit comments