1
Fork 0
mirror of git://git.sv.gnu.org/emacs.git synced 2026-01-07 20:30:32 -08:00

Actually derive channel membership from PREFIX in ERC

* lisp/erc/erc-backend.el (erc--with-isupport-data): Add comment for
possibly superior alternate implementation.
* lisp/erc/erc-common.el (erc--get-isupport-entry): Use helper to
initialize traditional prefix slots in overridden well-known
constructor.
(erc--parsed-prefix): Reverse order of characters in the `letters'
and `statuses' slots, in their defaults and also their definitions.
(erc--strpos): New function, a utility for finding a single character
in a string.
* lisp/erc/erc.el (erc--define-channel-user-status-compat-getter):
Modify to query advertised value for associated mode letter at runtime
instead of baking it in.
(erc-channel-user-voice, erc-channel-user-halfop, erc-channel-user-op,
erc-channel-user-admin, erc-channel-user-owner): Supply second
argument for fallback mode letter.
(erc--cusr-status-p, erc--cusr-change-status): New functions for
querying and modifying `erc-channel-user' statuses.
(erc-send-input-line): Update speaker time in own nick's
`erc-channel-member' entry.
(erc-get-channel-membership-prefix): Adapt code to prefer advertised
prefix for mode letter.
(erc--parsed-prefix): Save "reversed" `letters' and `statuses' so that
they're ordered from lowest to highest semantically.
(erc--get-prefix-flag, erc--init-cusr-fallback-status,
erc--compute-cusr-fallback-status): New functions for retrieving
internal prefix values and massaging hard-coded traditional prefixes
so they're compatible with existing `erc-channel-member' update code.
(erc--partition-prefixed-names): New function, separated for testing
and for conversion to a generic in the future when ERC supports
extensions that list member rolls in a different format.
(erc-channel-receive-names): Refactor to use new status-aware update
and init workhorse functions for updating and initializing a
`erc-channel-members' entry.
(erc--create-current-channel-member): New "status-aware" function
comprising the `addp' path of `erc-update-current-channel-member'.
(erc--update-current-channel-member): New "status-aware" function
comprising the "update" path of `erc-update-current-channel-member',
which ran when an existing `erc-channel-members' entry for the queried
nick was found.
(erc-update-current-channel-member): Split code body into two
constituent functions, both for readability and for usability, so
callers can more explicitly request the desired operation in a
"status-aware" manner.
(erc--update-membership-prefix): Remove unused function, originally
meant to be new in ERC 5.6.
(erc--process-channel-modes): Call `erc--cusr-change-status' instead
of `erc--update-membership-prefix'.
(erc--shuffle-nuh-nickward): New utility function to ensure code like
`erc--partition-prefixed-names' can use `erc--parse-nuh' in a
practical and relatively convenient way in the near future.
* test/lisp/erc/erc-scenarios-base-chan-modes.el
(erc-scenarios-base-chan-modes--speaker-status): New test.
* test/lisp/erc/erc-tests.el (erc--parsed-prefix): Reverse expected
order of various slot values in `erc--parsed-prefix' objects.
(erc--get-prefix-flag, erc--init-cusr-fallback-status,
erc--compute-cusr-fallback-status, erc--cusr-status-p,
erc--cusr-change-status): New tests.
(erc--update-channel-modes, erc-process-input-line): Use newly
available utilities imported from common library.
* test/lisp/erc/resources/base/modes/speaker-status.eld: New file.
(Bug#67220)
This commit is contained in:
F. Jason Park 2024-01-17 21:42:02 -08:00
parent d85f561da0
commit aedc8b55bf
6 changed files with 470 additions and 167 deletions

View file

@ -81,4 +81,62 @@
(should-not erc-channel-user-limit)
(funcall expect 10 "<Chad> after"))))
;; This asserts proper recognition of nonstandard prefixes advertised
;; via the "PREFIX=" ISUPPORT parameter. Note that without the IRCv3
;; `multi-prefix' extension, we can't easily sync a user's channel
;; membership status on receipt of a 352/353 by parsing the "flags"
;; parameter because even though servers remember multiple prefixes,
;; they only ever return the one with the highest rank. For example,
;; if on receipt of a 352, we were to "update" someone we believe to
;; be @+ by changing them to a to @, we'd be guilty of willful
;; munging. And if they later lose that @, we'd then see them as null
;; when in fact they're still +. However, we *could* use a single
;; degenerate prefix to "validate" an existing record to ensure
;; correctness of our processing logic, but it's unclear how such a
;; discrepancy ought to be handled beyond asking the user to file a
;; bug.
(ert-deftest erc-scenarios-base-chan-modes--speaker-status ()
:tags '(:expensive-test)
(erc-scenarios-common-with-cleanup
((erc-scenarios-common-dialog "base/modes")
(erc-server-flood-penalty 0.1)
(dumb-server (erc-d-run "localhost" t 'speaker-status))
(erc-show-speaker-membership-status t)
(erc-autojoin-channels-alist '(("." "#chan")))
(expect (erc-d-t-make-expecter)))
(ert-info ("Connect to foonet")
(with-current-buffer (erc :server "127.0.0.1"
:port (process-contact dumb-server :service)
:nick "tester"
:user "tester")
(funcall expect 5 "Here on foonet, we provide services")))
(with-current-buffer (erc-d-t-wait-for 5 (get-buffer "#chan"))
(ert-info ("Prefixes printed correctly in 353")
(funcall expect 10 "chan: +alice @fsbot -bob !foop"))
(ert-info ("Speakers honor option `erc-show-speaker-membership-status'")
(funcall expect 10 "<-bob> alice: Of that which hath")
(funcall expect 10 "<+alice> Hie you, make haste")
(funcall expect 10 "<!foop> hi"))
(ert-info ("Status conferred and rescinded")
(funcall expect 10 "*** foop (user@netadmin.example.net) has changed ")
(funcall expect 10 "mode for #chan to +v bob")
(funcall expect 10 "<+bob> alice: Fair as a text B")
(funcall expect 10 "<+alice> bob: Even as Apemantus")
(funcall expect 10 "mode for #chan to -v bob")
(funcall expect 10 "<-bob> alice: That's the way")
(funcall expect 10 "<+alice> Give it the beasts"))
;; If it had instead overwritten it, our two states would be
;; out of sync. (See comment above.)
(ert-info ("/WHO output confirms server shadowed V status")
(erc-scenarios-common-say "/who #chan")
(funcall expect 10 '(: "bob" (+ " ") "H-"))
(funcall expect 10 "<-bob> alice: Remains in danger")
(erc-cmd-QUIT "")))))
;;; erc-scenarios-base-chan-modes.el ends here

View file

@ -674,7 +674,7 @@
;; checking if null beforehand.
(should-not erc--parsed-prefix)
(should (equal (erc--parsed-prefix)
#s(erc--parsed-prefix nil "qaohv" "~&@%+"
#s(erc--parsed-prefix nil "vhoaq" "+%@&~"
((?q . ?~) (?a . ?&)
(?o . ?@) (?h . ?%) (?v . ?+)))))
(let ((cached (should erc--parsed-prefix)))
@ -696,7 +696,7 @@
(should (equal expected (erc--parsed-prefix-alist erc--parsed-prefix)))
(setq cached erc--parsed-prefix)
(should (equal cached
#s(erc--parsed-prefix ("(ov)@+") "ov" "@+"
#s(erc--parsed-prefix ("(ov)@+") "vo" "+@"
((?o . ?@) (?v . ?+)))))
;; Second target buffer reuses cached value.
(with-temp-buffer
@ -714,6 +714,88 @@
(erc-with-server-buffer erc--parsed-prefix))
'((?q . ?~) (?h . ?%)))))))
(ert-deftest erc--get-prefix-flag ()
(erc-tests-common-make-server-buf (buffer-name))
(should-not erc--parsed-prefix)
(should (= (erc--get-prefix-flag ?v) 1))
(should (= (erc--get-prefix-flag ?h) 2))
(should (= (erc--get-prefix-flag ?o) 4))
(should (= (erc--get-prefix-flag ?a) 8))
(should (= (erc--get-prefix-flag ?q) 16))
(ert-info ("With optional `from-prefix-p'")
(should (= (erc--get-prefix-flag ?+ nil 'fpp) 1))
(should (= (erc--get-prefix-flag ?% nil 'fpp) 2))
(should (= (erc--get-prefix-flag ?@ nil 'fpp) 4))
(should (= (erc--get-prefix-flag ?& nil 'fpp) 8))
(should (= (erc--get-prefix-flag ?~ nil 'fpp) 16)))
(should erc--parsed-prefix))
(ert-deftest erc--init-cusr-fallback-status ()
;; Fallback behavior active because no `erc--parsed-prefix'.
(should-not erc--parsed-prefix)
(should (= 0 (erc--init-cusr-fallback-status nil nil nil nil nil)))
(should (= 1 (erc--init-cusr-fallback-status t nil nil nil nil)))
(should (= 4 (erc--init-cusr-fallback-status nil nil t nil nil)))
(should-not erc--parsed-prefix) ; not created in non-ERC buffer.
;; Uses advertised server parameter.
(erc-tests-common-make-server-buf (buffer-name))
(setq erc-server-parameters '(("PREFIX" . "(YqaohvV)!~&@%+-")))
(should (= 0 (erc--init-cusr-fallback-status nil nil nil nil nil)))
(should (= 2 (erc--init-cusr-fallback-status t nil nil nil nil)))
(should (= 8 (erc--init-cusr-fallback-status nil nil t nil nil)))
(should erc--parsed-prefix))
(ert-deftest erc--compute-cusr-fallback-status ()
;; Useless without an `erc--parsed-prefix'.
(should (= 0 (erc--compute-cusr-fallback-status 0 nil nil nil nil nil)))
(should (= 0 (erc--compute-cusr-fallback-status 0 'on 'on 'on 'on 'on)))
(erc-tests-common-make-server-buf (buffer-name))
(should (= 0 (erc--compute-cusr-fallback-status 0 nil nil nil nil nil)))
(should (= 1 (erc--compute-cusr-fallback-status 0 'on nil nil nil nil)))
(should (= 1 (erc--compute-cusr-fallback-status 0 'on 'off 'off 'off 'off)))
(should (= 1 (erc--compute-cusr-fallback-status 1 'on 'off 'off 'off 'off)))
(should (= 1 (erc--compute-cusr-fallback-status 1 nil nil nil nil nil)))
(should (= 1 (erc--compute-cusr-fallback-status 3 nil 'off nil nil nil)))
(should (= 1 (erc--compute-cusr-fallback-status 7 nil 'off 'off nil nil)))
(should (= 4 (erc--compute-cusr-fallback-status 1 'off nil 'on nil nil))))
(ert-deftest erc--cusr-status-p ()
(erc-tests-common-make-server-buf (buffer-name))
(should-not erc--parsed-prefix)
(let ((cusr (make-erc-channel-user :voice t :op t)))
(should-not (erc--cusr-status-p cusr ?q))
(should-not (erc--cusr-status-p cusr ?a))
(should-not (erc--cusr-status-p cusr ?h))
(should (erc--cusr-status-p cusr ?o))
(should (erc--cusr-status-p cusr ?v)))
(should erc--parsed-prefix))
(ert-deftest erc--cusr-change-status ()
(erc-tests-common-make-server-buf (buffer-name))
(let ((cusr (make-erc-channel-user)))
(should-not (erc--cusr-status-p cusr ?o))
(should-not (erc--cusr-status-p cusr ?v))
(erc--cusr-change-status cusr ?o t)
(erc--cusr-change-status cusr ?v t)
(should (erc--cusr-status-p cusr ?o))
(should (erc--cusr-status-p cusr ?v))
(ert-info ("Reset with optional param")
(erc--cusr-change-status cusr ?q t 'reset)
(should-not (erc--cusr-status-p cusr ?o))
(should-not (erc--cusr-status-p cusr ?v))
(should (erc--cusr-status-p cusr ?q)))
(ert-info ("Clear with optional param")
(erc--cusr-change-status cusr ?v t)
(should (erc--cusr-status-p cusr ?v))
(erc--cusr-change-status cusr ?q nil 'reset)
(should-not (erc--cusr-status-p cusr ?v))
(should-not (erc--cusr-status-p cusr ?q)))))
;; This exists as a reference to assert legacy behavior in order to
;; preserve and incorporate it as a fallback in the 5.6+ replacement.
(ert-deftest erc-parse-modes ()
@ -737,12 +819,9 @@
(should (equal (erc-parse-modes "-l") '(nil nil (("l" off nil))))))))
(ert-deftest erc--update-channel-modes ()
(erc-mode)
(erc-tests-common-make-server-buf)
(setq erc-channel-users (make-hash-table :test #'equal)
erc-server-users (make-hash-table :test #'equal)
erc--isupport-params (make-hash-table)
erc--target (erc--target-from-string "#test"))
(erc-tests-common-init-server-proc "sleep" "1")
(let ((orig-handle-fn (symbol-function 'erc--handle-channel-mode))
calls)
@ -1715,13 +1794,13 @@
;; regardless of whether a command handler is summoned.
(ert-deftest erc-process-input-line ()
(let (erc-server-last-sent-time
erc-server-flood-queue
(orig-erc-cmd-MSG (symbol-function 'erc-cmd-MSG))
(erc-default-recipients '("#chan"))
(erc-tests-common-make-server-buf)
(let ((orig-erc-cmd-MSG (symbol-function 'erc-cmd-MSG))
(pop-flood-queue (lambda () (erc-with-server-buffer
(pop erc-server-flood-queue))))
calls)
(with-temp-buffer
(erc-tests-common-init-server-proc "sleep" "1")
(setq erc-server-current-nick "tester")
(with-current-buffer (erc--open-target "#chan")
(cl-letf (((symbol-function 'erc-cmd-MSG)
(lambda (line)
(push line calls)
@ -1735,49 +1814,50 @@
(ert-info ("Baseline")
(erc-process-input-line "/msg #chan hi\n")
(should (equal (pop calls) " #chan hi"))
(should (equal (pop erc-server-flood-queue)
(should (equal (funcall pop-flood-queue)
'("PRIVMSG #chan :hi\r\n" . utf-8))))
(ert-info ("Quote preserves line intact")
(erc-process-input-line "/QUOTE FAKE foo bar\n")
(should (equal (pop erc-server-flood-queue)
(should (equal (funcall pop-flood-queue)
'("FAKE foo bar\r\n" . utf-8))))
(ert-info ("Unknown command respected")
(erc-process-input-line "/FAKE foo bar\n")
(should (equal (pop erc-server-flood-queue)
(should (equal (funcall pop-flood-queue)
'("FAKE foo bar\r\n" . utf-8))))
(ert-info ("Spaces preserved")
(erc-process-input-line "/msg #chan hi you\n")
(should (equal (pop calls) " #chan hi you"))
(should (equal (pop erc-server-flood-queue)
(should (equal (funcall pop-flood-queue)
'("PRIVMSG #chan :hi you\r\n" . utf-8))))
(ert-info ("Empty line honored")
(erc-process-input-line "/msg #chan\n")
(should (equal (pop calls) " #chan"))
(should (equal (pop erc-server-flood-queue)
(should (equal (funcall pop-flood-queue)
'("PRIVMSG #chan :\r\n" . utf-8)))))
(ert-info ("Implicit cmd via `erc-send-input-line-function'")
(ert-info ("Baseline")
(erc-process-input-line "hi\n")
(should (equal (pop erc-server-flood-queue)
(should (equal (funcall pop-flood-queue)
'("PRIVMSG #chan :hi\r\n" . utf-8))))
(ert-info ("Spaces preserved")
(erc-process-input-line "hi you\n")
(should (equal (pop erc-server-flood-queue)
(should (equal (funcall pop-flood-queue)
'("PRIVMSG #chan :hi you\r\n" . utf-8))))
(ert-info ("Empty line transmitted with injected-space kludge")
(erc-process-input-line "\n")
(should (equal (pop erc-server-flood-queue)
(should (equal (funcall pop-flood-queue)
'("PRIVMSG #chan : \r\n" . utf-8))))
(should-not calls))))))
(should-not calls)))))
(erc-tests-common-kill-buffers))
(ert-deftest erc--get-inserted-msg-beg/basic ()
(erc-tests-common-assert-get-inserted-msg/basic

View file

@ -0,0 +1,69 @@
;; -*- mode: lisp-data; -*-
((nick 10 "NICK tester"))
((user 10 "USER tester 0 * :unknown")
(0.00 ":irc.example.net NOTICE * :*** Looking up your hostname...")
(0.00 ":irc.example.net NOTICE tester :*** Could not resolve your hostname: Domain not found; using your IP address (10.0.2.100) instead.")
(0.09 ":irc.example.net 001 tester :Welcome to the foonet IRC Network tester!tester@10.0.2.100")
(0.01 ":irc.example.net 002 tester :Your host is irc.example.net, running version InspIRCd-3")
(0.01 ":irc.example.net 003 tester :This server was created 07:50:59 Jan 22 2024")
(0.03 ":irc.example.net 004 tester irc.example.net InspIRCd-3 BIRcgikorsw ACHIKMORTVXabcefghijklmnopqrstvyz :HIVXabefghjkloqvy")
(0.00 ":irc.example.net 005 tester ACCEPT=30 AWAYLEN=200 BOT=B CALLERID=g CASEMAPPING=ascii CHANLIMIT=#:20 CHANMODES=IXbeg,k,Hfjl,ACKMORTcimnprstz CHANNELLEN=64 CHANTYPES=# ELIST=CMNTU ESILENCE=CcdiNnPpTtx EXCEPTS=e :are supported by this server")
(0.01 ":irc.example.net 005 tester EXTBAN=,ACORTUacjrwz HOSTLEN=64 INVEX=I KEYLEN=32 KICKLEN=255 LINELEN=512 MAXLIST=I:100,X:100,b:100,e:100,g:100 MAXTARGETS=20 MODES=20 MONITOR=30 NAMELEN=128 NAMESX NETWORK=foonet :are supported by this server")
(0.01 ":irc.example.net 005 tester NICKLEN=30 PREFIX=(yqaohvV)!~&@%+- SAFELIST SILENCE=32 STATUSMSG=!~&@%+- TOPICLEN=307 UHNAMES USERIP USERLEN=10 USERMODES=,,s,BIRcgikorw WHOX :are supported by this server")
(0.01 ":irc.example.net 251 tester :There are 2 users and 2 invisible on 2 servers")
(0.00 ":irc.example.net 252 tester 1 :operator(s) online")
(0.00 ":irc.example.net 253 tester 1 :unknown connections")
(0.00 ":irc.example.net 254 tester 2 :channels formed")
(0.00 ":irc.example.net 255 tester :I have 4 clients and 1 servers")
(0.00 ":irc.example.net 265 tester :Current local users: 4 Max: 5")
(0.00 ":irc.example.net 266 tester :Current global users: 4 Max: 5")
(0.00 ":irc.example.net 375 tester :irc.example.net message of the day")
(0.00 ":irc.example.net 372 tester : https://github.com/inspircd/inspircd-docker/issues")
(0.00 ":irc.example.net 372 tester : ")
(0.00 ":irc.example.net 372 tester : Have fun with the image!")
(0.00 ":irc.example.net 376 tester :End of message of the day.")
(0.00 ":irc.example.net 501 tester x :is not a recognised user mode.")
(0.00 ":NickServ!NickServ@services.int NOTICE tester :Welcome to foonet, tester! Here on foonet, we provide services to enable the registration of nicknames and channels! For details, type \2/msg NickServ help\2 and \2/msg ChanServ help\2."))
((mode 10 "MODE tester +i")
(0.01 ":tester!tester@10.0.2.100 MODE tester :+i"))
((join 10 "JOIN #chan")
(0.02 ":tester!tester@10.0.2.100 JOIN :#chan")
(0.02 ":irc.example.net 353 tester = #chan :+alice @fsbot -bob !foop tester")
(0.03 ":irc.example.net 366 tester #chan :End of /NAMES list.")
(0.00 ":bob!bob@localhost PRIVMSG #chan :tester, welcome!")
(0.01 ":alice!alice@localhost PRIVMSG #chan :tester, welcome!"))
((mode-chan 10 "MODE #chan")
(0.00 ":irc.example.net 324 tester #chan :+nt")
(0.01 ":irc.example.net 329 tester #chan :1705909863")
(0.03 ":bob!bob@localhost PRIVMSG #chan :alice: Of that which hath so faithfully been paid.")
(0.03 ":alice!alice@localhost PRIVMSG #chan :Hie you, make haste, for it grows very late.")
(0.03 ":foop!user@netadmin.example.net PRIVMSG #chan :hi")
;; (0.07 ":alice!alice@localhost PRIVMSG #chan :bob: And make a clear way to the gods.")
;; (0.04 ":bob!bob@localhost PRIVMSG #chan :Why, that they have; and bid them so be gone.")
;; (0.08 ":bob!bob@localhost PRIVMSG #chan :alice: Now stay your strife: what shall be is dispatch'd.")
(0.06 ":foop!user@netadmin.example.net MODE #chan +v :bob")
(0.05 ":bob!bob@localhost PRIVMSG #chan :alice: Fair as a text B in a copy-book.")
(0.07 ":alice!alice@localhost PRIVMSG #chan :bob: Even as Apemantus does now; hate a lord with my heart.")
(0.03 ":bob!bob@localhost PRIVMSG #chan :Then here is a supplication for you. And when you come to him, at the first approach you must kneel; then kiss his foot; then deliver up your pigeons; and then look for your reward. I'll be at hand, sir; see you do it bravely.")
(0.05 ":foop!user@netadmin.example.net MODE #chan -v :bob")
(0.04 ":bob!bob@localhost PRIVMSG #chan :alice: That's the way: for women are light at midnight.")
(0.04 ":alice!alice@localhost PRIVMSG #chan :Give it the beasts, to be rid of the men.")
;; (0.02 ":alice!alice@localhost PRIVMSG #chan :bob: Here comes young Master Ganymede, my new mistress's brother.")
)
((who-chan 10 "who #chan")
(0.03 ":irc.example.net 352 tester #chan alice localhost irc.example.net alice H+ :0 Irc bot based on irc3 http://irc3.readthedocs.io")
(0.03 ":irc.example.net 352 tester #chan fsbot localhost irc.example.net fsbot H@ :0 fsbot")
(0.01 ":irc.example.net 352 tester #chan bob localhost irc.example.net bob H- :0 Irc bot based on irc3 http://irc3.readthedocs.io")
(0.01 ":irc.example.net 352 tester #chan user netadmin.example.net irc.example.net foop H*! :0 unknown")
(0.01 ":irc.example.net 352 tester #chan tester 10.0.2.100 irc.example.net tester H :0 unknown")
(0.01 ":irc.example.net 315 tester #chan :End of /WHO list.")
;; (0.09 ":bob!bob@localhost PRIVMSG #chan :alice: Shall nothing wrong him. Thus it is, general.")
;; (0.04 ":alice!alice@localhost PRIVMSG #chan :bob: His father and I were soldiers together; to whom I have been often bound for no less than my life. Here comes the Briton: let him be so entertained amongst you as suits, with gentlemen of your knowing, to a stranger of his quality.")
(0.04 ":bob!bob@localhost PRIVMSG #chan :alice: Remains in danger of her former tooth."))
((quit 10 "QUIT :\2ERC\2")
(0.03 "ERROR :Closing link: (tester@10.0.2.100) [Quit: \2ERC\2 5.x (IRC client for GNU Emacs)]"))