diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el index 335896db13d..d14d85913b3 100644 --- a/lisp/erc/erc.el +++ b/lisp/erc/erc.el @@ -7991,11 +7991,28 @@ See associated unit test for precise behavior." (match-string 2 string) (match-string 3 string)))) -(defun erc--shuffle-nuh-nickward (nick login host) - "Interpret results of `erc--parse-nuh', promoting loners to nicks." - (cond (nick (cl-assert (null login)) (list nick login host)) - ((and (null login) host) (list host nil nil)) - ((and login (null host)) (list login nil nil)))) +(defvar erc--user-nuh-message-types + '(PRIVMSG JOIN PART QUIT NICK KICK TOPIC AWAY ACCOUNT TAGMSG)) + +(defun erc--interpret-nuh (nuh &optional cmd noerrorp) + "Return new NUH triple with non-nil nickname or host component, or signal. +If CMD is null or appears in `erc--user-nuh-message-types', promote a +lone host to a lone nick. With NOERRORP, return a copy of NUH instead +of signaling." + (pcase-let ((`(,nick ,login ,host) nuh)) + (cond (nick (list nick login host)) + ((and (null login) host) + (if (or (null cmd) (memq cmd erc--user-nuh-message-types)) + (list host nil nil) + (list nil nil host))) + ((and login + (let ((types (or (erc--get-isupport-entry 'CHANTYPES 'single) + erc--fallback-channel-prefixes))) + (not (seq-some (lambda (c) (seq-contains-p types c #'eq)) + login)))) + (list login nil host)) + (noerrorp (list nick login host)) + (t (error "Failed to interpret: %s" nuh))))) (defun erc-extract-nick (string) "Return the nick corresponding to a user specification STRING. diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el index 3900f5d4880..35997a83de1 100644 --- a/test/lisp/erc/erc-tests.el +++ b/test/lisp/erc/erc-tests.el @@ -674,6 +674,39 @@ ;; No fallback behavior. (should-not (erc--parse-nuh "abc\nde!fg@xy"))) +;; NUH interpretation rules: +;; +;; 1. "a@b" or "a!b" - "a" is the nick and "b" is the host. Can't have +;; a login without a nick and a host. +;; +;; 2. "a" - either a nick or a host, depending on message type. The +;; presence of a "." does not imply a host because some IRC-adjacent +;; bridges allow nicks to contain dots, and a host can be a host +;; name, like "localhost" without a domain structure. Nick-only +;; types include PRIVMSG, JOIN, PART, QUIT, NICK, KICK, TOPIC, AWAY, +;; ACCOUNT, and TAGMSG. MODE can be either but is usually a nick +;; unless recovering from a netsplit or as a response to a ChanServ +;; OP. NOTICE can be either but is always a nick when directed to a +;; channel. +;; +;; 3. "a!", "a!@", "a@", "!a@", "@a", etc. are pathological. +;; +(ert-deftest erc--interpret-nuh () + (should (equal (erc--interpret-nuh (erc--parse-nuh "a@b")) + '("a" nil "b"))) + (should (equal (erc--interpret-nuh (erc--parse-nuh "a!b")) + '("a" nil "b"))) + (should (equal (erc--interpret-nuh (erc--parse-nuh "B..o..b")) + '("B..o..b" nil nil))) + (should (equal (erc--interpret-nuh (erc--parse-nuh "gnu.org")) + '("gnu.org" nil nil))) + (should (equal (erc--interpret-nuh (erc--parse-nuh "localhost")) + '("localhost" nil nil))) + + ;; Reject login containing CHANTYPE chars. + (should (equal (erc--parse-nuh "a&b@c") '(nil "a&b" "c"))) + (should-error (erc--interpret-nuh '(nil "a&b" "c")))) + (ert-deftest erc--parsed-prefix () ;; Effectively a no-op in a non-ERC buffer. (should-not (erc--parsed-prefix))