bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC

"J.P." <[email protected]>
Newsgroups gmane.emacs.bugs,gmane.emacs.erc.general
Message-ID <[email protected]>
Update #6.

With the one-year anniversary of this bug finally upon us, it felt only
fitting to furnish yet another (hopefully final) update. I say final
because as long as we're willing to accept the proposed behavior as
mostly correct, a move to iterating on trunk seems (IMO) warranted, if
only to avoid further alienating those who've been patiently awaiting
these and other, dependent changes.

(If you'll recall, this was the only ERC bug to occupy #emacs topic real
estate for weeks on end during all of 27.1.)

To review, this bug set introduces the following items for export:

   5 functions
   6 options
   2 other           (everything else is internal)


 * lisp/erc/erc.el:

   function: `erc-auth-source-search'
             Default value of all `erc-auth-source-*-function' options.
             Queries `auth-source-search' with params derived from
             protocol context, connection properties, and caller
             overrides.

   function: `erc-compute-user'
             Determines first positional arg of IRC "USER" command to
             send when not otherwise supplied. Could be internal (IMO);
             exported for consistency with "compute-foo" convention.

     option: `erc-auth-source-server-function',
             `erc-auth-source-join-function'
             Functions for querying auth-source in various contexts. See
             also `erc-auth-source-services-function' below.

     option: `erc-prompt-hidden' and `erc-unhide-query-prompt'
             See bug#54826: Prevent duplicate prompts when reconnecting
             in ERC.

     option: `erc-reconnect-display'
             See bug#51753: ERC switches to channel buffer on reconnect.


 * lisp/erc/erc-backend.el:

   variable: `erc-session-username'
             See bug#54824: Add 'user' parameter to ERC entry-point
             commands.


 * lisp/erc/erc-networks.el:

   function: `erc-networks-shrink-ids-and-buffer-names' and
             `erc-networks-rename-surviving-target-buffer'
             These undo certain uniquifying effects when disambiguation
             is no longer relevant. Would be internal if not for being
             default members of various buffer-killing hooks.

   function: `erc-networks-on-MOTD-end'
             Performs primary network-context related setup right before
             logical connection formally established.

      other: `erc-obsolete-var'
             Generic context rewriter.


 * lisp/erc/erc-services.el:

     option: `erc-auth-source-services-function'
             See `erc-auth-source-server-function' above. Should perhaps
             be renamed `erc-auth-source-nickserv-function' to avoid
             confusion with the latter.


For major behavioral changes and deprecations, please see the updates to
etc/ERC-NEWS in the last patch below. Some of the language is merely
speculative and bears further discussion (in particular, the thing about
a new naming convention for edge functionality). In that same patch,
you'll also find updates to the texinfo doc describing (not so much
changes to but) new behavior for existing options and commands. Other,
less consequential changes of more or less minor disruptive potential
are noted in the commit logs themselves.

And with that, people, I once again implore you:

  see something, say something!

Thanks,
J.P.
0001-Rework-mutual-dependency-between-erc-and-erc-backend.patch (text/x-patch, 3.7 KB)
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Mon, 4 Apr 2022 22:38:22 -0700
Subject: [PATCH 01/35] Rework mutual dependency between erc and erc-backend

* lisp/erc/erc.el: Declare needed variables exported by erc-backend.el
as special near the top of the file, and only require `erc-backend'
after providing `erc' as a feature at the very end.

* lisp/erc/erc-backend.el: Don't preemptively provide `erc-backend'.

* test/lisp/erc/erc-tests.el (erc--meta--backend-dependencies): Add
utility test to scrape for unused vars that may accumulate over time.
(Bug#54825)
---
 lisp/erc/erc-backend.el    |  1 -
 lisp/erc/erc.el            | 23 ++++++++++++++++++++++-
 test/lisp/erc/erc-tests.el | 21 +++++++++++++++++++++
 3 files changed, 43 insertions(+), 2 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 4c13f4c419..1e3af1bedd 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -102,7 +102,6 @@
 ;; There's a fairly strong mutual dependency between erc.el and erc-backend.el.
 ;; Luckily, erc.el does not need erc-backend.el for macroexpansion whereas the
 ;; reverse is true:
-(provide 'erc-backend)
 (require 'erc)
 
 ;;;; Variables and options
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index ff482d4933..c4689a4b78 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -130,7 +130,26 @@ erc-scripts
   "Running scripts at startup and with /LOAD."
   :group 'erc)
 
-(require 'erc-backend)
+;; Defined in erc-backend
+(defvar erc--server-reconnecting)
+(defvar erc-channel-members-changed-hook)
+(defvar erc-server-367-functions)
+(defvar erc-server-announced-name)
+(defvar erc-server-connect-function)
+(defvar erc-server-connected)
+(defvar erc-server-current-nick)
+(defvar erc-server-filter-data)
+(defvar erc-server-lag)
+(defvar erc-server-last-sent-time)
+(defvar erc-server-parameters)
+(defvar erc-server-process)
+(defvar erc-server-quitting)
+(defvar erc-server-reconnect-count)
+(defvar erc-server-reconnecting)
+(defvar erc-session-client-certificate)
+(defvar erc-session-connector)
+(defvar erc-session-port)
+(defvar erc-session-server)
 
 ;; tunable connection and authentication parameters
 
@@ -7023,6 +7042,8 @@ erc-handle-irc-url
 
 (provide 'erc)
 
+(require 'erc-backend)
+
 ;; Deprecated. We might eventually stop requiring the goodies automatically.
 ;; IMPORTANT: This require must appear _after_ the above (provide 'erc) to
 ;; avoid a recursive require error when byte-compiling the entire package.
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 520f10dd4e..3c76cb97ca 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -48,6 +48,27 @@ erc--read-time-period
   (cl-letf (((symbol-function 'read-string) (lambda (&rest _) "1d")))
     (should (equal (erc--read-time-period "foo: ") 86400))))
 
+(ert-deftest erc--meta--backend-dependencies ()
+  (with-temp-buffer
+    (insert-file-contents-literally
+     (concat (file-name-sans-extension (symbol-file 'erc)) ".el"))
+    (let ((beg (search-forward ";; Defined in erc-backend"))
+          (end (search-forward "\n\n"))
+          vars)
+      (save-excursion
+        (save-restriction
+          (narrow-to-region beg end)
+          (goto-char (point-min))
+          (with-syntax-table lisp-data-mode-syntax-table
+            (condition-case _
+                (while (push (cadr (read (current-buffer))) vars))
+              (end-of-file)))))
+      (should (= (point) end))
+      (dolist (var vars)
+        (setq var (concat "\\_<" (symbol-name var) "\\_>"))
+        (ert-info (var)
+          (should (save-excursion (search-forward-regexp var nil t))))))))
+
 (ert-deftest erc-with-all-buffers-of-server ()
   (let (proc-exnet
         proc-onet
-- 
2.36.1
0002-Initialize-erc-server-filter-data-in-erc-backend.patch (text/x-patch, 1.9 KB)
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Tue, 5 Apr 2022 01:30:07 -0700
Subject: [PATCH 02/35] Initialize erc-server-filter-data in erc-backend

* lisp/erc/erc-backend.el (erc-server-connect): Set
`erc-server-filter-data' to nil upon (re)connecting.

* lisp/erc/erc.el (erc-open): For the sake of clarity, don't
initialize `erc-server-filter-data' here because non-connect
invocations merely set up a target buffer and have no business
touching this variable.
---
 lisp/erc/erc-backend.el | 1 +
 lisp/erc/erc.el         | 3 ---
 2 files changed, 1 insertion(+), 3 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 1e3af1bedd..d442013cf4 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -535,6 +535,7 @@ erc-server-connect
       (error "Connection attempt failed"))
     ;; Misc server variables
     (with-current-buffer buffer
+      (setq erc-server-filter-data nil)
       (setq erc-server-process process)
       (setq erc-server-quitting nil)
       (setq erc-server-reconnecting nil
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index c4689a4b78..9240791b1e 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -138,7 +138,6 @@ erc-server-announced-name
 (defvar erc-server-connect-function)
 (defvar erc-server-connected)
 (defvar erc-server-current-nick)
-(defvar erc-server-filter-data)
 (defvar erc-server-lag)
 (defvar erc-server-last-sent-time)
 (defvar erc-server-parameters)
@@ -2055,8 +2054,6 @@ erc-open
         (setq erc-server-users nil)
         (setq erc-channel-users
               (make-hash-table :test 'equal))))
-    ;; clear last incomplete line read
-    (setq erc-server-filter-data nil)
     (setq erc-channel-topic "")
     ;; limit on the number of users on the channel (mode +l)
     (setq erc-channel-user-limit nil)
-- 
2.36.1
0003-Accept-user-keyword-arg-in-ERC-entry-point-commands.patch (text/x-patch, 7.8 KB)
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Sun, 3 Apr 2022 14:24:24 -0700
Subject: [PATCH 03/35] Accept user keyword arg in ERC entry-point commands

* lisp/erc/erc-backend.el (erc-server-reconnect): Reuse the username
argument from the previous session's USER command when reconnecting.
Also pass the existing client certificate, fixing an issue related to
bug#47788.
(erc-session-user-full-name): Move variable here from erc.el.
(erc-session-username): Add new local variable to store entry point
parameter.

* lisp/erc/erc.el (erc-session-user-full-name): Move variable to
erc-backend.
(erc-open, erc-determine-parameters, erc, erc-tls): Accept new
optional user parameter.
(erc-query): Preserve current `erc-session-username' when calling
`erc-open'.
(erc-login): Use `erc-session-username' instead of deriving it.
(erc-compute-user): Add new function to determine user name from
explicit argument or user options.
(Bug#54824)
---
 lisp/erc/erc-backend.el | 12 +++++++++++-
 lisp/erc/erc.el         | 34 ++++++++++++++++++++++------------
 2 files changed, 33 insertions(+), 13 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index d442013cf4..8f271a7eb3 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -123,6 +123,14 @@ erc-server-current-nick
   "Nickname on the current server.
 Use `erc-current-nick' to access this.")
 
+(defvar-local erc-session-user-full-name nil
+  "Real name used for the current session.
+Sent as the last argument to the USER command.")
+
+(defvar-local erc-session-username nil
+  "Username used for the current session.
+Sent as the first argument of the USER command.")
+
 ;;; Server attributes
 
 (defvar-local erc-server-process nil
@@ -585,7 +593,9 @@ erc-server-reconnect
       (let ((erc-server-connect-function (or erc-session-connector
                                              #'erc-open-network-stream)))
         (erc-open erc-session-server erc-session-port erc-server-current-nick
-                  erc-session-user-full-name t erc-session-password)))))
+                  erc-session-user-full-name t erc-session-password
+                  nil nil nil erc-session-client-certificate
+                  erc-session-username)))))
 
 (defun erc-server-delayed-reconnect (buffer)
   (if (buffer-live-p buffer)
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 9240791b1e..1a6911a511 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -149,6 +149,8 @@ erc-session-client-certificate
 (defvar erc-session-connector)
 (defvar erc-session-port)
 (defvar erc-session-server)
+(defvar erc-session-user-full-name)
+(defvar erc-session-username)
 
 ;; tunable connection and authentication parameters
 
@@ -1820,9 +1822,6 @@ erc-buffer-list-with-nick
 (defvar-local erc-default-recipients nil
   "List of default recipients of the current buffer.")
 
-(defvar-local erc-session-user-full-name nil
-  "Full name of the user on the current server.")
-
 (defvar-local erc-channel-user-limit nil
   "Limit of users per channel.")
 
@@ -1989,8 +1988,8 @@ erc-setup-buffer
 
 (defun erc-open (&optional server port nick full-name
                            connect passwd tgt-list channel process
-                           client-certificate)
-  "Connect to SERVER on PORT as NICK with FULL-NAME.
+                           client-certificate user)
+  "Connect to SERVER on PORT as NICK with USER and FULL-NAME.
 
 If CONNECT is non-nil, connect to the server.  Otherwise assume
 already connected and just create a separate buffer for the new
@@ -2095,7 +2094,7 @@ erc-open
       (erc-display-prompt)
       (goto-char (point-max)))
 
-    (erc-determine-parameters server port nick full-name)
+    (erc-determine-parameters server port nick full-name user)
 
     ;; Saving log file on exit
     (run-hook-with-args 'erc-connect-pre-hook buffer)
@@ -2216,6 +2215,7 @@ erc-select-read-args
 (cl-defun erc (&key (server (erc-compute-server))
                     (port   (erc-compute-port))
                     (nick   (erc-compute-nick))
+                    (user   (erc-compute-user))
                     password
                     (full-name (erc-compute-full-name)))
   "ERC is a powerful, modular, and extensible IRC client.
@@ -2227,6 +2227,7 @@ erc
    (server (erc-compute-server))
    (port   (erc-compute-port))
    (nick   (erc-compute-nick))
+   (user   (erc-compute-user))
    password
    (full-name (erc-compute-full-name))
 
@@ -2238,7 +2239,7 @@ erc
 whereas `erc-compute-port' and `erc-compute-nick' will be invoked
 for the values of the other parameters."
   (interactive (erc-select-read-args))
-  (erc-open server port nick full-name t password))
+  (erc-open server port nick full-name t password nil nil nil nil user))
 
 ;;;###autoload
 (defalias 'erc-select #'erc)
@@ -2248,6 +2249,7 @@ 'erc-ssl
 (cl-defun erc-tls (&key (server (erc-compute-server))
                         (port   (erc-compute-port))
                         (nick   (erc-compute-nick))
+                        (user   (erc-compute-user))
                         password
                         (full-name (erc-compute-full-name))
                         client-certificate)
@@ -2291,7 +2293,7 @@ erc-tls
 		 (erc-select-read-args)))
   (let ((erc-server-connect-function 'erc-open-tls-stream))
     (erc-open server port nick full-name t password
-              nil nil nil client-certificate)))
+              nil nil nil client-certificate user)))
 
 (defun erc-open-tls-stream (name buffer host port &rest parameters)
   "Open an TLS stream to an IRC server.
@@ -4311,7 +4313,8 @@ erc-query
                        nil
                        (list target)
                        target
-                       erc-server-process)))
+                       erc-server-process
+                       erc-session-username)))
     (unless buf
       (error "Couldn't open query window"))
     (erc-update-mode-line)
@@ -6153,14 +6156,14 @@ erc-login
   (erc-server-send
    (format "USER %s %s %s :%s"
            ;; hacked - S.B.
-           (if erc-anonymous-login erc-email-userid (user-login-name))
+           erc-session-username
            "0" "*"
            erc-session-user-full-name))
   (erc-update-mode-line))
 
 ;; connection properties' heuristics
 
-(defun erc-determine-parameters (&optional server port nick name)
+(defun erc-determine-parameters (&optional server port nick name user)
   "Determine the connection and authentication parameters.
 Sets the buffer local variables:
 
@@ -6168,11 +6171,13 @@ erc-determine-parameters
 - `erc-session-server'
 - `erc-session-port'
 - `erc-session-user-full-name'
+- `erc-session-username'
 - `erc-server-current-nick'"
   (setq erc-session-connector erc-server-connect-function
         erc-session-server (erc-compute-server server)
         erc-session-port (or port erc-default-port)
-        erc-session-user-full-name (erc-compute-full-name name))
+        erc-session-user-full-name (erc-compute-full-name name)
+        erc-session-username (erc-compute-user user))
   (erc-set-current-nick (erc-compute-nick nick)))
 
 (defun erc-compute-server (&optional server)
@@ -6190,6 +6195,10 @@ erc-compute-server
       (getenv "IRCSERVER")
       erc-default-server))
 
+(defun erc-compute-user (&optional user)
+  "Return a suitable value for the session user name."
+  (or user (if erc-anonymous-login erc-email-userid (user-login-name))))
+
 (defun erc-compute-nick (&optional nick)
   "Return user's IRC nick.
 
@@ -7018,6 +7027,7 @@ erc-get-parsed-vector-type
 ;; Teach url.el how to open irc:// URLs with ERC.
 ;; To activate, customize `url-irc-function' to `url-irc-erc'.
 
+;; FIXME change user to nick, and use API to find server buffer
 ;;;###autoload
 (defun erc-handle-irc-url (host port channel user password)
   "Use ERC to IRC on HOST:PORT in CHANNEL as USER with PASSWORD.
-- 
2.36.1
0004-Add-some-ERC-test-helpers.patch (text/x-patch, 2.2 KB)
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Tue, 5 Apr 2022 17:45:00 -0700
Subject: [PATCH 04/35] Add some ERC test helpers

* test/lisp/erc/erc-tests.el (erc-tests--test-prep,
erc-tests--set-fake-server-process): Factor out some common
buffer-prep boilerplate involving user input and the server process.
Shared with bug#54536.
---
 test/lisp/erc/erc-tests.el | 24 ++++++++++++++++++------
 1 file changed, 18 insertions(+), 6 deletions(-)

diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 3c76cb97ca..d67aabf7c7 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -135,6 +135,22 @@ erc-with-all-buffers-of-server
     (should (get-buffer "#spam"))
     (kill-buffer "#spam")))
 
+(defun erc-tests--send-prep ()
+  ;; Caller should probably shadow `erc-insert-modify-hook' or
+  ;; populate user tables for erc-button.
+  (erc-mode)
+  (insert "\n\n")
+  (setq erc-input-marker (make-marker)
+        erc-insert-marker (make-marker))
+  (set-marker erc-insert-marker (point-max))
+  (erc-display-prompt)
+  (should (= (point) erc-input-marker)))
+
+(defun erc-tests--set-fake-server-process (&rest args)
+  (setq erc-server-process
+        (apply #'start-process (car args) (current-buffer) args))
+  (set-process-query-on-exit-flag erc-server-process nil))
+
 (ert-deftest erc--switch-to-buffer ()
   (defvar erc-modified-channels-alist) ; lisp/erc/erc-track.el
 
@@ -218,14 +234,10 @@ erc-ring-previous-command-base-case
 (ert-deftest erc-ring-previous-command ()
   (with-current-buffer (get-buffer-create "*#fake*")
     (erc-mode)
-    (insert "\n\n")
+    (erc-tests--send-prep)
+    (setq-local erc-last-input-time 0)
     (should-not (local-variable-if-set-p 'erc-send-completed-hook))
     (set (make-local-variable 'erc-send-completed-hook) nil) ; skip t (globals)
-    (setq erc-input-marker (make-marker)
-          erc-insert-marker (make-marker))
-    (set-marker erc-insert-marker (point-max))
-    (erc-display-prompt)
-    (should (= (point) erc-input-marker))
     ;; Just in case erc-ring-mode is already on
     (setq-local erc-pre-send-functions nil)
     (add-hook 'erc-pre-send-functions #'erc-add-to-input-ring)
-- 
2.36.1
0005-SQUASH-ME-Remove-duplicate-ERC-prompt-on-reconnect.patch (text/x-patch, 15.7 KB)
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Mon, 4 Apr 2022 01:50:50 -0700
Subject: [PATCH 05/35] SQUASH-ME: Remove duplicate ERC prompt on reconnect

* lisp/erc/erc-backend.el (erc--unhide-prompt, erc--hide-prompt,
erc--unhide-prompt-on-self-insert): Add functions to ensure prompt is
hidden on disconnect and shown when a user types /reconnect in a
disconnected server buffer.
(erc-process-sentinel): Register aforementioned function with
`pre-command-hook' when prompt is deleted after disconnecting.
(erc-server-PRIVMSG): ensure prompt is showing when a new message
arrives from target.

* lisp/erc/erc.el (erc-hide-prompt): Repurpose unused option by
changing meaning slightly to mean "selectively hide prompt when
disconnected."  Also delete obsolete, commented-out code that at some
point used this option in its prior incarnation.
(erc-prompt-hidden): Add new option to specify look of prompt when
hidden.
(erc-unhide-query-prompt): Add option to force-reveal query prompts on
reconnect.
(erc-open): Augment earlier reconnect-detection
semantics by incorporating `erc--server-reconnecting'.  In existing
buffers, remove prompt-related hooks and reveal prompt, if necessary.
(erc-cmd-RECONNECT): Allow a user to reconnect when already
connected (by first disconnecting).
(erc-connection-established): Possibly unhide query prompts.
(Bug#54826)
---
 lisp/erc/erc-backend.el    |  41 ++++++++++--
 lisp/erc/erc.el            |  81 +++++++++++++----------
 test/lisp/erc/erc-tests.el | 128 +++++++++++++++++++++++++++++++++++++
 3 files changed, 210 insertions(+), 40 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 8f271a7eb3..345b78f736 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -706,6 +706,39 @@ erc-process-sentinel-1
       ;; unexpected disconnect
       (erc-process-sentinel-2 event buffer))))
 
+(defun erc--unhide-prompt ()
+  (remove-hook 'pre-command-hook #'erc--unhide-prompt-on-self-insert t)
+  (when (and (marker-position erc-insert-marker)
+             (marker-position erc-input-marker))
+    (with-silent-modifications
+      (remove-text-properties erc-insert-marker erc-input-marker
+                              '(display nil)))))
+
+(defun erc--unhide-prompt-on-self-insert ()
+  (when (and (eq this-command #'self-insert-command)
+             (or (eobp) (= (point) erc-input-marker)))
+    (erc--unhide-prompt)))
+
+(defun erc--hide-prompt (proc)
+  (erc-with-all-buffers-of-server
+      proc nil ; sorta wish this was indent 2
+      (when (and erc-hide-prompt
+                 (or (eq erc-hide-prompt t)
+                     ;; FIXME use `erc--target' after bug#48598
+                     (memq (if (erc-default-target)
+                               (if (erc-channel-p (car erc-default-recipients))
+                                   'channel
+                                 'query)
+                             'server)
+                           erc-hide-prompt))
+                 (marker-position erc-insert-marker)
+                 (marker-position erc-input-marker)
+                 (get-text-property erc-insert-marker 'erc-prompt))
+        (with-silent-modifications
+          (add-text-properties erc-insert-marker (1- erc-input-marker)
+                               `(display ,erc-prompt-hidden)))
+        (add-hook 'pre-command-hook #'erc--unhide-prompt-on-self-insert 0 t))))
+
 (defun erc-process-sentinel (cproc event)
   "Sentinel function for ERC process."
   (let ((buf (process-buffer cproc)))
@@ -728,11 +761,8 @@ erc-process-sentinel
           (dolist (buf (erc-buffer-filter (lambda () (boundp 'erc-channel-users)) cproc))
             (with-current-buffer buf
               (setq erc-channel-users (make-hash-table :test 'equal))))
-          ;; Remove the prompt
-          (goto-char (or (marker-position erc-input-marker) (point-max)))
-          (forward-line 0)
-          (erc-remove-text-properties-region (point) (point-max))
-          (delete-region (point) (point-max))
+          ;; Hide the prompt
+          (erc--hide-prompt cproc)
           ;; Decide what to do with the buffer
           ;; Restart if disconnected
           (erc-process-sentinel-1 event buf))))))
@@ -1480,6 +1510,7 @@ define-erc-response-handler
         (setq buffer (erc-get-buffer (if privp nick tgt) proc))
         (when buffer
           (with-current-buffer buffer
+            (when privp (erc--unhide-prompt))
             ;; update the chat partner info.  Add to the list if private
             ;; message.  We will accumulate private identities indefinitely
             ;; at this point.
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 1a6911a511..e876a8f8ba 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -244,13 +244,34 @@ erc-send-whitespace-lines
   :group 'erc
   :type 'boolean)
 
-(defcustom erc-hide-prompt nil
-  "If non-nil, do not display the prompt for commands.
-
-\(A command is any input starting with a `/').
+(defcustom erc-prompt-hidden ">"
+  "Text to show in lieu of the prompt when hidden."
+  :package-version '(ERC . "5.4.1") ; FIXME increment on next ELPA release
+  :group 'erc-display
+  :type 'string)
 
-See also the variables `erc-prompt' and `erc-command-indicator'."
+(defcustom erc-hide-prompt t
+  "If non-nil, hide input prompt upon disconnecting.
+To unhide, type something in the input area.  Once revealed, a prompt
+remains unhidden until the next disconnection.  Channel prompts are
+unhidden upon rejoining.  See `erc-unhide-query-prompt' for behavior
+concerning query prompts."
+  :package-version '(ERC . "5.4.1") ; FIXME increment on next ELPA release
   :group 'erc-display
+  :type '(choice (const :tag "Always hide prompt" t)
+                 (set (const server)
+                      (const query)
+                      (const channel))))
+
+(defcustom erc-unhide-query-prompt nil
+  "When non-nil, always reveal query prompts upon reconnecting.
+Otherwise, prompts in a connection's query buffers remain hidden until
+the user types in the input area or a new message arrives from the
+target."
+  :package-version '(ERC . "5.4.1")
+  :group 'erc-display
+  ;; Extensions may one day offer a way to discover whether a target
+  ;; is online.  When that happens, this can be expanded accordingly.
   :type 'boolean)
 
 ;; tunable GUI stuff
@@ -2013,7 +2034,7 @@ erc-open
         (buffer (erc-get-buffer-create server port channel))
         (old-buffer (current-buffer))
         old-point
-        continued-session)
+        (continued-session (and erc-reuse-buffers erc--server-reconnecting)))
     (when connect (run-hook-with-args 'erc-before-connect server port nick))
     (erc-update-modules)
     (set-buffer buffer)
@@ -2031,7 +2052,7 @@ erc-open
     ;; (the buffer may have existed)
     (goto-char (point-max))
     (forward-line 0)
-    (when (get-text-property (point) 'erc-prompt)
+    (when (or continued-session (get-text-property (point) 'erc-prompt))
       (setq continued-session t)
       (set-marker erc-input-marker
                   (or (next-single-property-change (point) 'erc-prompt)
@@ -2089,7 +2110,8 @@ erc-open
       (goto-char (point-max))
       (insert "\n"))
     (if continued-session
-        (goto-char old-point)
+        (progn (goto-char old-point)
+               (erc--unhide-prompt))
       (set-marker erc-insert-marker (point))
       (erc-display-prompt)
       (goto-char (point-max)))
@@ -3753,12 +3775,15 @@ erc-cmd-RECONNECT
       (setq erc--server-reconnecting t)
       (setq erc-server-reconnect-count 0)
       (setq process (get-buffer-process (erc-server-buffer)))
-      (if process
-          (delete-process process)
-        (erc-server-reconnect))
+      (when process
+        (delete-process process))
+      (erc-server-reconnect)
       (with-suppressed-warnings ((obsolete erc-server-reconnecting))
-        (setq erc-server-reconnecting nil))
-      (setq erc--server-reconnecting nil)))
+        (if erc-reuse-buffers
+            (progn (cl-assert (not erc--server-reconnecting))
+                   (cl-assert (not erc-server-reconnecting)))
+          (setq erc--server-reconnecting nil
+                erc-server-reconnecting nil)))))
   t)
 (put 'erc-cmd-RECONNECT 'process-not-needed t)
 
@@ -4720,7 +4745,14 @@ erc-connection-established
         (erc-update-mode-line)
         (erc-set-initial-user-mode nick buffer)
         (erc-server-setup-periodical-ping buffer)
-        (run-hook-with-args 'erc-after-connect server nick)))))
+        (run-hook-with-args 'erc-after-connect server nick))))
+
+  (when erc-unhide-query-prompt
+    (erc-with-all-buffers-of-server proc
+      nil ; FIXME use `erc--target' after bug#48598
+      (when (and (erc-default-target)
+                 (not (erc-channel-p (car erc-default-recipients))))
+        (erc--unhide-prompt)))))
 
 (defun erc-set-initial-user-mode (nick buffer)
   "If `erc-user-mode' is non-nil for NICK, set the user modes.
@@ -5674,27 +5706,6 @@ erc-send-input
             (erc-process-input-line (concat string "\n") t nil))
           t))))))
 
-;; (defun erc-display-command (line)
-;;   (when erc-insert-this
-;;     (let ((insert-position (point)))
-;;       (unless erc-hide-prompt
-;;         (erc-display-prompt nil nil (erc-command-indicator)
-;;                             (and (erc-command-indicator)
-;;                                  'erc-command-indicator-face)))
-;;       (let ((beg (point)))
-;;         (insert line)
-;;         (erc-put-text-property beg (point)
-;;                                'font-lock-face 'erc-command-indicator-face)
-;;         (insert "\n"))
-;;       (when (processp erc-server-process)
-;;         (set-marker (process-mark erc-server-process) (point)))
-;;       (set-marker erc-insert-marker (point))
-;;       (save-excursion
-;;         (save-restriction
-;;           (narrow-to-region insert-position (point))
-;;           (run-hooks 'erc-send-modify-hook)
-;;           (run-hooks 'erc-send-post-hook))))))
-
 (defun erc-display-msg (line)
   "Display LINE as a message of the user to the current target at point."
   (when erc-insert-this
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index d67aabf7c7..061dfc2f5e 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -151,6 +151,134 @@ erc-tests--set-fake-server-process
         (apply #'start-process (car args) (current-buffer) args))
   (set-process-query-on-exit-flag erc-server-process nil))
 
+(ert-deftest erc-hide-prompt ()
+  (let (erc-kill-channel-hook erc-kill-server-hook erc-kill-buffer-hook)
+
+    (with-current-buffer (get-buffer-create "ServNet")
+      (erc-tests--send-prep)
+      (goto-char erc-insert-marker)
+      (should (looking-at-p (regexp-quote erc-prompt)))
+      (erc-tests--set-fake-server-process "sleep" "1")
+      (set-process-sentinel erc-server-process #'ignore)
+      (setq erc-network 'ServNet)
+      (set-process-query-on-exit-flag erc-server-process nil))
+
+    (with-current-buffer (get-buffer-create "#chan")
+      (erc-tests--send-prep)
+      (goto-char erc-insert-marker)
+      (should (looking-at-p (regexp-quote erc-prompt)))
+      (setq erc-server-process (buffer-local-value 'erc-server-process
+                                                   (get-buffer "ServNet"))
+            erc-default-recipients '("#chan")))
+
+    (with-current-buffer (get-buffer-create "bob")
+      (erc-tests--send-prep)
+      (goto-char erc-insert-marker)
+      (should (looking-at-p (regexp-quote erc-prompt)))
+      (setq erc-server-process (buffer-local-value 'erc-server-process
+                                                   (get-buffer "ServNet"))
+            erc-default-recipients '("bob")))
+
+    (ert-info ("Value: t (default)")
+      (should (eq erc-hide-prompt t))
+      (with-current-buffer "ServNet"
+        (should (= (point) erc-insert-marker))
+        (erc--hide-prompt erc-server-process)
+        (should (string= ">" (get-text-property (point) 'display))))
+
+      (with-current-buffer "#chan"
+        (goto-char erc-insert-marker)
+        (should (string= ">" (get-text-property (point) 'display)))
+        (should (memq #'erc--unhide-prompt-on-self-insert pre-command-hook))
+        (goto-char erc-input-marker)
+        (ert-simulate-command '(self-insert-command 1 ?/))
+        (goto-char erc-insert-marker)
+        (should-not (get-text-property (point) 'display))
+        (should-not (memq #'erc--unhide-prompt-on-self-insert
+                          pre-command-hook)))
+
+      (with-current-buffer "bob"
+        (goto-char erc-insert-marker)
+        (should (string= ">" (get-text-property (point) 'display)))
+        (should (memq #'erc--unhide-prompt-on-self-insert pre-command-hook))
+        (goto-char erc-input-marker)
+        (ert-simulate-command '(self-insert-command 1 ?/))
+        (goto-char erc-insert-marker)
+        (should-not (get-text-property (point) 'display))
+        (should-not (memq #'erc--unhide-prompt-on-self-insert
+                          pre-command-hook)))
+
+      (with-current-buffer "ServNet"
+        (should (get-text-property erc-insert-marker 'display))
+        (should (memq #'erc--unhide-prompt-on-self-insert pre-command-hook))
+        (erc--unhide-prompt)
+        (should-not (memq #'erc--unhide-prompt-on-self-insert
+                          pre-command-hook))
+        (should-not (get-text-property erc-insert-marker 'display))))
+
+    (ert-info ("Value: server")
+      (setq erc-hide-prompt '(server))
+      (with-current-buffer "ServNet"
+        (erc--hide-prompt erc-server-process)
+        (should (string= ">" (get-text-property erc-insert-marker 'display))))
+
+      (with-current-buffer "#chan"
+        (should-not (get-text-property erc-insert-marker 'display)))
+
+      (with-current-buffer "bob"
+        (should-not (get-text-property erc-insert-marker 'display)))
+
+      (with-current-buffer "ServNet"
+        (erc--unhide-prompt)
+        (should-not (get-text-property erc-insert-marker 'display))))
+
+    (ert-info ("Value: channel")
+      (setq erc-hide-prompt '(channel))
+      (with-current-buffer "ServNet"
+        (erc--hide-prompt erc-server-process)
+        (should-not (get-text-property erc-insert-marker 'display)))
+
+      (with-current-buffer "bob"
+        (should-not (get-text-property erc-insert-marker 'display)))
+
+      (with-current-buffer "#chan"
+        (should (string= ">" (get-text-property erc-insert-marker 'display)))
+        (erc--unhide-prompt)
+        (should-not (get-text-property erc-insert-marker 'display))))
+
+    (ert-info ("Value: query")
+      (setq erc-hide-prompt '(query))
+      (with-current-buffer "ServNet"
+        (erc--hide-prompt erc-server-process)
+        (should-not (get-text-property erc-insert-marker 'display)))
+
+      (with-current-buffer "bob"
+        (should (string= ">" (get-text-property erc-insert-marker 'display)))
+        (erc--unhide-prompt)
+        (should-not (get-text-property erc-insert-marker 'display)))
+
+      (with-current-buffer "#chan"
+        (should-not (get-text-property erc-insert-marker 'display))))
+
+    (ert-info ("Value: nil")
+      (setq erc-hide-prompt nil)
+      (with-current-buffer "ServNet"
+        (erc--hide-prompt erc-server-process)
+        (should-not (get-text-property erc-insert-marker 'display)))
+
+      (with-current-buffer "bob"
+        (should-not (get-text-property erc-insert-marker 'display)))
+
+      (with-current-buffer "#chan"
+        (should-not (get-text-property erc-insert-marker 'display))
+        (erc--unhide-prompt) ; won't blow up when prompt already showing
+        (should-not (get-text-property erc-insert-marker 'display))))
+
+    (when noninteractive
+      (kill-buffer "#chan")
+      (kill-buffer "bob")
+      (kill-buffer "ServNet"))))
+
 (ert-deftest erc--switch-to-buffer ()
   (defvar erc-modified-channels-alist) ; lisp/erc/erc-track.el
 
-- 
2.36.1
0006-Customize-displaying-of-ERC-buffers-on-reconnect.patch (text/x-patch, 4 KB)
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Thu, 18 Nov 2021 23:39:54 -0800
Subject: [PATCH 06/35] Customize displaying of ERC buffers on reconnect

* lisp/erc/erc-backend.el (erc--server-last-reconnect-count):
Add variable to record last reconnect tally.

* lisp/erc/erc.el (erc-reconnect-display): Add new option to specify
channel-buffer display behavior on reconnect.
(erc-setup-buffer): Use option `erc-reconnect-display' if warranted.
(erc-cmd-JOIN): Forget last reconnect count when issuing a manual
/JOIN command.
(erc-connection-established): Record reconnect count in internal var
before resetting.
(Bug#51753)
---
 lisp/erc/erc-backend.el |  3 +++
 lisp/erc/erc.el         | 26 ++++++++++++++++++++++++--
 2 files changed, 27 insertions(+), 2 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 345b78f736..787c5a8b57 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -200,6 +200,9 @@ erc-server-connected
 (defvar-local erc-server-reconnect-count 0
   "Number of times we have failed to reconnect to the current server.")
 
+(defvar-local erc--server-last-reconnect-count 0
+  "Snapshot of reconnect count when the connection was established.")
+
 (defvar-local erc-server-quitting nil
   "Non-nil if the user requests a quit.")
 
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index e876a8f8ba..4f587cbfbc 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -131,6 +131,7 @@ erc-scripts
   :group 'erc)
 
 ;; Defined in erc-backend
+(defvar erc--server-last-reconnect-count)
 (defvar erc--server-reconnecting)
 (defvar erc-channel-members-changed-hook)
 (defvar erc-server-367-functions)
@@ -1562,6 +1563,22 @@ erc-join-buffer
                  (const :tag "Use current buffer" buffer)
                  (const :tag "Use current buffer" t)))
 
+(defcustom erc-reconnect-display nil
+  "How (and whether) to display a channel buffer upon reconnecting.
+
+This only affects automatic reconnections and is ignored when issuing a
+/reconnect command or reinvoking `erc-tls' with the same args (assuming
+success, of course).  See `erc-join-buffer' for a description of
+possible values."
+  :package-version '(ERC . "5.4.1") ; FIXME increment upon publishing to ELPA
+  :group 'erc-buffers
+  :type '(choice (const :tag "Use value of `erc-join-buffer'" nil)
+                 (const :tag "Split window and select" window)
+                 (const :tag "Split window, don't select" window-noselect)
+                 (const :tag "New frame" frame)
+                 (const :tag "Bury in new buffer" bury)
+                 (const :tag "Use current buffer" buffer)))
+
 (defcustom erc-frame-alist nil
   "Alist of frame parameters for creating erc frames.
 A value of nil means to use `default-frame-alist'."
@@ -1983,7 +2000,10 @@ erc-update-modules
 
 (defun erc-setup-buffer (buffer)
   "Consults `erc-join-buffer' to find out how to display `BUFFER'."
-  (pcase erc-join-buffer
+  (pcase (if (zerop (erc-with-server-buffer
+                      erc--server-last-reconnect-count))
+             erc-join-buffer
+           (or erc-reconnect-display erc-join-buffer))
     ('window
      (if (active-minibuffer-window)
          (display-buffer buffer)
@@ -3250,6 +3270,7 @@ erc-cmd-JOIN
             (switch-to-buffer (if (get-buffer chnl-name)
                                   chnl-name
                                 (concat chnl-name "/" server)))
+          (setq erc--server-last-reconnect-count 0)
 	  (erc-server-join-channel server chnl key)))))
   t)
 
@@ -4741,7 +4762,8 @@ erc-connection-established
             (nick (car (erc-response.command-args parsed)))
             (buffer (process-buffer proc)))
         (setq erc-server-connected t)
-	(setq erc-server-reconnect-count 0)
+        (setq erc--server-last-reconnect-count erc-server-reconnect-count
+              erc-server-reconnect-count 0)
         (erc-update-mode-line)
         (erc-set-initial-user-mode nick buffer)
         (erc-server-setup-periodical-ping buffer)
-- 
2.36.1
0007-Allow-exemption-from-flood-penalty-in-erc-backend.patch (text/x-patch, 2.8 KB)
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Sun, 13 Mar 2022 01:34:10 -0800
Subject: [PATCH 07/35] Allow exemption from flood penalty in erc-backend

* lisp/erc/erc-backend (erc-server-send, erc-server-PING): Change name
of param `forcep' in `erc-server-send' to `force' and change its type
to the union of the symbol `no-penalty' and the set of all other
non-nil values.  In `erc-server-PING', use this exemption when calling
`erc-server-send'.  This fix was fast tracked and summarily
incorporated into bug#48598 because users of the soju bouncer are all
affected.  See update #5 in the bug's email thread under the section
entitled "Riders" for an explanation.
---
 lisp/erc/erc-backend.el | 18 +++++++++---------
 1 file changed, 9 insertions(+), 9 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 787c5a8b57..96305c9410 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -816,11 +816,11 @@ erc-send-line
         (erc-split-line text)))
 
 ;; From Circe, with modifications
-(defun erc-server-send (string &optional forcep target)
+(defun erc-server-send (string &optional force target)
   "Send STRING to the current server.
-If FORCEP is non-nil, no flood protection is done - the string is
-sent directly.  This might cause the messages to arrive in a wrong
-order.
+When FORCE is non-nil, bypass flood protection so that STRING is sent
+directly without modifying the queue.  When FORCE is the symbol
+`no-penalty', exempt this round from accumulating a timeout penalty.
 
 If TARGET is specified, look up encoding information for that
 channel in `erc-encoding-coding-alist' or
@@ -836,11 +836,11 @@ erc-server-send
     (if (erc-server-process-alive)
         (erc-with-server-buffer
           (let ((str (concat string "\r\n")))
-            (if forcep
+            (if force
                 (progn
-                  (setq erc-server-flood-last-message
-                        (+ erc-server-flood-penalty
-                           erc-server-flood-last-message))
+                  (unless (eq force 'no-penalty)
+                    (cl-incf erc-server-flood-last-message
+                             erc-server-flood-penalty))
                   (erc-log-irc-protocol str 'outbound)
                   (condition-case nil
                       (progn
@@ -1470,7 +1470,7 @@ define-erc-response-handler
   (let ((pinger (car (erc-response.command-args parsed))))
     (erc-log (format "PING: %s" pinger))
     ;; ping response to the server MUST be forced, or you can lose big
-    (erc-server-send (format "PONG :%s" pinger) t)
+    (erc-server-send (format "PONG :%s" pinger) 'no-penalty)
     (when erc-verbose-server-ping
       (erc-display-message
        parsed 'error proc
-- 
2.36.1
0008-Don-t-set-erc-server-announced-name-unless-known.patch (text/x-patch, 2.3 KB)
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Mon, 14 Jun 2021 23:40:45 -0700
Subject: [PATCH 08/35] Don't set erc-server-announced-name unless known

* lisp/erc/erc.el (erc-open): whenever this function is called, the
variable `erc-server-announced-name' may be set locally in the calling
server buffer.  However, if that buffer's dialed server matches that
of the one being created, the announced name is copied over on faith.
But there's no guarantee that the name will match the one ultimately
emitted by the server during its introductory burst.  Beyond
potentially causing confusion in protocol logs, this behavior may
complicate debugging efforts.  Setting the variable to nil helps
ensure a consistent environment when preparing a buffer for all newly
dialed connections.  This commit also simplifies the setting of
`erc-server-connected', which is always nil when connecting and
vice-versa.
---
 lisp/erc/erc.el | 11 ++++-------
 1 file changed, 4 insertions(+), 7 deletions(-)

diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 4f587cbfbc..16252a181d 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -2047,11 +2047,7 @@ erc-open
 private key and the certificate.
 
 Returns the buffer for the given server or channel."
-  (let ((server-announced-name (when (and (boundp 'erc-session-server)
-                                          (string= server erc-session-server))
-                                 erc-server-announced-name))
-        (connected-p (unless connect erc-server-connected))
-        (buffer (erc-get-buffer-create server port channel))
+  (let ((buffer (erc-get-buffer-create server port channel))
         (old-buffer (current-buffer))
         old-point
         (continued-session (and erc-reuse-buffers erc--server-reconnecting)))
@@ -2062,8 +2058,9 @@ erc-open
     (let ((old-recon-count erc-server-reconnect-count))
       (erc-mode)
       (setq erc-server-reconnect-count old-recon-count))
-    (setq erc-server-announced-name server-announced-name)
-    (setq erc-server-connected connected-p)
+    (when (setq erc-server-connected (not connect))
+      (setq erc-server-announced-name
+            (buffer-local-value 'erc-server-announced-name old-buffer)))
     ;; connection parameters
     (setq erc-server-process process)
     (setq erc-insert-marker (make-marker))
-- 
2.36.1
0009-Require-erc-networks-in-erc.el.patch (text/x-patch, 4 KB)
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Tue, 17 Aug 2021 01:50:29 -0700
Subject: [PATCH 09/35] Require erc-networks in erc.el

* lisp/erc/erc.el: Require erc-networks.el, which ERC can't run
without these days.  To sidestep the circular dependency, require it
last, just after erc-goodies.  Remove the `declare-function' for
`erc-network-name' because it's not currently needed at load time.
(erc-log-irc-protocol, erc-hide-current-message-p): Remove `fboundp'
guard logic from `erc-network-name' invocations but preserve meaning
by interpreting `erc-network' being unset to mean module isn't loaded
or authoritative network detection has failed.
(erc-format-network): Likewise here.  At the moment, this function
always returns the empty string because the function
`erc-network-name' always returns non-nil, perhaps from the
fallback/failure sentinel "Unknown", perhaps from the printed form of
nil.

* lisp/erc/erc-networks.el (erc-network): This is called throughout
erc.el but was previously cumbersome to use on account of being
guarded by `fboundp'.  It now relies on the fact that its namesake
variable is set in target buffers as well.
---
 lisp/erc/erc-networks.el |  2 +-
 lisp/erc/erc.el          | 14 ++++----------
 2 files changed, 5 insertions(+), 11 deletions(-)

diff --git a/lisp/erc/erc-networks.el b/lisp/erc/erc-networks.el
index 553697ae84..58223f37cf 100644
--- a/lisp/erc/erc-networks.el
+++ b/lisp/erc/erc-networks.el
@@ -753,7 +753,7 @@ erc-determine-network
 
 (defun erc-network ()
   "Return the value of `erc-network' for the current server."
-  (erc-with-server-buffer erc-network))
+  (or erc-network (erc-with-server-buffer erc-network)))
 
 (defun erc-network-name ()
   "Return the name of the current network as a string."
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 16252a181d..45a01bcbf4 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -2388,8 +2388,6 @@ erc-debug-irc-protocol
 WARNING: Do not set this variable directly!  Instead, use the
 function `erc-toggle-debug-irc-protocol' to toggle its value.")
 
-(declare-function erc-network-name "erc-networks" ())
-
 (defun erc-log-irc-protocol (string &optional outbound)
   "Append STRING to the buffer *erc-protocol*.
 
@@ -2403,9 +2401,7 @@ erc-log-irc-protocol
 available at run time, starting with the network name, followed by the
 announced host name, and falling back to the dialed <server>:<port>."
   (when erc-debug-irc-protocol
-    (let ((esid (or (and (fboundp 'erc-network)
-                         (erc-network)
-                         (erc-network-name))
+    (let ((esid (or (and (erc-network) (erc-network-name))
                     erc-server-announced-name
                     (format "%s:%s" erc-session-server erc-session-port)))
           (ts (when erc-debug-irc-protocol-time-format
@@ -2808,7 +2804,7 @@ erc-hide-current-message-p
   (let* ((command (erc-response.command parsed))
          (sender (car (erc-parse-user (erc-response.sender parsed))))
          (channel (car (erc-response.command-args parsed)))
-         (network (or (and (fboundp 'erc-network-name) (erc-network-name))
+         (network (or (and (erc-network) (erc-network-name))
 		      (erc-shorten-server-name
 		       (or erc-server-announced-name
 			   erc-session-server))))
@@ -6528,10 +6524,7 @@ erc-format-target-and/or-server
 
 (defun erc-format-network ()
   "Return the name of the network we are currently on."
-  (let ((network (and (fboundp 'erc-network-name) (erc-network-name))))
-    (if (and network (symbolp network))
-        (symbol-name network)
-      "")))
+  (erc-network-name))
 
 (defun erc-format-target-and/or-network ()
   "Return the network or the current target and network combined.
@@ -7085,5 +7078,6 @@ erc-handle-irc-url
 ;; IMPORTANT: This require must appear _after_ the above (provide 'erc) to
 ;; avoid a recursive require error when byte-compiling the entire package.
 (require 'erc-goodies)
+(require 'erc-networks)
 
 ;;; erc.el ends here
-- 
2.36.1
0010-Update-ISUPPORT-handling-in-ERC.patch (text/x-patch, 14.7 KB)
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Thu, 12 Aug 2021 03:10:31 -0700
Subject: [PATCH 10/35] Update ISUPPORT handling in ERC

* lisp/erc/erc-backend (erc--isupport-params): Add new variable to
hold a hashmap of parsed `erc-server-parameters' in a more useful
format.  But keep `erc-server-parameters' around for public use.  We
currently lack dedicated local variables for certain discovered IRC
session properties, such as what prefix characters are supported for
channels, etc.  And the truth of this needs querying many times per
second at various points.  As such, caching here seems justified but
can be easily removed if deemed otherwise because all ingredients are
internal.
(erc--parse-isupport-value): Add helper function that parses an
ISUPPORT value and returns the component parts with backslash-x hex
escapes removed.  This can probably use some streamlining.
(erc--with-memoization): Add compat alias for use in internal ISUPPORT
getter.  Should be moved to `erc-compat.el' when that library is fully
reincorporated.
(erc--get-isupport-entry): Add internal getter to look up ISUPPORT
items.
(erc-server-005): Treat `erc-server-response' "command args" field as
read-only.  Previously, this field was set to nil after processing,
which was unhelpful to other parts of the library.  Also call above
mentioned helper to parse values.  And add some bookkeeping to handle
negation.

* lisp/erc/erc-capab.el (erc-capab-identify-send-messages): Use
internal ISUPPORT getter.

* lisp/erc/erc.el (erc-cmd-NICK, erc-parse-prefix,
erc-nickname-in-use): Use internal ISUPPORT getter.

* test/lisp/erc/erc-tests.el: Add tests for the above mentioned
changes in erc-backend.el.
---
 lisp/erc/erc-backend.el    | 97 ++++++++++++++++++++++++++++++++------
 lisp/erc/erc-capab.el      |  2 +-
 lisp/erc/erc.el            | 13 ++---
 test/lisp/erc/erc-tests.el | 95 +++++++++++++++++++++++++++++++++++++
 4 files changed, 184 insertions(+), 23 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 96305c9410..f8ca3df603 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -185,6 +185,11 @@ erc-server-parameters
 TOPICLEN=160 - maximum allowed topic length
 WALLCHOPS - supports sending messages to all operators in a channel")
 
+(defvar-local erc--isupport-params nil
+  "Hash map of \"ISUPPORT\" params.
+Keys are symbols.  Values are lists of zero or more strings with hex
+escapes removed.")
+
 ;;; Server and connection state
 
 (defvar erc-server-ping-timer-alist nil
@@ -1625,6 +1630,66 @@ define-erc-response-handler
      ?U (nth 3 (erc-response.command-args parsed))
      ?C (nth 4 (erc-response.command-args parsed)))))
 
+(defun erc--parse-isupport-value (value)
+  "Return list of unescaped components from an \"ISUPPORT\" VALUE."
+  ;; https://tools.ietf.org/html/draft-brocklesby-irc-isupport-03#section-2
+  ;;
+  ;; > The server SHOULD send "X", not "X="; this is the normalised form.
+  ;;
+  ;; Note: for now, assume the server will only send non-empty values,
+  ;; possibly with printable ASCII escapes.  Though in practice, the
+  ;; only two escapes we're likely to see are backslash and space,
+  ;; meaning the pattern is too liberal.
+  (let (case-fold-search)
+    (mapcar
+     (lambda (v)
+       (let ((start 0)
+             m
+             c)
+         (while (and (< start (length v))
+                     (string-match "[\\]x[0-9A-F][0-9A-F]" v start))
+           (setq m (substring v (+ 2 (match-beginning 0)) (match-end 0))
+                 c (string-to-number m 16))
+           (if (<= ?\  c ?~)
+               (setq v (concat (substring v 0 (match-beginning 0))
+                               (string c)
+                               (substring v (match-end 0)))
+                     start (- (match-end 0) 3))
+             (setq start (match-end 0))))
+         v))
+     (if (if (>= emacs-major-version 28)
+             (string-search "," value)
+           (string-match-p "," value))
+         (split-string value ",")
+       (list value)))))
+
+;; FIXME move to erc-compat (once we decide how to load it)
+(defalias 'erc--with-memoization
+  (cond
+   ((fboundp 'with-memoization) #'with-memoization) ; 29.1
+   ((fboundp 'cl--generic-with-memoization) #'cl--generic-with-memoization)
+   (t (lambda (_ v) v))))
+
+(defun erc--get-isupport-entry (key &optional single)
+  "Return an item for \"ISUPPORT\" token KEY, a symbol.
+When a lookup fails return nil.  Otherwise return a list whose CAR is
+KEY and whose CDR is zero or more strings.  With SINGLE, just return the
+first value, if any.  The latter is potentially ambiguous and only
+useful for tokens supporting a single primitive value."
+  (if-let* ((table (or erc--isupport-params
+                       (erc-with-server-buffer erc--isupport-params)))
+            (value (erc--with-memoization (gethash key table)
+                     (when-let ((v (assoc (symbol-name key)
+                                          erc-server-parameters)))
+                       (if (cdr v)
+                           (erc--parse-isupport-value (cdr v))
+                         '--empty--)))))
+      (pcase value
+        ('--empty-- (unless single (list key)))
+        (`(,head . ,_) (if single head (cons key value))))
+    (when table
+      (remhash key table))))
+
 (define-erc-response-handler (005)
   "Set the variable `erc-server-parameters' and display the received message.
 
@@ -1636,21 +1701,25 @@ define-erc-response-handler
 
 A server may send more than one 005 message."
   nil
-  (let ((line (mapconcat #'identity
-                         (setf (erc-response.command-args parsed)
-                               (cdr (erc-response.command-args parsed)))
-                         " ")))
-    (while (erc-response.command-args parsed)
-      (let ((section (pop (erc-response.command-args parsed))))
-        ;; fill erc-server-parameters
-        (when (string-match "^\\([A-Z]+\\)=\\(.*\\)$\\|^\\([A-Z]+\\)$"
+  (unless erc--isupport-params
+    (setq erc--isupport-params (make-hash-table)))
+  (let* ((args (cdr (erc-response.command-args parsed)))
+         (line (string-join args " ")))
+    (while args
+      (let ((section (pop args))
+            key
+            value
+            negated)
+        (when (string-match "^\\([A-Z]+\\)=\\(.*\\)$\\|^\\(-\\)?\\([A-Z]+\\)$"
                             section)
-          (add-to-list 'erc-server-parameters
-                       `(,(or (match-string 1 section)
-                              (match-string 3 section))
-                         .
-                         ,(match-string 2 section))))))
-    (erc-display-message parsed 'notice proc line)))
+          (setq key (or (match-string 1 section) (match-string 4 section))
+                value (match-string 2 section)
+                negated (and (match-string 3 section) '-))
+          (setf (alist-get key erc-server-parameters '- 'remove #'equal)
+                (or value negated))
+          (remhash (intern key) erc--isupport-params))))
+    (erc-display-message parsed 'notice proc line)
+    nil))
 
 (define-erc-response-handler (221)
   "Display the current user modes." nil
diff --git a/lisp/erc/erc-capab.el b/lisp/erc/erc-capab.el
index 8d0f40af99..c590b45fd2 100644
--- a/lisp/erc/erc-capab.el
+++ b/lisp/erc/erc-capab.el
@@ -137,7 +137,7 @@ erc-capab-identify-send-messages
              ;; could possibly check for '("IRCD" . "dancer") in
              ;; `erc-server-parameters' instead of looking for a specific name
              ;; in `erc-server-version'
-             (assoc "CAPAB" erc-server-parameters))
+             (erc--get-isupport-entry 'CAPAB))
     (erc-log "Sending CAPAB IDENTIFY-MSG and IDENTIFY-CTCP")
     (erc-server-send "CAPAB IDENTIFY-MSG")
     (erc-server-send "CAPAB IDENTIFY-CTCP")
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 45a01bcbf4..30185c1395 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -141,7 +141,6 @@ erc-server-connected
 (defvar erc-server-current-nick)
 (defvar erc-server-lag)
 (defvar erc-server-last-sent-time)
-(defvar erc-server-parameters)
 (defvar erc-server-process)
 (defvar erc-server-quitting)
 (defvar erc-server-reconnect-count)
@@ -3566,8 +3565,8 @@ erc-cmd-SQUERY
 (defun erc-cmd-NICK (nick)
   "Change current nickname to NICK."
   (erc-log (format "cmd: NICK: %s (erc-bad-nick: %S)" nick erc-bad-nick))
-  (let ((nicklen (cdr (assoc "NICKLEN" (erc-with-server-buffer
-                                         erc-server-parameters)))))
+  (let ((nicklen (erc-with-server-buffer
+                   (erc--get-isupport-entry 'NICKLEN 'single))))
     (and nicklen (> (length nick) (string-to-number nicklen))
          (erc-display-message
           nil 'notice 'active 'nick-too-long
@@ -4436,9 +4435,8 @@ erc-nickname-in-use
        (format "Nickname %s is %s, try another." nick reason))
     (setq erc-nick-change-attempt-count (+ erc-nick-change-attempt-count 1))
     (let ((newnick (nth 1 erc-default-nicks))
-          (nicklen (cdr (assoc "NICKLEN"
-                               (erc-with-server-buffer
-                                 erc-server-parameters)))))
+          (nicklen (erc-with-server-buffer
+                     (erc--get-isupport-entry 'NICKLEN 'single))))
       (setq erc-bad-nick t)
       ;; try to use a different nick
       (if erc-default-nicks
@@ -5049,8 +5047,7 @@ erc-channel-end-receiving-names
 (defun erc-parse-prefix ()
   "Return an alist of valid prefix character types and their representations.
 Example: (operator) o => @, (voiced) v => +."
-  (let ((str (or (cdr (assoc "PREFIX" (erc-with-server-buffer
-                                        erc-server-parameters)))
+  (let ((str (or (erc-with-server-buffer (erc--get-isupport-entry 'PREFIX t))
                  ;; provide a sane default
                  "(qaohv)~&@%+"))
         types chars)
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 061dfc2f5e..2fd3774b40 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -349,6 +349,101 @@ erc-lurker-maybe-trim
     (setq erc-lurker-ignore-chars "_-`") ; set of chars, not character alts
     (should (string= "nick" (erc-lurker-maybe-trim "nick-_`")))))
 
+(ert-deftest erc--parse-isupport-value ()
+  (should (equal (erc--parse-isupport-value "a,b") '("a" "b")))
+  (should (equal (erc--parse-isupport-value "a,b,c") '("a" "b" "c")))
+
+  (should (equal (erc--parse-isupport-value "abc") '("abc")))
+  (should (equal (erc--parse-isupport-value "\\x20foo") '(" foo")))
+  (should (equal (erc--parse-isupport-value "foo\\x20") '("foo ")))
+  (should (equal (erc--parse-isupport-value "a\\x20b\\x20c") '("a b c")))
+  (should (equal (erc--parse-isupport-value "a\\x20b\\x20c\\x20") '("a b c ")))
+  (should (equal (erc--parse-isupport-value "\\x20a\\x20b\\x20c") '(" a b c")))
+  (should (equal (erc--parse-isupport-value "a\\x20\\x20c") '("a  c")))
+  (should (equal (erc--parse-isupport-value "\\x20\\x20\\x20") '("   ")))
+  (should (equal (erc--parse-isupport-value "\\x5Co/") '("\\o/")))
+  (should (equal (erc--parse-isupport-value "\\x7F,\\x19") '("\\x7F" "\\x19")))
+  (should (equal (erc--parse-isupport-value "a\\x2Cb,c") '("a,b" "c"))))
+
+(ert-deftest erc--get-isupport-entry ()
+  (let ((erc--isupport-params (make-hash-table))
+        (erc-server-parameters '(("FOO" . "1") ("BAR") ("BAZ" . "A,B,C")))
+        (items (lambda ()
+                 (cl-loop for k being the hash-keys of erc--isupport-params
+                          using (hash-values v) collect (cons k v)))))
+
+    (should-not (erc--get-isupport-entry 'FAKE))
+    (should-not (erc--get-isupport-entry 'FAKE 'single))
+    (should (zerop (hash-table-count erc--isupport-params)))
+
+    (should (equal (erc--get-isupport-entry 'BAR) '(BAR)))
+    (should-not (erc--get-isupport-entry 'BAR 'single))
+    (should (= 1 (hash-table-count erc--isupport-params)))
+
+    (should (equal (erc--get-isupport-entry 'BAZ) '(BAZ "A" "B" "C")))
+    (should (equal (erc--get-isupport-entry 'BAZ 'single) "A"))
+    (should (= 2 (hash-table-count erc--isupport-params)))
+
+    (should (equal (erc--get-isupport-entry 'FOO 'single) "1"))
+    (should (equal (erc--get-isupport-entry 'FOO) '(FOO "1")))
+
+    (should (equal (funcall items)
+                   '((BAR . --empty--) (BAZ "A" "B" "C") (FOO "1"))))))
+
+(ert-deftest erc-server-005 ()
+  (let* ((erc-server-005-functions (copy-sequence erc-server-005-functions))
+         (hooked 0)
+         (verify #'ignore)
+         (hook (lambda (_ _) (funcall verify) (cl-incf hooked)))
+         erc-server-parameters
+         erc--isupport-params
+         erc-timer-hook
+         calls
+         args
+         parsed)
+    (add-hook 'erc-server-005-functions hook 90)
+    (should (eq (cadr erc-server-005-functions) hook))
+    (cl-letf (((symbol-function 'erc-display-message)
+               (lambda (_ _ _ line) (push line calls))))
+
+      (ert-info ("Baseline")
+        (setq args '("tester" "BOT=B" "EXCEPTS" "PREFIX=(ov)@+" "are supp...")
+              parsed (make-erc-response :command-args args :command "005"))
+
+        (setq verify
+              (lambda ()
+                (should (equal erc-server-parameters
+                               '(("PREFIX" . "(ov)@+") ("EXCEPTS")
+                                 ("BOT" . "B"))))
+                (should (zerop (hash-table-count erc--isupport-params)))
+                (should (equal "(ov)@+" (erc--get-isupport-entry 'PREFIX t)))
+                (should (equal '(EXCEPTS) (erc--get-isupport-entry 'EXCEPTS)))
+                (should (equal "B" (erc--get-isupport-entry 'BOT t)))
+                (should (string= (pop calls)
+                                 "BOT=B EXCEPTS PREFIX=(ov)@+ are supp..."))
+                (should (equal args (erc-response.command-args parsed)))))
+
+        (erc-call-hooks nil parsed))
+
+      (ert-info ("Negated, updated")
+        (setq args '("tester" "-EXCEPTS" "-FAKE" "PREFIX=(ohv)@%+" "are su...")
+              parsed (make-erc-response :command-args args :command "005"))
+
+        (setq verify
+              (lambda ()
+                (should (equal erc-server-parameters
+                               '(("PREFIX" . "(ohv)@%+") ("BOT" . "B"))))
+                (should (string= (pop calls)
+                                 "-EXCEPTS -FAKE PREFIX=(ohv)@%+ are su..."))
+                (should (equal "(ohv)@%+" (erc--get-isupport-entry 'PREFIX t)))
+                (should (equal "B" (erc--get-isupport-entry 'BOT t)))
+                (should-not (erc--get-isupport-entry 'EXCEPTS))
+                (should (equal args (erc-response.command-args parsed)))))
+
+        (erc-call-hooks nil parsed))
+      (should (= hooked 2))))
+  (should-not (cadr erc-server-005-functions)))
+
 (ert-deftest erc-ring-previous-command-base-case ()
   (ert-info ("Create ring when nonexistent and do nothing")
     (let (erc-input-ring
-- 
2.36.1
0011-Recognize-ASCII-and-strict-CASEMAPPINGs-in-ERC.patch (text/x-patch, 3.7 KB)
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Tue, 5 Oct 2021 19:03:56 -0700
Subject: [PATCH 11/35] Recognize ASCII and strict CASEMAPPINGs in ERC

* lisp/erc/erc.el (erc-downcase, erc--casemapping-rfc1459-strict,
erc--casemapping-rfc1459): Add new translation tables for the latter
two mappings and use them in `erc-downcase'.

* test/lisp/erc/erc-tests.el: Add test for `erc-downcase'.
---
 lisp/erc/erc.el            | 34 +++++++++++++++++++++++-----------
 test/lisp/erc/erc-tests.el | 24 ++++++++++++++++++++++++
 2 files changed, 47 insertions(+), 11 deletions(-)

diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 30185c1395..a995321ac3 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -393,18 +393,30 @@ erc-server-users
   "Hash table of users on the current server.
 It associates nicknames with `erc-server-user' struct instances.")
 
+(defconst erc--casemapping-rfc1459
+  (make-translation-table
+   '((?\[ . ?\{) (?\] . ?\}) (?\\ . ?\|) (?~  . ?^))
+   (mapcar (lambda (c) (cons c (+ c 32))) "ABCDEFGHIJKLMNOPQRSTUVWXYZ")))
+
+(defconst erc--casemapping-rfc1459-strict
+  (make-translation-table
+   '((?\[ . ?\{) (?\] . ?\}) (?\\ . ?\|))
+   (mapcar (lambda (c) (cons c (+ c 32))) "ABCDEFGHIJKLMNOPQRSTUVWXYZ")))
+
 (defun erc-downcase (string)
-  "Convert STRING to IRC standard conforming downcase."
-  (let ((s (downcase string))
-        (c '((?\[ . ?\{)
-             (?\] . ?\})
-             (?\\ . ?\|)
-             (?~  . ?^))))
-    (save-match-data
-      (while (string-match "[]\\[~]" s)
-        (aset s (match-beginning 0)
-              (cdr (assq (aref s (match-beginning 0)) c)))))
-    s))
+  "Return a downcased copy of STRING with properties.
+Use the CASEMAPPING ISUPPORT parameter to determine the style."
+  (let* ((mapping (erc--get-isupport-entry 'CASEMAPPING 'single))
+         (inhibit-read-only t))
+    (if (equal mapping "ascii")
+        (downcase string)
+      (with-temp-buffer
+        (insert string)
+        (translate-region (point-min) (point-max)
+                          (if (equal mapping "rfc1459-strict")
+                              erc--casemapping-rfc1459-strict
+                            erc--casemapping-rfc1459))
+        (buffer-string)))))
 
 (defmacro erc-with-server-buffer (&rest body)
   "Execute BODY in the current ERC server buffer.
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 2fd3774b40..9ac8e3c292 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -444,6 +444,30 @@ erc-server-005
       (should (= hooked 2))))
   (should-not (cadr erc-server-005-functions)))
 
+(ert-deftest erc-downcase ()
+  (let ((erc--isupport-params (make-hash-table)))
+
+    (puthash 'PREFIX '("(ov)@+") erc--isupport-params)
+    (puthash 'BOT '("B") erc--isupport-params)
+
+    (ert-info ("ascii")
+      (puthash 'CASEMAPPING  '("ascii") erc--isupport-params)
+      (should (equal (erc-downcase "Bob[m]`") "bob[m]`"))
+      (should (equal (erc-downcase "Tilde~") "tilde~" ))
+      (should (equal (erc-downcase "\\O/") "\\o/" )))
+
+    (ert-info ("rfc1459")
+      (puthash 'CASEMAPPING  '("rfc1459") erc--isupport-params)
+      (should (equal (erc-downcase "Bob[m]`") "bob{m}`" ))
+      (should (equal (erc-downcase "Tilde~") "tilde^" ))
+      (should (equal (erc-downcase "\\O/") "|o/" )))
+
+    (ert-info ("rfc1459-strict")
+      (puthash 'CASEMAPPING  '("rfc1459-strict") erc--isupport-params)
+      (should (equal (erc-downcase "Bob[m]`") "bob{m}`"))
+      (should (equal (erc-downcase "Tilde~") "tilde~" ))
+      (should (equal (erc-downcase "\\O/") "|o/" )))))
+
 (ert-deftest erc-ring-previous-command-base-case ()
   (ert-info ("Create ring when nonexistent and do nothing")
     (let (erc-input-ring
-- 
2.36.1
0012-Make-ERC-respect-spaces-in-server-passwords.patch (text/x-patch, 1.1 KB)
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Sun, 15 Aug 2021 21:57:24 -0700
Subject: [PATCH 12/35] Make ERC respect spaces in server passwords

* lisp/erc/erc.el (erc-login): Also known as connection passwords,
these are sent as the sole arg to the PASS command, which is nowadays
often overloaded with other semantics imposed by various entities to
convey things like bouncer or services creds.
---
 lisp/erc/erc.el | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index a995321ac3..3cbad2209e 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -6185,7 +6185,7 @@ erc-login
                    erc-session-server
                    erc-session-user-full-name))
   (if erc-session-password
-      (erc-server-send (format "PASS %s" erc-session-password))
+      (erc-server-send (concat "PASS :" erc-session-password))
     (message "Logging in without password"))
   (erc-server-send (format "NICK %s" (erc-current-nick)))
   (erc-server-send
-- 
2.36.1
0013-Add-helper-to-determine-local-channels-in-ERC.patch (text/x-patch, 2.4 KB)
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Mon, 16 Aug 2021 05:01:16 -0700
Subject: [PATCH 13/35] Add helper to determine local channels in ERC

* lisp/erc/erc.el (erc--valid-local-channel-p): Add internal helper to
determine whether an IRC channel is local according to its network's
CHANTYPES ISUPPORT parameter.

* test/lisp/erc/erc-tests.el (erc--valid-local-channel-p): Add test
for this helper.
---
 lisp/erc/erc.el            |  8 ++++++++
 test/lisp/erc/erc-tests.el | 12 ++++++++++++
 2 files changed, 20 insertions(+)

diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 3cbad2209e..11ce9d6630 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -3251,6 +3251,14 @@ erc-server-join-channel
 				 (concat " " password)
 			       "")))))
 
+(defun erc--valid-local-channel-p (channel)
+  "Non-nil when channel is server-local on a network that allows them."
+  (and-let* (((eq ?& (aref channel 0)))
+             (chan-types (erc--get-isupport-entry 'CHANTYPES 'single))
+             ((if (>= emacs-major-version 28)
+                  (string-search "&" chan-types)
+                (string-match-p "&" chan-types))))))
+
 (defun erc-cmd-JOIN (channel &optional key)
   "Join the channel given in CHANNEL, optionally with KEY.
 If CHANNEL is specified as \"-invite\", join the channel to which you
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 9ac8e3c292..8df9446c33 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -468,6 +468,18 @@ erc-downcase
       (should (equal (erc-downcase "Tilde~") "tilde~" ))
       (should (equal (erc-downcase "\\O/") "|o/" )))))
 
+(ert-deftest erc--valid-local-channel-p ()
+  (ert-info ("Local channels not supported")
+    (let ((erc--isupport-params (make-hash-table)))
+      (puthash 'CHANTYPES  '("#") erc--isupport-params)
+      (should-not (erc--valid-local-channel-p "#chan"))
+      (should-not (erc--valid-local-channel-p "&local"))))
+  (ert-info ("Local channels supported")
+    (let ((erc--isupport-params (make-hash-table)))
+      (puthash 'CHANTYPES  '("&#") erc--isupport-params)
+      (should-not (erc--valid-local-channel-p "#chan"))
+      (should (erc--valid-local-channel-p "&local")))))
+
 (ert-deftest erc-ring-previous-command-base-case ()
   (ert-info ("Create ring when nonexistent and do nothing")
     (let (erc-input-ring
-- 
2.36.1
0014-Add-eventual-replacement-for-erc-default-recipients.patch (text/x-patch, 4.8 KB)
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Tue, 19 Oct 2021 22:53:03 -0700
Subject: [PATCH 14/35] Add eventual replacement for erc-default-recipients

* lisp/erc/erc.el (erc--target, erc--target-channel,
erc--target-channel-local): Add new structs to hold info on a buffer's
target; stored in a local variable of the same name.
(erc--target-from-string): Add standalone constructor for
`erc--target'.
(erc--default-target): Add temporary internal getter to ease
transition to `erc--target' everywhere.
(erc-open): Create above items in non-server buffers.

* lisp/erc/erc-backend.el (erc-server-NICK): Recreate `erc--target'
when necessary.
---
 lisp/erc/erc-backend.el    |  4 ++--
 lisp/erc/erc.el            | 39 ++++++++++++++++++++++++++++++++++++++
 test/lisp/erc/erc-tests.el | 12 ++++++++++++
 3 files changed, 53 insertions(+), 2 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index f8ca3df603..1117e3069b 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -1427,8 +1427,8 @@ define-erc-response-handler
       (erc-buffer-filter
        (lambda ()
          (when (equal (erc-default-target) nick)
-           (setq erc-default-recipients
-                 (cons nn (cdr erc-default-recipients)))
+           (setq erc-default-recipients (cons nn (cdr erc-default-recipients))
+                 erc--target (erc--target-from-string nn))
            (rename-buffer nn t)         ; bug#12002
            (erc-update-mode-line)
            (cl-pushnew (current-buffer) bufs))))
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 11ce9d6630..85b95a1d7b 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1391,6 +1391,44 @@ define-erc-module
        (put ',enable  'definition-name ',name)
        (put ',disable 'definition-name ',name))))
 
+;; The rationale for favoring inheritance here (nicer dispatch) is
+;; kinda flimsy since there aren't yet any actual methods.
+
+(cl-defstruct erc--target
+  (string "" :type string :documentation "Received name of target.")
+  (symbol nil :type symbol :documentation "Case-mapped name as symbol."))
+
+;; These should probably take on a `joined' field to track joinedness,
+;; which should be toggled by `erc-server-JOIN', `erc-server-PART',
+;; etc.  Functions like `erc--current-buffer-joined-p' (bug#48598) may
+;; find it useful.
+
+(cl-defstruct (erc--target-channel (:include erc--target)))
+
+(cl-defstruct (erc--target-channel-local (:include erc--target-channel)))
+
+;; At some point, it may make sense to add a query type with an
+;; account field, which may help support reassociation across
+;; reconnects and nick changes (likely requires v3 extensions).
+
+(defun erc--target-from-string (string)
+  "Construct an `erc--target' variant from STRING."
+  (funcall (if (erc-channel-p string)
+               (if (erc--valid-local-channel-p string)
+                   #'make-erc--target-channel-local
+                 #'make-erc--target-channel)
+             #'make-erc--target)
+           :string string :symbol (intern (erc-downcase string))))
+
+(defvar-local erc--target nil
+  "Info about a buffer's target, if any.")
+
+;; Temporary internal getter to ease transition to `erc--target' everywhere.
+(defun erc--default-target ()
+  "Return target string or nil."
+  (when erc--target
+    (erc--target-string erc--target)))
+
 (defun erc-once-with-server-event (event f)
   "Run function F the next time EVENT occurs in the `current-buffer'.
 
@@ -2091,6 +2129,7 @@ erc-open
     (set-marker erc-insert-marker (point))
     ;; stack of default recipients
     (setq erc-default-recipients tgt-list)
+    (setq erc--target (and channel (erc--target-from-string channel)))
     (setq erc-server-current-nick nil)
     ;; Initialize erc-server-users and erc-channel-users
     (if connect
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 8df9446c33..9576aeb92b 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -480,6 +480,18 @@ erc--valid-local-channel-p
       (should-not (erc--valid-local-channel-p "#chan"))
       (should (erc--valid-local-channel-p "&local")))))
 
+(ert-deftest erc--target-from-string ()
+  (should (equal (erc--target-from-string "#chan")
+                 #s(erc--target-channel "#chan" \#chan)))
+
+  (should (equal (erc--target-from-string "Bob")
+                 #s(erc--target "Bob" bob)))
+
+  (let ((erc--isupport-params (make-hash-table)))
+    (puthash 'CHANTYPES  '("&#") erc--isupport-params)
+    (should (equal (erc--target-from-string "&Bitlbee")
+                   #s(erc--target-channel-local "&Bitlbee" &bitlbee)))))
+
 (ert-deftest erc-ring-previous-command-base-case ()
   (ert-info ("Create ring when nonexistent and do nothing")
     (let (erc-input-ring
-- 
2.36.1
0015-Discourage-ill-defined-use-of-buffer-targets-in-ERC.patch (text/x-patch, 8 KB)
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Wed, 20 Oct 2021 03:52:18 -0700
Subject: [PATCH 15/35] Discourage ill-defined use of buffer targets in ERC

* lisp/erc/erc.el (erc-default-recipients, erc-default-target):
Explain that the variable has fallen out of favor and that the
function may have been used historically by third-party code for
detecting channel subscription status, even though that's never been
the case internally since at least the adoption of version control.
Recommend newer alternatives.

(erc--current-buffer-joined-p): Add possibly temporary predicate for
detecting whether a buffer's target is a joined channel.  The existing
means are inconsistent, as discussed in bug#48598.  The mere fact that
they are disparate is unfriendly to new contributors.  For example, in
the function `erc-autojoin-channels', the `process-status' of the
`erc-server-process' is used to detect whether a buffer needs joining.
That's fine in that specific situation, but it won't work elsewhere.
And neither will checking whether `erc-default-target' is nil, so
long as `erc-delete-default-channel' and friends remain in play.

(erc-add-default-channel, erc-delete-default-channel, erc-add-query,
erc-delete-query): Deprecate these helpers, which rely on an unused
usage variant of `erc-default-recipients'.

* lisp/erc/erc-services.el: remove stray `erc-default-recipients'
declaration.

* lisp/erc/erc-backend.el (erc-server-NICK, erc-server-JOIN,
erc-server-KICK, erc-server-PART): wrap deprecated helpers to suppress
warnings.

* lisp/erc/erc-join.el (erc-autojoin-channels): Use helper to detect
whether a buffer needs joining.  Prefer this to server liveliness, as
explained above.
---
 lisp/erc/erc-backend.el | 10 +++++++---
 lisp/erc/erc-join.el    |  2 +-
 lisp/erc/erc-track.el   |  2 --
 lisp/erc/erc.el         | 41 ++++++++++++++++++++++++++++++++++++++++-
 4 files changed, 48 insertions(+), 7 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 1117e3069b..4c30eeb76f 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -1339,7 +1339,9 @@ define-erc-response-handler
                                              erc-server-process))
                       (when buffer
                         (set-buffer buffer)
-                        (erc-add-default-channel chnl)
+                        (with-suppressed-warnings
+                            ((obsolete erc-add-default-channel))
+                          (erc-add-default-channel chnl))
                         (erc-server-send (format "MODE %s" chnl)))
                       (erc-with-buffer (chnl proc)
                         (erc-channel-begin-receiving-names))
@@ -1376,7 +1378,8 @@ define-erc-response-handler
         (erc-with-buffer
             (buffer)
           (erc-remove-channel-users))
-        (erc-delete-default-channel ch buffer)
+        (with-suppressed-warnings ((obsolete erc-delete-default-channel))
+          (erc-delete-default-channel ch buffer))
         (erc-update-mode-line buffer))
        ((string= nick (erc-current-nick))
         (erc-display-message
@@ -1465,7 +1468,8 @@ define-erc-response-handler
         (erc-with-buffer
             (buffer)
           (erc-remove-channel-users))
-        (erc-delete-default-channel chnl buffer)
+        (with-suppressed-warnings ((obsolete erc-delete-default-channel))
+          (erc-delete-default-channel chnl buffer))
         (erc-update-mode-line buffer)
         (when erc-kill-buffer-on-part
           (kill-buffer buffer))))))
diff --git a/lisp/erc/erc-join.el b/lisp/erc/erc-join.el
index b9788c192b..425de4dc56 100644
--- a/lisp/erc/erc-join.el
+++ b/lisp/erc/erc-join.el
@@ -176,7 +176,7 @@ erc-autojoin-channels
                                                 (erc-downcase current)))))))))
 	      (when (or (not buffer)
 			(not (with-current-buffer buffer
-			       (erc-server-process-alive))))
+                               (erc--current-buffer-joined-p))))
 		(erc-server-join-channel server chan))))))))
   ;; Return nil to avoid stomping on any other hook funcs.
   nil)
diff --git a/lisp/erc/erc-track.el b/lisp/erc/erc-track.el
index 9118d7b994..e8117f9a89 100644
--- a/lisp/erc/erc-track.el
+++ b/lisp/erc/erc-track.el
@@ -353,8 +353,6 @@ erc-track-shorten-names
      (> (length s) erc-track-shorten-cutoff))
    erc-track-shorten-start))
 
-(defvar erc-default-recipients)
-
 (defun erc-all-buffer-names ()
   "Return all channel or query buffer names.
 Note that we cannot use `erc-channel-list' with a nil argument,
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 85b95a1d7b..fe1de72b32 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1906,6 +1906,21 @@ erc-buffer-list-with-nick
 
 ;; Some local variables
 
+;; TODO eventually deprecate this variable
+;;
+;; In the ancient, pre-CVS days (prior to June 2001), this list may
+;; have been used for supporting the changing of a buffer's target on
+;; the fly (mid-session).  Such usage, which allowed cons cells like
+;; (QUERY . bob) to serve as the list's head, was either never fully
+;; integrated or was partially clobbered prior to the introduction of
+;; version control.  But vestiges remain (see `erc-dcc-chat-mode').
+;; And despite appearances, no evidence has emerged that ERC ever
+;; supported one-to-many target buffers.  If such a thing was aspired
+;; to, it was never realized.
+;;
+;; New library code should use the `erc--target' struct instead.
+;; Third-party code can continue to use this until a getter for
+;; `erc--target' (or whatever replaces it) is exported.
 (defvar-local erc-default-recipients nil
   "List of default recipients of the current buffer.")
 
@@ -5867,6 +5882,27 @@ erc-nick-equal-p
 
 ;; default target handling
 
+(defun erc--current-buffer-joined-p ()
+  "Return whether the current target buffer is joined."
+  ;; This may be a reliable means of detecting subscription status,
+  ;; but it's also roundabout and awkward.  Perhaps it's worth
+  ;; discussing adding a joined slot to `erc--target' for this.
+  (cl-assert erc--target)
+  (and (erc--target-channel-p erc--target)
+       (erc-get-channel-user (erc-current-nick)) t))
+
+;; This function happens to return nil in channel buffers previously
+;; parted or those from which a user had been kicked.  While this
+;; "works" for detecting whether a channel is currently subscribed to,
+;; new code should consider using
+;;
+;;   (erc-get-channel-user (erc-current-nick))
+;;
+;; instead.  For retrieving a target regardless of subscription or
+;; connection status, use replacements based on `erc--target'.
+;; (Coming soon.)
+;;
+;; TODO deprecate this
 (defun erc-default-target ()
   "Return the current default target (as a character string) or nil if none."
   (let ((tgt (car erc-default-recipients)))
@@ -5877,12 +5913,14 @@ erc-default-target
 
 (defun erc-add-default-channel (channel)
   "Add CHANNEL to the default channel list."
+  (declare (obsolete "use `erc-cmd-JOIN' or similar instead" "29.1"))
   (let ((chl (downcase channel)))
     (setq erc-default-recipients
           (cons chl erc-default-recipients))))
 
 (defun erc-delete-default-channel (channel &optional buffer)
   "Delete CHANNEL from the default channel list."
+  (declare (obsolete "use `erc-cmd-PART' or similar instead" "29.1"))
   (with-current-buffer (if (and buffer
                                 (bufferp buffer))
                            buffer
@@ -5894,6 +5932,7 @@ erc-add-query
   "Add QUERY'd NICKNAME to the default channel list.
 
 The previous default target of QUERY type gets removed."
+  (declare (obsolete "use `erc-cmd-QUERY' or similar instead" "29.1"))
   (let ((d1 (car erc-default-recipients))
         (d2 (cdr erc-default-recipients))
         (qt (cons 'QUERY (downcase nickname))))
@@ -5904,7 +5943,7 @@ erc-add-query
 
 (defun erc-delete-query ()
   "Delete the topmost target if it is a QUERY."
-
+  (declare (obsolete "use one query buffer per target instead" "29.1"))
   (let ((d1 (car erc-default-recipients))
         (d2 (cdr erc-default-recipients)))
     (if (and (listp d1)
-- 
2.36.1
0016-Add-ERC-test-server-and-related-resources.patch (text/x-patch, 179.9 KB) - not displayed
0017-Address-long-standing-ERC-buffer-naming-issues.patch (text/x-patch, 136.2 KB) - not displayed
0018-SQUASH-ME-Add-user-oriented-test-scenarios-for-ERC.patch (text/x-patch, 296.6 KB) - not displayed
0019-Register-erc-kill-buffer-function-locally.patch (text/x-patch, 1.4 KB)
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Wed, 27 Oct 2021 21:13:24 -0700
Subject: [PATCH 19/35] Register erc-kill-buffer-function locally

* lisp/erc/erc.el (erc-kill-buffer-function): don't add hook when
loading file.  Not that it matters, but this would run twice because
of the erc{-backend} dependency cycle.  Move to major-mode setup and
make buffer-local instead.  Depends on tests in Bug#48598.
---
 lisp/erc/erc.el | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 65a249492d..06779c2c60 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1590,6 +1590,7 @@ erc-mode
   (setq-local paragraph-start
               (concat "\\(" (regexp-quote (erc-prompt)) "\\)"))
   (setq-local completion-ignore-case t)
+  (add-hook 'kill-buffer-hook #'erc-kill-buffer-function nil t)
   (add-hook 'completion-at-point-functions #'erc-complete-word-at-point nil t))
 
 ;; activation
@@ -7110,9 +7111,6 @@ erc-format-message
 
 ;;; Various hook functions
 
-;; FIXME: Don't set the hook globally!
-(add-hook 'kill-buffer-hook #'erc-kill-buffer-function)
-
 (defcustom erc-kill-server-hook '(erc-kill-server
                                   erc-networks-shrink-ids-and-buffer-names)
   "Invoked whenever a live server buffer is killed via `kill-buffer'."
-- 
2.36.1
0020-Don-t-call-erc-auto-query-twice-on-PRIVMSG.patch (text/x-patch, 7.2 KB)
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Fri, 7 May 2021 01:52:41 -0700
Subject: [PATCH 20/35] Don't call erc-auto-query twice on PRIVMSG

* lisp/erc/erc-backend.el (erc-server-JOIN): Use `erc--open-target'
instead of `erc-join'.
(erc-server-PRIVMSG): Don't call `erc-auto-query' at all, and instead
borrow the portion of its logic that detects when a query buffer
should be created instead of a channel buffer.

* lisp/erc/erc.el (erc-cmd-QUERY): Update the mode line explicitly
after calling `erc-query' in case it's needed after `erc-setup-buffer'
runs. Simplify.
(erc-query, erc--open-target): Replace uses of `erc-query'
with `erc--open-target' and make the former obsolete.  Don't call
`erc-update-mode-line' because `erc-open' already does that.
(erc-auto-query): Make this function obsolete.  It was previously only
used in erc-backend.el and only sewed confusion.
(erc-query-on-unjoined-chan-privmsg): Add note questioning its role.
It was previously only used by the now deprecated `erc-auto-query'.
---
 lisp/erc/erc-backend.el | 26 +++++++------------
 lisp/erc/erc.el         | 56 +++++++++++++++++++++++------------------
 2 files changed, 41 insertions(+), 41 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 2a352874e5..7e174a6fd1 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -1334,16 +1334,7 @@ define-erc-response-handler
         (let* ((str (cond
                      ;; If I have joined a channel
                      ((erc-current-nick-p nick)
-                      (setq buffer (erc-open erc-session-server erc-session-port
-                                             nick erc-session-user-full-name
-                                             nil nil
-                                             (list chnl) chnl
-                                             erc-server-process
-                                             nil
-                                             erc-session-username
-                                             (erc-networks--id-given
-                                              erc-networks--id)))
-                      (when buffer
+                      (when (setq buffer (erc--open-target chnl))
                         (set-buffer buffer)
                         (with-suppressed-warnings
                             ((obsolete erc-add-default-channel))
@@ -1534,6 +1525,13 @@ define-erc-response-handler
              fnick)
         (setf (erc-response.contents parsed) msg)
         (setq buffer (erc-get-buffer (if privp nick tgt) proc))
+        ;; Even worth checking for empty target here? (invalid anyway)
+        (unless (or buffer noticep (string-empty-p tgt) (eq ?$ (aref tgt 0)))
+          (if (and privp msgp (not (erc-is-message-ctcp-and-not-action-p msg)))
+              (when erc-auto-query
+                (let ((erc-join-buffer erc-auto-query))
+                  (setq buffer (erc--open-target nick))))
+            (setq buffer (erc--open-target tgt))))
         (when buffer
           (with-current-buffer buffer
             (when privp (erc--unhide-prompt))
@@ -1569,13 +1567,7 @@ define-erc-response-handler
                                     s parsed buffer nick)
                 (run-hook-with-args-until-success
                  'erc-echo-notice-hook s parsed buffer nick))
-            (erc-display-message parsed nil buffer s)))
-        (when (string= cmd "PRIVMSG")
-          (erc-auto-query proc parsed))))))
-
-;; FIXME: need clean way of specifying extra hooks in
-;; define-erc-response-handler.
-(add-hook 'erc-server-PRIVMSG-functions #'erc-auto-query)
+            (erc-display-message parsed nil buffer s)))))))
 
 (define-erc-response-handler (QUIT)
   "Another user has quit IRC." nil
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 06779c2c60..463c497844 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -3790,13 +3790,14 @@ erc-cmd-QUERY
   ;; `kill-buffer'?  If it makes sense, re-add it.  -- SK @ 2021-11-11
   (interactive
    (list (read-string "Start a query with: ")))
-  (let ((session-buffer (erc-server-buffer))
-        (erc-join-buffer erc-query-display))
-    (if user
-        (erc-query user session-buffer)
+  (unless user
       ;; currently broken, evil hack to display help anyway
                                         ;(erc-delete-query))))
-      (signal 'wrong-number-of-arguments ""))))
+    (signal 'wrong-number-of-arguments ""))
+  (let ((erc-join-buffer erc-query-display))
+    (erc-with-server-buffer
+     (erc--open-target user))))
+
 (defalias 'erc-cmd-Q #'erc-cmd-QUERY)
 
 (defun erc-quit/part-reason-default ()
@@ -4472,28 +4473,30 @@ erc-debug-missing-hooks
   (nconc erc-server-vectors (list parsed))
   nil)
 
-(defun erc-query (target server)
-  "Open a query buffer on TARGET, using SERVER.
+(defun erc--open-target (target)
+  "Open an ERC buffer on TARGET."
+  (erc-open erc-session-server
+            erc-session-port
+            (erc-current-nick)
+            erc-session-user-full-name
+            nil
+            nil
+            (list target)
+            target
+            erc-server-process
+            nil
+            erc-session-username
+            (erc-networks--id-given erc-networks--id)))
+
+(defun erc-query (target server-buffer)
+  "Open a query buffer on TARGET using SERVER-BUFFER.
 To change how this query window is displayed, use `let' to bind
 `erc-join-buffer' before calling this."
-  (unless (and server
-               (buffer-live-p server)
-               (set-buffer server))
+  (declare (obsolete "bind `erc-cmd-query' and call `erc-cmd-QUERY'" "29.1"))
+  (unless (buffer-live-p server-buffer)
     (error "Couldn't switch to server buffer"))
-  (let ((buf (erc-open erc-session-server
-                       erc-session-port
-                       (erc-current-nick)
-                       erc-session-user-full-name
-                       nil
-                       nil
-                       (list target)
-                       target
-                       erc-server-process
-                       erc-session-username)))
-    (unless buf
-      (error "Couldn't open query window"))
-    (erc-update-mode-line)
-    buf))
+  (with-current-buffer server-buffer
+    (erc--open-target target)))
 
 (defcustom erc-auto-query 'window-noselect
   "If non-nil, create a query buffer each time you receive a private message.
@@ -4512,6 +4515,9 @@ erc-auto-query
                  (const :tag "Use current buffer" buffer)
                  (const :tag "Use current buffer" t)))
 
+;; FIXME either retire this or put it to use or more clearly explain
+;; what it's supposed to do.  It's currently only used by the obsolete
+;; function `erc-auto-query'.
 (defcustom erc-query-on-unjoined-chan-privmsg t
   "If non-nil create query buffer on receiving any PRIVMSG at all.
 This includes PRIVMSGs directed to channels.  If you are using an IRC
@@ -4634,6 +4640,8 @@ erc-auto-query
              (erc-cmd-QUERY query))
            nil))))
 
+(make-obsolete 'erc-auto-query "try erc-cmd-QUERY instead" "29.1")
+
 (defun erc-is-message-ctcp-p (message)
   "Check if MESSAGE is a CTCP message or not."
   (string-match "^\C-a\\([^\C-a]*\\)\C-a?$" message))
-- 
2.36.1
0021-SQUASH-ME-Add-ERC-scenarios-for-identity-aware-msg-h.patch (text/x-patch, 6.5 KB)
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Thu, 30 Sep 2021 06:11:50 -0700
Subject: [PATCH 21/35] SQUASH-ME: Add ERC scenarios for identity-aware msg
 handlers

* test/lisp/erc/erc-scenarios/erc-scenarios-misc.el: Add test for
server masks.

* test/lisp/erc/erc-scenarios/resources/base/mask-target-routing/foonet.eld:
Add server data for server-mask test
---
 .../erc/erc-scenarios/erc-scenarios-misc.el   | 35 +++++++++++++++
 .../base/mask-target-routing/foonet.eld       | 45 +++++++++++++++++++
 2 files changed, 80 insertions(+)
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/mask-target-routing/foonet.eld

diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-misc.el b/test/lisp/erc/erc-scenarios/erc-scenarios-misc.el
index 956280c2e7..f65041c97c 100644
--- a/test/lisp/erc/erc-scenarios/erc-scenarios-misc.el
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-misc.el
@@ -107,4 +107,39 @@ erc-scenarios-networks-announced-missing
         (should-not erc-network)
         (should (string= erc-server-announced-name "irc.foonet.org"))))))
 
+;; Targets that are host/server masks like $*, $$*, and #* are routed
+;; to the server buffer: https://github.com/ircdocs/wooooms/issues/5
+
+(ert-deftest erc-scenarios-base-mask-target-routing ()
+  :tags '(:expensive-test)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/mask-target-routing")
+       (erc-d-linger-secs 0.5)
+       (dumb-server (erc-d-run "localhost" t 'foonet))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter)))
+
+    (ert-info ("Connect to foonet")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :password "changeme"
+                                :full-name "tester")
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+
+    (erc-d-t-wait-for 10 (get-buffer "foonet"))
+
+    (ert-info ("Channel buffer #foo playback received")
+      (with-current-buffer (erc-d-t-wait-for 3 (get-buffer "#foo"))
+        (funcall expect 10 "Excellent workman")))
+
+    (ert-info ("Global notices routed to server buffer")
+      (with-current-buffer "foonet"
+        (funcall expect 10 "going down soon")
+        (funcall expect 10 "this is a warning")
+        (funcall expect 10 "second warning")
+        (funcall expect 10 "final warning")))
+
+    (should-not (get-buffer "$*"))))
+
 ;;; erc-scenarios-misc.el ends here
diff --git a/test/lisp/erc/erc-scenarios/resources/base/mask-target-routing/foonet.eld b/test/lisp/erc/erc-scenarios/resources/base/mask-target-routing/foonet.eld
new file mode 100644
index 0000000000..796d5566b6
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/mask-target-routing/foonet.eld
@@ -0,0 +1,45 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.foonet.org 003 tester :This server was created Mon, 31 May 2021 09:56:24 UTC")
+ (0 ":irc.foonet.org 004 tester irc.foonet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.foonet.org 005 tester AWAYLEN=390 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# ELIST=U EXCEPTS EXTBAN=,m FORWARD=f INVEX KICKLEN=390 :are supported by this server")
+ (0 ":irc.foonet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=foonet NICKLEN=32 PREFIX=(qaohv)~&@%+ STATUSMSG=~&@%+ TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 TOPICLEN=390 UTF8MAPPING=rfc8265 UTF8ONLY WHOX :are supported by this server")
+ (0 ":irc.foonet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.foonet.org 251 tester :There are 0 users and 4 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 254 tester 2 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 4 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 4 4 :Current local users 4, max 4")
+ (0 ":irc.foonet.org 266 tester 4 4 :Current global users 4, max 4")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":[email protected] JOIN #foo")
+ (0 ":irc.foonet.org 353 tester = #foo :alice @bob rando tester")
+ (0 ":irc.foonet.org 366 tester #foo :End of /NAMES list.")
+ (0 ":***[email protected] PRIVMSG #foo :Buffer Playback...")
+ (0 ":[email protected] PRIVMSG #foo :[10:00:02] bob: All that he is hath reference to your highness.")
+ (0 ":[email protected] PRIVMSG #foo :[10:00:06] alice: Excellent workman! Thou canst not paint a man so bad as is thyself.")
+ (0 ":***[email protected] PRIVMSG #foo :Playback Complete.")
+ (0 ":irc.foonet.org NOTICE tester :[09:56:57] This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.")
+ (0 ":irc.foonet.org 305 tester :You are no longer marked as being away"))
+
+((mode 5 "MODE #foo")
+ (0 ":irc.foonet.org 324 tester #foo +nt")
+ (0 ":irc.foonet.org 329 tester #foo 1622454985")
+ ;; Invalid msg
+ (0.1 ":[email protected] PRIVMSG :")
+ (0.1 ":[email protected] PRIVMSG #foo :bob: Farewell, pretty lady: you must hold the credit of your father.")
+ (0.1 ":[email protected] NOTICE $* :[Global notice] going down soon.")
+ (0.1 ":[email protected] PRIVMSG #foo :bob: Well, this is the forest of Arden.")
+ (0.1 ":[email protected] NOTICE $$* :[Global notice] this is a warning.")
+ (0.1 ":[email protected] PRIVMSG #foo :bob: Be married under a bush, like a beggar ? Get you to church, and have a good priest that can tell you what marriage is: this fellow will but join you together as they join wainscot; then one of you will prove a shrunk panel, and like green timber, warp, warp.")
+ (0.1 ":[email protected] PRIVMSG $* :[Global msg] second warning.")
+ (0.1 ":[email protected] PRIVMSG #foo :bob: And will you, being a man of your breeding.")
+ (0.1 ":[email protected] NOTICE #* :[Global notice] final warning."))
-- 
2.36.1
0022-Favor-network-identities-in-erc-join.patch (text/x-patch, 24 KB)
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Sun, 30 May 2021 00:50:50 -0700
Subject: [PATCH 22/35] Favor network identities in erc-join

* lisp/erc/erc-join.el (erc-autojoin-server-match): Favor network
identities, falling back on old definition.
(erc-autojoin--join): Add new helper containing common code from
hookees `erc-autojoin-after-ident' and `erc-autojoin-channels'.
(erc-autojoin-after-ident, erc-autojoin-channels): No longer make a
point of returning nil because the hooks they're registered on,
`erc-nickserv-identified-hook' and `erc-after-connect', don't stop on
success.
(erc-autojoin--mutate): Add helper for `erc-autojoin-add' and
`erc-autojoin-remove'.
(erc-autojoin-add, erc-autojoin-remove): Favor given network
identities, over networks, when matching keys for
`erc-autojoin-channels-alist'.
---
 lisp/erc/erc-join.el            | 121 +++++------
 test/lisp/erc/erc-join-tests.el | 361 ++++++++++++++++++++++++++++++++
 2 files changed, 411 insertions(+), 71 deletions(-)
 create mode 100644 test/lisp/erc/erc-join-tests.el

diff --git a/lisp/erc/erc-join.el b/lisp/erc/erc-join.el
index 425de4dc56..d4edca236d 100644
--- a/lisp/erc/erc-join.el
+++ b/lisp/erc/erc-join.el
@@ -33,8 +33,6 @@
 ;;; Code:
 
 (require 'erc)
-(require 'auth-source)
-(require 'erc-networks)
 
 (defgroup erc-autojoin nil
   "Enable autojoining."
@@ -57,11 +55,16 @@ erc-autojoin-channels-alist
 Every element in the alist has the form (SERVER . CHANNELS).
 SERVER is a regexp matching the server, and channels is the list
 of channels to join.  SERVER can also be a symbol, in which case
-it is matched against the value of `erc-network' instead of
+it's matched against a non-nil `:id' passed to `erc' or `erc-tls'
+when connecting or the value of the current `erc-network' instead of
 `erc-server-announced-name' or `erc-session-server' (this can be
 useful when connecting to an IRC proxy that relays several
 networks under the same server).
 
+Note that for historical reasons, this option is mutated at runtime,
+which is regrettable but here to stay.  Please double check the value
+before saving it to a `custom-file'.
+
 If the channel(s) require channel keys for joining, the passwords
 are found via auth-source.  For instance, if you use ~/.authinfo
 as your auth-source backend, then put something like the
@@ -123,33 +126,32 @@ erc-autojoin-channels-delayed
       (erc-autojoin-channels server nick))))
 
 (defun erc-autojoin-server-match (candidate)
-  "Match the current network or server against CANDIDATE.
-This should be a key from `erc-autojoin-channels-alist'."
-  (or (eq candidate (erc-network))
-      (and (stringp candidate)
-	   (string-match-p candidate
-                           (or erc-server-announced-name
-			       erc-session-server)))))
+  "Match the current network ID or server against CANDIDATE.
+CANDIDATE is a key from `erc-autojoin-channels-alist'.  Return the
+matching entity, either a string or a non-nil symbol (in the case of a
+network or a network ID).  Return nil on failure."
+  (if (symbolp candidate)
+      (eq (or (erc-networks--id-given erc-networks--id) (erc-network))
+          candidate)
+    (when (stringp candidate)
+      (string-match-p candidate (or erc-server-announced-name
+                                    erc-session-server)))))
+
+(defun erc-autojoin--join ()
+  ;; This is called in the server buffer
+  (pcase-dolist (`(,name . ,channels) erc-autojoin-channels-alist)
+    (when-let ((match (erc-autojoin-server-match name)))
+      (dolist (chan channels)
+        (let ((buf (erc-get-buffer chan erc-server-process)))
+          (unless (and buf (with-current-buffer buf
+                             (erc--current-buffer-joined-p)))
+            (erc-server-join-channel match chan)))))))
 
 (defun erc-autojoin-after-ident (_network _nick)
   "Autojoin channels in `erc-autojoin-channels-alist'.
 This function is run from `erc-nickserv-identified-hook'."
-  (if erc--autojoin-timer
-      (setq erc--autojoin-timer
-	    (cancel-timer erc--autojoin-timer)))
   (when (eq erc-autojoin-timing 'ident)
-    (let ((server (or erc-session-server erc-server-announced-name))
-	  (joined (mapcar (lambda (buf)
-			    (with-current-buffer buf (erc-default-target)))
-			  (erc-channel-list erc-server-process))))
-      ;; We may already be in these channels, e.g. because the
-      ;; autojoin timer went off.
-      (dolist (l erc-autojoin-channels-alist)
-	(when (erc-autojoin-server-match (car l))
-	  (dolist (chan (cdr l))
-	    (unless (erc-member-ignore-case chan joined)
-	      (erc-server-join-channel server chan)))))))
-  nil)
+    (erc-autojoin--join)))
 
 (defun erc-autojoin-channels (server nick)
   "Autojoin channels in `erc-autojoin-channels-alist'."
@@ -162,24 +164,7 @@ erc-autojoin-channels
 			      #'erc-autojoin-channels-delayed
 			      server nick (current-buffer))))
     ;; `erc-autojoin-timing' is `connect':
-    (let ((server (or erc-session-server erc-server-announced-name)))
-      (dolist (l erc-autojoin-channels-alist)
-        (when (erc-autojoin-server-match (car l))
-	  (dolist (chan (cdr l))
-	    (let ((buffer
-                   (car (erc-buffer-filter
-                         (lambda ()
-                           (let ((current (erc-default-target)))
-                             (and (stringp current)
-                                  (erc-autojoin-server-match (car l))
-                                  (string-equal (erc-downcase chan)
-                                                (erc-downcase current)))))))))
-	      (when (or (not buffer)
-			(not (with-current-buffer buffer
-                               (erc--current-buffer-joined-p))))
-		(erc-server-join-channel server chan))))))))
-  ;; Return nil to avoid stomping on any other hook funcs.
-  nil)
+    (erc-autojoin--join)))
 
 (defun erc-autojoin-current-server ()
   "Compute the current server for lookup in `erc-autojoin-channels-alist'.
@@ -190,24 +175,29 @@ erc-autojoin-current-server
 	(match-string 1 server)
       server)))
 
+(defun erc-autojoin--mutate (proc parsed remove)
+  (when-let* ((nick (car (erc-parse-user (erc-response.sender parsed))))
+              ((erc-current-nick-p nick))
+              (chnl (car (erc-response.command-args parsed)))
+              (elem (or (and (erc--valid-local-channel-p chnl)
+                             (regexp-quote erc-server-announced-name))
+                        (erc-networks--id-given erc-networks--id)
+                        (erc-network)
+                        (with-current-buffer (process-buffer proc)
+                          (erc-autojoin-current-server))))
+              (test (if (symbolp elem) #'eq #'equal)))
+    (if remove
+        (let ((cs (delete chnl (assoc-default elem erc-autojoin-channels-alist
+                                              test))))
+          (setf (alist-get elem erc-autojoin-channels-alist nil (null cs) test)
+                cs))
+      (cl-pushnew chnl
+                  (alist-get elem erc-autojoin-channels-alist nil nil test)
+                  :test #'equal))))
+
 (defun erc-autojoin-add (proc parsed)
   "Add the channel being joined to `erc-autojoin-channels-alist'."
-  (let* ((chnl (erc-response.contents parsed))
-	 (nick (car (erc-parse-user (erc-response.sender parsed))))
-	 (server (with-current-buffer (process-buffer proc)
-		   (erc-autojoin-current-server))))
-    (when (erc-current-nick-p nick)
-      (let ((elem (or (assoc (erc-network) erc-autojoin-channels-alist)
-		      (assoc server erc-autojoin-channels-alist))))
-	(if elem
-	    (unless (member chnl (cdr elem))
-	      (setcdr elem (cons chnl (cdr elem))))
-	  ;; This always keys on server, not network -- user can
-	  ;; override by simply adding a network to
-	  ;; `erc-autojoin-channels-alist'
-	  (setq erc-autojoin-channels-alist
-		(cons (list server chnl)
-		      erc-autojoin-channels-alist))))))
+  (erc-autojoin--mutate proc parsed nil)
   ;; We must return nil to tell ERC to continue running the other
   ;; functions.
   nil)
@@ -216,18 +206,7 @@ erc-autojoin-add
 
 (defun erc-autojoin-remove (proc parsed)
   "Remove the channel being left from `erc-autojoin-channels-alist'."
-  (let* ((chnl (car (erc-response.command-args parsed)))
-	 (nick (car (erc-parse-user (erc-response.sender parsed))))
-	 (server (with-current-buffer (process-buffer proc)
-		   (erc-autojoin-current-server))))
-    (when (erc-current-nick-p nick)
-      (let ((elem (or (assoc (erc-network) erc-autojoin-channels-alist)
-		      (assoc server erc-autojoin-channels-alist))))
-	(when elem
-	  (setcdr elem (delete chnl (cdr elem)))
-	  (unless (cdr elem)
-	    (setq erc-autojoin-channels-alist
-		  (delete elem erc-autojoin-channels-alist)))))))
+  (erc-autojoin--mutate proc parsed 'remove)
   ;; We must return nil to tell ERC to continue running the other
   ;; functions.
   nil)
diff --git a/test/lisp/erc/erc-join-tests.el b/test/lisp/erc/erc-join-tests.el
new file mode 100644
index 0000000000..8210defbfb
--- /dev/null
+++ b/test/lisp/erc/erc-join-tests.el
@@ -0,0 +1,361 @@
+;;; erc-join-tests.el --- Tests for erc-join.  -*- lexical-binding:t -*-
+
+;; Copyright (C) 2020-2022 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+;;
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published
+;; by the Free Software Foundation, either version 3 of the License,
+;; or (at your option) any later version.
+;;
+;; GNU Emacs is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Code:
+
+(require 'ert-x)
+(require 'erc-join)
+(require 'erc-networks)
+
+(ert-deftest erc-autojoin-channels--connect ()
+  (should (eq erc-autojoin-timing 'connect))
+  (should (= erc-autojoin-delay 30))
+  (should-not erc--autojoin-timer)
+
+  (let (calls
+        common
+        erc-kill-channel-hook erc-kill-server-hook erc-kill-buffer-hook)
+
+    (cl-letf (((symbol-function 'erc-server-send)
+               (lambda (line) (push line calls))))
+
+      (setq common
+            (lambda ()
+              (ert-with-test-buffer (:name "foonet")
+                (erc-mode)
+                (setq erc-server-process
+                      (start-process "true" (current-buffer) "true")
+                      erc-network 'FooNet
+                      erc-session-server "irc.gnu.chat"
+                      erc-server-current-nick "tester"
+                      erc-networks--id (erc-networks--id-create nil)
+                      erc-server-announced-name "foo.gnu.chat")
+                (set-process-query-on-exit-flag erc-server-process nil)
+                (erc-autojoin-channels erc-server-announced-name
+                                       "tester")
+                (should-not erc--autojoin-timer))))
+
+      (ert-info ("Join immediately on connect; server")
+        (let ((erc-autojoin-channels-alist '(("\\.gnu\\.chat\\'" "#chan"))))
+          (funcall common))
+        (should (equal (pop calls) "JOIN #chan")))
+
+      (ert-info ("Join immediately on connect; network")
+        (let ((erc-autojoin-channels-alist '((FooNet "#chan"))))
+          (funcall common))
+        (should (equal (pop calls) "JOIN #chan")))
+
+      (ert-info ("Do nothing; server")
+        (let ((erc-autojoin-channels-alist '(("bar\\.gnu\\.chat" "#chan"))))
+          (funcall common))
+        (should-not calls))
+
+      (ert-info ("Do nothing; network")
+        (let ((erc-autojoin-channels-alist '((BarNet "#chan"))))
+          (funcall common))
+        (should-not calls)))))
+
+(ert-deftest erc-autojoin-channels--delay ()
+  (should (eq erc-autojoin-timing 'connect))
+  (should (= erc-autojoin-delay 30))
+  (should-not erc--autojoin-timer)
+
+  (let (calls
+        common
+        erc-kill-channel-hook erc-kill-server-hook erc-kill-buffer-hook
+        (erc-autojoin-timing 'ident)
+        (erc-autojoin-delay 0.05))
+
+    (cl-letf (((symbol-function 'erc-server-send)
+               (lambda (line) (push line calls)))
+              ((symbol-function 'erc-autojoin-after-ident)
+               (lambda (&rest _r) (error "I ran but shouldn't have"))))
+
+      (setq common
+            (lambda ()
+              (ert-with-test-buffer (:name "foonet")
+                (erc-mode)
+                (setq erc-server-process
+                      (start-process "true" (current-buffer) "true")
+                      erc-network 'FooNet
+                      erc-session-server "irc.gnu.chat"
+                      erc-server-current-nick "tester"
+                      erc-networks--id (erc-networks--id-create nil)
+                      erc-server-announced-name "foo.gnu.chat")
+                (set-process-query-on-exit-flag erc-server-process nil)
+                (should-not erc--autojoin-timer)
+                (erc-autojoin-channels erc-server-announced-name "tester")
+                (should erc--autojoin-timer)
+                (should-not calls)
+                (sleep-for 0.1))))
+
+      (ert-info ("Deferred on connect; server")
+        (let ((erc-autojoin-channels-alist '(("\\.gnu\\.chat\\'" "#chan"))))
+          (funcall common))
+        (should (equal (pop calls) "JOIN #chan")))
+
+      (ert-info ("Deferred on connect; network")
+        (let ((erc-autojoin-channels-alist '((FooNet "#chan"))))
+          (funcall common))
+        (should (equal (pop calls) "JOIN #chan")))
+
+      (ert-info ("Do nothing; server")
+        (let ((erc-autojoin-channels-alist '(("bar\\.gnu\\.chat" "#chan"))))
+          (funcall common))
+        (should-not calls)))))
+
+(ert-deftest erc-autojoin-channels--ident ()
+  (should (eq erc-autojoin-timing 'connect))
+  (should (= erc-autojoin-delay 30))
+  (should-not erc--autojoin-timer)
+
+  (let (calls
+        common
+        erc-kill-channel-hook erc-kill-server-hook erc-kill-buffer-hook
+        (erc-autojoin-timing 'ident))
+
+    (cl-letf (((symbol-function 'erc-server-send)
+               (lambda (line) (push line calls))))
+
+      (setq common
+            (lambda ()
+              (ert-with-test-buffer (:name "foonet")
+                (erc-mode)
+                (setq erc-server-process
+                      (start-process "true" (current-buffer) "true")
+                      erc-network 'FooNet
+                      erc-server-current-nick "tester"
+                      erc-networks--id (erc-networks--id-create nil)
+                      erc-server-announced-name "foo.gnu.chat")
+                (set-process-query-on-exit-flag erc-server-process nil)
+                (erc-autojoin-after-ident 'FooNet "tester")
+                (should-not erc--autojoin-timer))))
+
+      (ert-info ("Join on NickServ hook; server")
+        (let ((erc-autojoin-channels-alist '(("\\.gnu\\.chat\\'" "#chan"))))
+          (funcall common))
+        (should (equal (pop calls) "JOIN #chan")))
+
+      (ert-info ("Join on NickServ hook; network")
+        (let ((erc-autojoin-channels-alist '((FooNet "#chan"))))
+          (funcall common))
+        (should (equal (pop calls) "JOIN #chan"))))))
+
+(defun erc-join-tests--autojoin-add--common (setup &optional fwd)
+  (let (calls
+        erc-autojoin-channels-alist
+        erc-kill-channel-hook erc-kill-server-hook erc-kill-buffer-hook)
+
+    (cl-letf (((symbol-function 'erc-handle-parsed-server-response)
+               (lambda (_p m) (push m calls))))
+
+      (ert-with-test-buffer (:name "foonet")
+        (erc-mode)
+        (setq erc-server-process
+              (start-process "true" (current-buffer) "true")
+              erc-server-current-nick "tester"
+              erc--isupport-params (make-hash-table)
+              erc-server-announced-name "foo.gnu.chat")
+        (puthash 'CHANTYPES '("&#") erc--isupport-params)
+        (funcall setup)
+        (set-process-query-on-exit-flag erc-server-process nil)
+        (should-not calls)
+
+        (ert-info ("Add #chan")
+          (erc-parse-server-response erc-server-process
+                                     (concat ":[email protected] JOIN #chan"
+                                             (and fwd " * :Tes Ter")))
+          (should calls)
+          (erc-autojoin-add erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist '((FooNet "#chan")))))
+
+        (ert-info ("More recently joined chans are prepended")
+          (erc-parse-server-response
+           erc-server-process ; with account username
+           (concat ":[email protected] JOIN #spam" (and fwd " tester :Tes Ter")))
+          (should calls)
+          (erc-autojoin-add erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '((FooNet "#spam" "#chan")))))
+
+        (ert-info ("Duplicates skipped")
+          (erc-parse-server-response erc-server-process
+                                     (concat ":[email protected] JOIN #chan"
+                                             (and fwd " * :Tes Ter")))
+          (should calls)
+          (erc-autojoin-add erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '((FooNet "#spam" "#chan")))))
+
+        (ert-info ("Server used for local channel")
+          (erc-parse-server-response erc-server-process
+                                     (concat ":[email protected] JOIN &local"
+                                             (and fwd " * :Tes Ter")))
+          (should calls)
+          (erc-autojoin-add erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '(("foo\\.gnu\\.chat" "&local")
+                           (FooNet "#spam" "#chan")))))))))
+
+(ert-deftest erc-autojoin-add--network ()
+  (erc-join-tests--autojoin-add--common
+   (lambda () (setq erc-network 'FooNet
+                    erc-networks--id (erc-networks--id-create nil)))))
+
+(ert-deftest erc-autojoin-add--network-extended-syntax ()
+  (erc-join-tests--autojoin-add--common
+   (lambda () (setq erc-network 'FooNet
+                    erc-networks--id (erc-networks--id-create nil)))
+   'forward-compatible))
+
+(ert-deftest erc-autojoin-add--network-id ()
+  (erc-join-tests--autojoin-add--common
+   (lambda () (setq erc-network 'invalid
+                    erc-networks--id (erc-networks--id-create 'FooNet)))))
+
+(ert-deftest erc-autojoin-add--server ()
+  (let (calls
+        erc-autojoin-channels-alist
+        erc-kill-channel-hook erc-kill-server-hook erc-kill-buffer-hook)
+
+    (cl-letf (((symbol-function 'erc-handle-parsed-server-response)
+               (lambda (_p m) (push m calls))))
+
+      (ert-info ("Network unavailable, announced name used")
+        (setq erc-autojoin-channels-alist nil)
+        (ert-with-test-buffer (:name "foonet")
+          (erc-mode)
+          (setq erc-server-process
+                (start-process "true" (current-buffer) "true")
+                erc-server-current-nick "tester"
+                erc-server-announced-name "foo.gnu.chat"
+                erc-networks--id (make-erc-networks--id)) ; assume too early
+          (set-process-query-on-exit-flag erc-server-process nil)
+          (should-not calls)
+          (erc-parse-server-response erc-server-process
+                                     ":[email protected] JOIN #chan")
+          (should calls)
+          (erc-autojoin-add erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '(("gnu.chat" "#chan")))))))))
+
+(defun erc-join-tests--autojoin-remove--common (setup)
+  (let (calls
+        erc-autojoin-channels-alist
+        erc-kill-channel-hook erc-kill-server-hook erc-kill-buffer-hook)
+
+    (cl-letf (((symbol-function 'erc-handle-parsed-server-response)
+               (lambda (_p m) (push m calls))))
+
+      (setq erc-autojoin-channels-alist ; mutated, so can't quote whole thing
+            (list '(FooNet "#spam" "##chan")
+                  '(BarNet "#bar" "##bar")
+                  '("foo\\.gnu\\.chat" "&local")))
+
+      (ert-with-test-buffer (:name "foonet")
+        (erc-mode)
+        (setq erc-server-process
+              (start-process "true" (current-buffer) "true")
+              erc-server-current-nick "tester"
+              erc--isupport-params (make-hash-table)
+              erc-server-announced-name "foo.gnu.chat")
+        (puthash 'CHANTYPES '("&#") erc--isupport-params)
+        (funcall setup)
+        (set-process-query-on-exit-flag erc-server-process nil)
+        (should-not calls)
+
+        (ert-info ("Remove #chan")
+          (erc-parse-server-response erc-server-process
+                                     ":[email protected] PART ##chan")
+          (should calls)
+          (erc-autojoin-remove erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '((FooNet "#spam")
+                           (BarNet "#bar" "##bar")
+                           ("foo\\.gnu\\.chat" "&local")))))
+
+        (ert-info ("Wrong network, nothing done")
+          (erc-parse-server-response erc-server-process
+                                     ":[email protected] PART #bar")
+          (should calls)
+          (erc-autojoin-remove erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '((FooNet "#spam")
+                           (BarNet "#bar" "##bar")
+                           ("foo\\.gnu\\.chat" "&local")))))
+
+        (ert-info ("Local channel keyed by server found")
+          (erc-parse-server-response erc-server-process
+                                     ":[email protected] PART &local")
+          (should calls)
+          (erc-autojoin-remove erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '((FooNet "#spam") (BarNet "#bar" "##bar")))))))))
+
+(ert-deftest erc-autojoin-remove--network ()
+  (erc-join-tests--autojoin-remove--common
+   (lambda () (setq erc-network 'FooNet
+                    erc-networks--id (erc-networks--id-create nil)))))
+
+(ert-deftest erc-autojoin-remove--network-id ()
+  (erc-join-tests--autojoin-remove--common
+   (lambda () (setq erc-network 'fake-a-roo
+                    erc-networks--id (erc-networks--id-create 'FooNet)))))
+
+(ert-deftest erc-autojoin-remove--server ()
+  (let (calls
+        erc-autojoin-channels-alist
+        erc-kill-channel-hook erc-kill-server-hook erc-kill-buffer-hook)
+
+    (cl-letf (((symbol-function 'erc-handle-parsed-server-response)
+               (lambda (_p m) (push m calls))))
+
+      (setq erc-autojoin-channels-alist (list '("gnu.chat" "#spam" "##chan")
+                                              '("fsf.chat" "#bar" "##bar")))
+
+      (ert-with-test-buffer (:name "foonet")
+        (erc-mode)
+        (setq erc-server-process
+              (start-process "true" (current-buffer) "true")
+              erc-server-current-nick "tester"
+              erc-server-announced-name "foo.gnu.chat"
+              ;; Assume special case w/o known network
+              erc-networks--id (make-erc-networks--id))
+        (set-process-query-on-exit-flag erc-server-process nil)
+        (should-not calls)
+
+        (ert-info ("Announced name matched, #chan removed")
+          (erc-parse-server-response erc-server-process
+                                     ":[email protected] PART ##chan")
+          (should calls)
+          (erc-autojoin-remove erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '(("gnu.chat" "#spam")
+                           ("fsf.chat" "#bar" "##bar")))))
+
+        (ert-info ("Wrong announced name, nothing done")
+          (erc-parse-server-response erc-server-process
+                                     ":[email protected] PART #bar")
+          (should calls)
+          (erc-autojoin-remove erc-server-process (pop calls))
+          (should (equal erc-autojoin-channels-alist
+                         '(("gnu.chat" "#spam")
+                           ("fsf.chat" "#bar" "##bar")))))))))
+
+;;; erc-join-tests.el ends here
-- 
2.36.1
0023-SQUASH-ME-Add-ERC-test-scenarios-for-identity-aware-.patch (text/x-patch, 34.9 KB)
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Thu, 30 Sep 2021 03:28:54 -0700
Subject: [PATCH 23/35] SQUASH-ME: Add ERC test scenarios for identity-aware
 autojoin

XXX this commit should not stand alone. It should be squashed or
fixup'd into "Favor network IDs and networks in erc-join.el"
---
 .../erc-scenarios-base-reconnect.el           |   2 +-
 .../erc-scenarios-join-netid-newcmd-id.el     |  50 ++++++++
 .../erc-scenarios-join-netid-newcmd.el        |  37 ++++++
 .../erc-scenarios-join-netid-recon-id.el      |  46 ++++++++
 .../erc-scenarios-join-netid-recon.el         |  36 ++++++
 .../resources/erc-scenarios-common.el         | 110 ++++++++++++++++++
 .../resources/join/legacy/foonet.eld          |  38 ++++++
 .../resources/join/network-id/barnet.eld      |  43 +++++++
 .../join/network-id/foonet-again.eld          |  46 ++++++++
 .../resources/join/network-id/foonet.eld      |  39 +++++++
 .../resources/join/reconnect/foonet-again.eld |  45 +++++++
 .../resources/join/reconnect/foonet.eld       |  45 +++++++
 12 files changed, 536 insertions(+), 1 deletion(-)
 create mode 100644 test/lisp/erc/erc-scenarios/erc-scenarios-join-netid-newcmd-id.el
 create mode 100644 test/lisp/erc/erc-scenarios/erc-scenarios-join-netid-newcmd.el
 create mode 100644 test/lisp/erc/erc-scenarios/erc-scenarios-join-netid-recon-id.el
 create mode 100644 test/lisp/erc/erc-scenarios/erc-scenarios-join-netid-recon.el
 create mode 100644 test/lisp/erc/erc-scenarios/resources/join/legacy/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/join/network-id/barnet.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/join/network-id/foonet-again.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/join/network-id/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/join/reconnect/foonet-again.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/join/reconnect/foonet.eld

diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-base-reconnect.el b/test/lisp/erc/erc-scenarios/erc-scenarios-base-reconnect.el
index 3028d888cf..06143a7a0c 100644
--- a/test/lisp/erc/erc-scenarios/erc-scenarios-base-reconnect.el
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-base-reconnect.el
@@ -95,7 +95,7 @@ erc-scenarios-common--base-reconnect-options
       (with-current-buffer erc-server-buffer
         (funcall expect 10 "Connection failed!  Re-establishing")))
 
-    (should (equal erc-autojoin-channels-alist '(("foonet.org" "#chan"))))
+    (should (equal erc-autojoin-channels-alist '((FooNet "#chan"))))
 
     (funcall test)
 
diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-join-netid-newcmd-id.el b/test/lisp/erc/erc-scenarios/erc-scenarios-join-netid-newcmd-id.el
new file mode 100644
index 0000000000..e2e437321d
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-join-netid-newcmd-id.el
@@ -0,0 +1,50 @@
+;;; erc-scenarios-join-netid-newcmd-id.el --- join netid newcmd scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+;;
+;; This file is part of GNU Emacs.
+;;
+;; This program is free software: you can redistribute it and/or
+;; modify it under the terms of the GNU General Public License as
+;; published by the Free Software Foundation, either version 3 of the
+;; License, or (at your option) any later version.
+;;
+;; This program is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see
+;; <https://www.gnu.org/licenses/>.
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(ert-deftest erc-scenarios-join-netid--newcmd-id ()
+  :tags '(:expensive-test)
+  (let ((connect (lambda ()
+                   (erc :server "127.0.0.1"
+                        :port (with-current-buffer "oofnet"
+                                (process-contact erc-server-process :service))
+                        :nick "tester"
+                        :password "foonet:changeme"
+                        :full-name "tester"
+                        :id 'oofnet))))
+    (erc-scenarios-common--join-network-id connect 'oofnet nil)))
+
+(ert-deftest erc-scenarios-join-netid--newcmd-ids ()
+  :tags '(:expensive-test)
+  (let ((connect (lambda ()
+                   (erc :server "127.0.0.1"
+                        :port (with-current-buffer "oofnet"
+                                (process-contact erc-server-process :service))
+                        :nick "tester"
+                        :password "foonet:changeme"
+                        :full-name "tester"
+                        :id 'oofnet))))
+    (erc-scenarios-common--join-network-id connect 'oofnet 'rabnet)))
+
+;;; erc-scenarios-join-netid-newcmd-id.el ends here
diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-join-netid-newcmd.el b/test/lisp/erc/erc-scenarios/erc-scenarios-join-netid-newcmd.el
new file mode 100644
index 0000000000..1a541a46b3
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-join-netid-newcmd.el
@@ -0,0 +1,37 @@
+;;; erc-scenarios-join-netid-newcmd.el --- join netid newcmd scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+;;
+;; This file is part of GNU Emacs.
+;;
+;; This program is free software: you can redistribute it and/or
+;; modify it under the terms of the GNU General Public License as
+;; published by the Free Software Foundation, either version 3 of the
+;; License, or (at your option) any later version.
+;;
+;; This program is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see
+;; <https://www.gnu.org/licenses/>.
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(ert-deftest erc-scenarios-join-netid--newcmd ()
+  :tags '(:expensive-test)
+  (let ((connect (lambda ()
+                   (erc :server "127.0.0.1"
+                        :port (with-current-buffer "foonet"
+                                (process-contact erc-server-process :service))
+                        :nick "tester"
+                        :password "foonet:changeme"
+                        :full-name "tester"))))
+    (erc-scenarios-common--join-network-id connect nil nil)))
+
+;;; erc-scenarios-join-netid-newcmd.el ends here
diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-join-netid-recon-id.el b/test/lisp/erc/erc-scenarios/erc-scenarios-join-netid-recon-id.el
new file mode 100644
index 0000000000..92bdd643de
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-join-netid-recon-id.el
@@ -0,0 +1,46 @@
+;;; erc-scenarios-join-netid-recon-id.el --- join-netid-recon scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+;;
+;; This file is part of GNU Emacs.
+;;
+;; This program is free software: you can redistribute it and/or
+;; modify it under the terms of the GNU General Public License as
+;; published by the Free Software Foundation, either version 3 of the
+;; License, or (at your option) any later version.
+;;
+;; This program is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see
+;; <https://www.gnu.org/licenses/>.
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(ert-deftest erc-scenarios-join-netid--recon-id ()
+  :tags '(:expensive-test)
+  (let ((connect (lambda ()
+                   (with-current-buffer "oofnet"
+                     (erc-cmd-RECONNECT)
+                     (should (eq (current-buffer)
+                                 (process-buffer erc-server-process)))
+                     (current-buffer)))))
+    (erc-scenarios-common--join-network-id connect 'oofnet nil)))
+
+(ert-deftest erc-scenarios-join-netid--recon-ids ()
+  :tags '(:expensive-test)
+  (let ((connect (lambda ()
+                   (with-current-buffer "oofnet"
+                     (erc-cmd-RECONNECT)
+                     (should (eq (current-buffer)
+                                 (process-buffer erc-server-process)))
+                     (current-buffer)))))
+    (erc-scenarios-common--join-network-id connect 'oofnet 'rabnet)))
+
+;;; erc-scenarios-join-netid-recon-id.el ends here
diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-join-netid-recon.el b/test/lisp/erc/erc-scenarios/erc-scenarios-join-netid-recon.el
new file mode 100644
index 0000000000..cbdba07e25
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-join-netid-recon.el
@@ -0,0 +1,36 @@
+;;; erc-scenarios-join-netid-recon.el --- join-netid-recon scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+;;
+;; This file is part of GNU Emacs.
+;;
+;; This program is free software: you can redistribute it and/or
+;; modify it under the terms of the GNU General Public License as
+;; published by the Free Software Foundation, either version 3 of the
+;; License, or (at your option) any later version.
+;;
+;; This program is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see
+;; <https://www.gnu.org/licenses/>.
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(ert-deftest erc-scenarios-join-netid--recon ()
+  :tags '(:expensive-test)
+  (let ((connect (lambda ()
+                   (with-current-buffer "foonet"
+                     (erc-cmd-RECONNECT)
+                     (should (eq (current-buffer)
+                                 (process-buffer erc-server-process)))
+                     (current-buffer)))))
+    (erc-scenarios-common--join-network-id connect nil nil)))
+
+;;; erc-scenarios-join-netid-recon.el ends here
diff --git a/test/lisp/erc/erc-scenarios/resources/erc-scenarios-common.el b/test/lisp/erc/erc-scenarios/resources/erc-scenarios-common.el
index 1d8fd31dd4..3973bfbf35 100644
--- a/test/lisp/erc/erc-scenarios/resources/erc-scenarios-common.el
+++ b/test/lisp/erc/erc-scenarios/resources/erc-scenarios-common.el
@@ -407,6 +407,116 @@ erc-scenarios-common--upstream-reconnect
 
     (funcall test)))
 
+;; XXX this is okay, but we also need to check that target buffers are
+;; already associated with a new process *before* a JOIN is sent by a
+;; server's playback burst.  This doesn't do that.
+;;
+;; This *does* check that superfluous JOINs sent by the autojoin
+;; module are harmless when they're not acked (superfluous because the
+;; bouncer/server intitates the JOIN).
+
+(defun erc-scenarios-common--join-network-id (foo-reconnector foo-id bar-id)
+  "Ensure channels rejoined by erc-join.el DTRT.
+Originally from scenario clash-of-chans/autojoin as described in
+Bug#48598: 28.0.50; buffer-naming collisions involving bouncers in ERC."
+  (erc-scenarios-common-with-cleanup
+      ((chan-buf-foo (format "#chan@%s" (or foo-id "foonet")))
+       (chan-buf-bar (format "#chan@%s" (or bar-id "barnet")))
+       (erc-scenarios-common-dialog "join/network-id")
+       (erc-d-t-cleanup-sleep-secs 1)
+       (erc-server-flood-penalty 0.5)
+       (dumb-server (erc-d-run "localhost" t 'foonet 'barnet 'foonet-again))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       erc-server-buffer-foo erc-server-process-foo
+       erc-server-buffer-bar erc-server-process-bar)
+
+    (should (memq 'autojoin erc-modules))
+
+    (ert-info ("Connect to foonet")
+      (with-current-buffer
+          (setq erc-server-buffer-foo (erc :server "127.0.0.1"
+                                           :port port
+                                           :nick "tester"
+                                           :password "foonet:changeme"
+                                           :full-name "tester"
+                                           :id foo-id))
+        (setq erc-server-process-foo erc-server-process)
+        (erc-scenarios-common-assert-initial-buf-name foo-id port)
+        (erc-d-t-wait-for 5 (eq (erc-network) 'foonet))
+        (funcall expect 5 "foonet")))
+
+    (ert-info ("Join #chan, find sentinel, quit")
+      (with-current-buffer erc-server-buffer-foo (erc-cmd-JOIN "#chan"))
+      (with-current-buffer (erc-d-t-wait-for 5 (get-buffer "#chan"))
+        (funcall expect 5 "vile thing")
+        (erc-cmd-QUIT "")))
+
+    (erc-d-t-wait-for 2 "Foonet connection deceased"
+      (not (erc-server-process-alive erc-server-buffer-foo)))
+
+    (should (equal erc-autojoin-channels-alist
+                   (if foo-id '((oofnet "#chan")) '((foonet "#chan")))))
+
+    (ert-info ("Connect to barnet")
+      (with-current-buffer
+          (setq erc-server-buffer-bar (erc :server "127.0.0.1"
+                                           :port port
+                                           :nick "tester"
+                                           :password "barnet:changeme"
+                                           :full-name "tester"
+                                           :id bar-id))
+        (setq erc-server-process-bar erc-server-process)
+        (erc-d-t-wait-for 5 (eq erc-network 'barnet))
+        (should (string= (buffer-name) (if bar-id "rabnet" "barnet")))))
+
+    (ert-info ("Server buffers are unique, no stray IP-based names")
+      (should-not (eq erc-server-buffer-foo erc-server-buffer-bar))
+      (should-not (erc-scenarios-common-buflist "127.0.0.1")))
+
+    (ert-info ("Only one #chan buffer exists")
+      (should (equal (list (get-buffer "#chan"))
+                     (erc-scenarios-common-buflist "#chan"))))
+
+    (ert-info ("#chan is not auto-joined")
+      (with-current-buffer "#chan"
+        (erc-d-t-absent-for 0.1 "<joe>")
+        (should-not (process-live-p erc-server-process))
+        (erc-d-t-ensure-for 0.1 "server buffer remains foonet"
+          (eq erc-server-process erc-server-process-foo))))
+
+    (with-current-buffer erc-server-buffer-bar
+      (erc-cmd-JOIN "#chan")
+      (erc-d-t-wait-for 3 (get-buffer chan-buf-foo))
+      (erc-d-t-wait-for 3 (get-buffer chan-buf-bar))
+      (with-current-buffer chan-buf-bar
+        (erc-d-t-wait-for 3 (eq erc-server-process erc-server-process-bar))
+        (funcall expect 5 "marry her instantly")))
+
+    (ert-info ("Reconnect to foonet")
+      (with-current-buffer (setq erc-server-buffer-foo
+                                 (funcall foo-reconnector))
+        (should (member (if foo-id '(oofnet "#chan") '(foonet "#chan"))
+                        erc-autojoin-channels-alist))
+        (erc-d-t-wait-for 3 (erc-server-process-alive))
+        (setq erc-server-process-foo erc-server-process)
+        (erc-d-t-wait-for 2 (eq erc-network 'foonet))
+        (should (string= (buffer-name) (if foo-id "oofnet" "foonet")))
+        (funcall expect 5 "foonet")))
+
+    (ert-info ("#chan@foonet is clean, no cross-contamination")
+      (with-current-buffer chan-buf-foo
+        (erc-d-t-wait-for 3 (eq erc-server-process erc-server-process-foo))
+        (funcall expect 3 "<bob>")
+        (erc-d-t-absent-for 0.1 "<joe>")
+        (while (accept-process-output erc-server-process-foo))
+        (funcall expect 3 "not given me")))
+
+    (ert-info ("All #chan@barnet output received")
+      (with-current-buffer chan-buf-bar
+        (while (accept-process-output erc-server-process-bar))
+        (funcall expect 3 "hath an uncle here")))))
+
 (provide 'erc-scenarios-common)
 
 ;;; erc-scenarios-common.el ends here
diff --git a/test/lisp/erc/erc-scenarios/resources/join/legacy/foonet.eld b/test/lisp/erc/erc-scenarios/resources/join/legacy/foonet.eld
new file mode 100644
index 0000000000..344ba7c1da
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/join/legacy/foonet.eld
@@ -0,0 +1,38 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.foonet.org 003 tester :This server was created Tue, 04 May 2021 05:06:18 UTC")
+ (0 ":irc.foonet.org 004 tester irc.foonet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.foonet.org 005 tester AWAYLEN=390 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# ELIST=U EXCEPTS EXTBAN=,m FORWARD=f INVEX KICKLEN=390 :are supported by this server")
+ (0 ":irc.foonet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=FooNet NICKLEN=32 PREFIX=(qaohv)~&@%+ STATUSMSG=~&@%+ TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 TOPICLEN=390 UTF8MAPPING=rfc8265 UTF8ONLY WHOX :are supported by this server")
+ (0 ":irc.foonet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.foonet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 3.2 "MODE tester +i")
+ (0 ":irc.foonet.org 221 tester +i")
+ (0 ":irc.foonet.org NOTICE tester :This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect."))
+
+((join 6 "JOIN #chan")
+ (0 ":[email protected] JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((mode 5 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620104779")
+ (0.1 ":[email protected] PRIVMSG #chan :tester, welcome!")
+ (0.1 ":[email protected] PRIVMSG #chan :tester, welcome!")
+ (0.1 ":[email protected] PRIVMSG #chan :alice: But, as it seems, did violence on herself.")
+ (0.1 ":[email protected] PRIVMSG #chan :bob: Well, this is the forest of Arden.")
+ (0.1 ":[email protected] PRIVMSG #chan :bob: And will you, being a man of your breeding, be married under a bush, like a beggar ? Get you to church, and have a good priest that can tell you what marriage is: this fellow will but join you together as they join wainscot; then one of you will prove a shrunk panel, and like green timber, warp, warp.")
+ (0.1 ":[email protected] PRIVMSG #chan :alice: Live, and be prosperous; and farewell, good fellow."))
diff --git a/test/lisp/erc/erc-scenarios/resources/join/network-id/barnet.eld b/test/lisp/erc/erc-scenarios/resources/join/network-id/barnet.eld
new file mode 100644
index 0000000000..e33dd6be29
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/join/network-id/barnet.eld
@@ -0,0 +1,43 @@
+;; -*- mode: lisp-data; -*-
+((pass 2 "PASS :barnet:changeme"))
+((nick 2 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Mon, 10 May 2021 00:58:22 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.org 005 tester AWAYLEN=390 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# ELIST=U EXCEPTS EXTBAN=,m FORWARD=f INVEX KICKLEN=390 :are supported by this server")
+ (0 ":irc.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet NICKLEN=32 PREFIX=(qaohv)~&@%+ STATUSMSG=~&@%+ TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 TOPICLEN=390 UTF8MAPPING=rfc8265 UTF8ONLY WHOX :are supported by this server")
+ (0 ":irc.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 12 "MODE tester +i"))
+;; No mode answer
+
+((join 2 "JOIN #chan")
+ (0 ":[email protected] JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :@joe mike tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":[email protected] PRIVMSG #chan :tester, welcome!")
+ (0 ":[email protected] PRIVMSG #chan :tester, welcome!"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620608304")
+ ;; Wait for foonet's buffer playback
+ (0.1 ":[email protected] PRIVMSG #chan :joe: Go take her hence, and marry her instantly.")
+ (0.1 ":[email protected] PRIVMSG #chan :mike: Of all the four, or the three, or the two, or one of the four.")
+ (0.1 ":[email protected] PRIVMSG #chan :joe: And gives the crutch the cradle's infancy.")
+ (0.1 ":[email protected] PRIVMSG #chan :mike: Such is the simplicity of man to hearken after the flesh.")
+ (0.05 ":[email protected] PRIVMSG #chan :joe: The leaf to read them. Let us toward the king.")
+ (0.05 ":[email protected] PRIVMSG #chan :mike: Many can brook the weather that love not the wind.")
+ (0.05 ":[email protected] PRIVMSG #chan :joe: And now, dear maid, be you as free to us.")
+ (0.00 ":[email protected] PRIVMSG #chan :mike: He hath an uncle here in Messina will be very much glad of it."))
+
+((linger 3.5 LINGER))
diff --git a/test/lisp/erc/erc-scenarios/resources/join/network-id/foonet-again.eld b/test/lisp/erc/erc-scenarios/resources/join/network-id/foonet-again.eld
new file mode 100644
index 0000000000..b230eff27c
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/join/network-id/foonet-again.eld
@@ -0,0 +1,46 @@
+;; -*- mode: lisp-data; -*-
+((pass-redux 10 "PASS :foonet:changeme"))
+((nick-redux 1 "NICK tester"))
+
+((user-redux 1 "USER user 0 * :tester")
+ (0 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.foonet.org 003 tester :This server was created Mon, 10 May 2021 00:58:22 UTC")
+ (0 ":irc.foonet.org 004 tester irc.foonet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.foonet.org 005 tester AWAYLEN=390 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# ELIST=U EXCEPTS EXTBAN=,m FORWARD=f INVEX KICKLEN=390 :are supported by this server")
+ (0 ":irc.foonet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=foonet NICKLEN=32 PREFIX=(qaohv)~&@%+ STATUSMSG=~&@%+ TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 TOPICLEN=390 UTF8MAPPING=rfc8265 UTF8ONLY WHOX :are supported by this server")
+ (0 ":irc.foonet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.foonet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 10.2 "MODE tester +i")
+ ;; No mode answer ^
+
+ ;; History
+ (0 ":[email protected] JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :@alice bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***[email protected] PRIVMSG #chan :Buffer Playback...")
+ (0 ":[email protected] PRIVMSG #chan :[02:43:23] alice: And soar with them above a common bound.")
+ (0 ":[email protected] PRIVMSG #chan :[02:43:27] bob: And be aveng'd on cursed Tamora.")
+ (0 ":[email protected] PRIVMSG #chan :[02:43:29] alice: He did love her, sir, as a gentleman loves a woman.")
+ (0 ":***[email protected] PRIVMSG #chan :Playback Complete."))
+
+;; As a server, we ignore useless join sent by autojoin module
+((~join 10 "JOIN #chan"))
+
+((mode-redux 10 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620608304")
+ (0.1 ":[email protected] PRIVMSG #chan :bob: Ay, madam, with the swiftest wing of speed.")
+ (0.1 ":[email protected] PRIVMSG #chan :alice: Five times in that ere once in our five wits.")
+ (0.1 ":[email protected] PRIVMSG #chan :bob: And bid him come to take his last farewell.")
+ (0.1 ":[email protected] PRIVMSG #chan :alice: But we are spirits of another sort.")
+ (0.1 ":[email protected] PRIVMSG #chan :bob: It was not given me, nor I did not buy it."))
+
+((linger 6 LINGER))
diff --git a/test/lisp/erc/erc-scenarios/resources/join/network-id/foonet.eld b/test/lisp/erc/erc-scenarios/resources/join/network-id/foonet.eld
new file mode 100644
index 0000000000..eb44e58a59
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/join/network-id/foonet.eld
@@ -0,0 +1,39 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((nick 1 "NICK tester"))
+
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.foonet.org 003 tester :This server was created Mon, 10 May 2021 00:58:22 UTC")
+ (0 ":irc.foonet.org 004 tester irc.foonet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.foonet.org 005 tester AWAYLEN=390 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# ELIST=U EXCEPTS EXTBAN=,m FORWARD=f INVEX KICKLEN=390 :are supported by this server")
+ (0 ":irc.foonet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=foonet NICKLEN=32 PREFIX=(qaohv)~&@%+ STATUSMSG=~&@%+ TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 TOPICLEN=390 UTF8MAPPING=rfc8265 UTF8ONLY WHOX :are supported by this server")
+ (0 ":irc.foonet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.foonet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 10.2 "MODE tester +i"))
+;; No mode answer ^
+
+((join 3 "JOIN #chan")
+ (0 ":[email protected] JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :@alice bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":[email protected] PRIVMSG #chan :tester, welcome!")
+ (0 ":[email protected] PRIVMSG #chan :tester, welcome!"))
+
+((mode 3 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620608304")
+ (0.1 ":[email protected] PRIVMSG #chan :alice: Pray you, sir, deliver me this paper.")
+ (0.1 ":[email protected] PRIVMSG #chan :bob: Wake when some vile thing is near."))
+
+((quit 3 "QUIT :\2ERC\2"))
+
+((drop 0 DROP))
diff --git a/test/lisp/erc/erc-scenarios/resources/join/reconnect/foonet-again.eld b/test/lisp/erc/erc-scenarios/resources/join/reconnect/foonet-again.eld
new file mode 100644
index 0000000000..f1fcc439cc
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/join/reconnect/foonet-again.eld
@@ -0,0 +1,45 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.foonet.org 003 tester :This server was created Tue, 04 May 2021 05:06:18 UTC")
+ (0 ":irc.foonet.org 004 tester irc.foonet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.foonet.org 005 tester AWAYLEN=390 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# ELIST=U EXCEPTS EXTBAN=,m FORWARD=f INVEX KICKLEN=390 :are supported by this server")
+ (0 ":irc.foonet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=FooNet NICKLEN=32 PREFIX=(qaohv)~&@%+ STATUSMSG=~&@%+ TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 TOPICLEN=390 UTF8MAPPING=rfc8265 UTF8ONLY WHOX :are supported by this server")
+ (0 ":irc.foonet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.foonet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 3.2 "MODE tester +i")
+ (0 ":irc.foonet.org 221 tester +i")
+ (0 ":irc.foonet.org NOTICE tester :This server is still in debug mode."))
+
+((~join-chan 12 "JOIN #chan")
+ (0 ":[email protected] JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list"))
+
+((~join-spam 12 "JOIN #spam")
+ (0 ":[email protected] JOIN #spam")
+ (0 ":irc.foonet.org 353 tester = #spam :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #spam :End of NAMES list"))
+
+((~mode-chan 4 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620104779")
+ (0.1 ":[email protected] PRIVMSG #chan :alice: But, as it seems, did violence on herself.")
+ (0.1 ":[email protected] PRIVMSG #chan :bob: Well, this is the forest of Arden."))
+
+((mode-spam 4 "MODE #spam")
+ (0 ":irc.foonet.org 324 tester #spam +nt")
+ (0 ":irc.foonet.org 329 tester #spam 1620104779")
+ (0.1 ":[email protected] PRIVMSG #spam :alice: Signior Iachimo will not from it. Pray, let us follow 'em.")
+ (0.1 ":[email protected] PRIVMSG #spam :bob: Our queen and all her elves come here anon."))
diff --git a/test/lisp/erc/erc-scenarios/resources/join/reconnect/foonet.eld b/test/lisp/erc/erc-scenarios/resources/join/reconnect/foonet.eld
new file mode 100644
index 0000000000..efb269f5ae
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/join/reconnect/foonet.eld
@@ -0,0 +1,45 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.foonet.org 003 tester :This server was created Tue, 04 May 2021 05:06:18 UTC")
+ (0 ":irc.foonet.org 004 tester irc.foonet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.foonet.org 005 tester AWAYLEN=390 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# ELIST=U EXCEPTS EXTBAN=,m FORWARD=f INVEX KICKLEN=390 :are supported by this server")
+ (0 ":irc.foonet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=FooNet NICKLEN=32 PREFIX=(qaohv)~&@%+ STATUSMSG=~&@%+ TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 TOPICLEN=390 UTF8MAPPING=rfc8265 UTF8ONLY WHOX :are supported by this server")
+ (0 ":irc.foonet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.foonet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 3.2 "MODE tester +i")
+ (0 ":irc.foonet.org 221 tester +i")
+ (0 ":irc.foonet.org NOTICE tester :This server is in debug mode.")
+
+ (0 ":[email protected] JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list")
+
+ (0 ":[email protected] JOIN #spam")
+ (0 ":irc.foonet.org 353 tester = #spam :alice tester @bob")
+ (0 ":irc.foonet.org 366 tester #spam :End of NAMES list"))
+
+((mode-chan 4 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620104779")
+ (0.1 ":[email protected] PRIVMSG #chan :tester, welcome!")
+ (0.1 ":[email protected] PRIVMSG #chan :tester, welcome!"))
+
+((mode-spam 4 "MODE #spam")
+ (0 ":irc.foonet.org 324 tester #spam +nt")
+ (0 ":irc.foonet.org 329 tester #spam 1620104779")
+ (0.1 ":[email protected] PRIVMSG #spam :tester, welcome!")
+ (0.1 ":[email protected] PRIVMSG #spam :tester, welcome!"))
+
+((drop 0 DROP))
-- 
2.36.1
0024-Standardize-auth-source-queries-in-ERC.patch (text/x-patch, 43.6 KB)
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Mon, 16 Aug 2021 04:38:18 -0700
Subject: [PATCH 24/35] Standardize auth-source queries in ERC

* lisp/erc/erc.el (erc-password): Deprecate variable only used by
`erc-select-read-args'.  Server passwords are primarily used as
surrogates for other forms of authentication.  Such use is common but
nonstandard and often discouraged in favor of the de facto standard,
SASL.  Folks in the habit of invoking `erc(-tls)' interactively should
be encouraged to use auth-source instead.
(erc-select-read-args): Before this change, `erc-select-read-args'
offered to use the value of a non-nil `erc-password' as the :password
argument for `erc' and `erc-tls', referring to it as the "default"
password.  And when `erc-prompt-for-password' was nil and
`erc-password' wasn't, the latter was passed along unconditionally.
This only further complicated an already confusing situation for new
users, who in most cases shouldn't be worried about sending a PASS
command at all.  Until SASL arrives, they should provide server
passwords manually or learn to use auth-source.
(erc-auth-source-server-function, erc-auth-source-join-function): New
user options for retrieving a password externally, ostensibly by
calling `auth-source-search'.
(erc--auth-source-determine-params-defaults): New helper for
`erc--auth-source-search' with potential for exporting publicly in the
future.  Favors :host and :port fields above others.  Prioritizes
network IDs over announced servers and dialed endpoints.
(erc--auth-source-determine-params-merge): Add new function for
merging contextual and default parameters.  This is another contender
for possible exporting.
(erc--auth-source-search): New function for consulting auth-source and
sorting the result as filtered and prioritized by the previously
mentioned helpers.
(erc-auth-source-search): New function to serve as default
value for auth-source query-function options.
(erc-server-join-channel): Use user option for consulting auth-source
facility.  Also accept nil for first argument (instead of server).
(erc-cmd-JOIN): Use above-mentioned facilities when joining new
channel.  Omit server when calling `erc-server-join-channel'.  Don't
filter target buffers twice.  Don't call `switch-to-buffer', which
would create phantom buffers with names like target/server that were
never used.  IOW, only switch to existing target buffers.
(erc--compute-server-password): Add new helper function for
determining password.
(erc-open, erc-determine-parameters): Move password figuring from the
first to the latter.

* lisp/erc/erc-services.el
(erc-auth-source-services-function): Add new option for consulting
auth-source in a NickServ context.
(erc-nickserv-get-password): Pass network-context ID, when looking up
password in `erc-nickserv-passwords' and when formatting prompt for
user input.
(erc-nickserv-passwords): Add comment to custom option definition type
tag.

* test/lisp/erc/erc-services-tests.el: Add new test file for above
changes.  For now, stash auth-source-related tests here until a
suitable home can be found.

* lisp/erc/erc-join.el (erc-autojoin--join): Don't pass session-like
entity from `erc-autojoin-channels-alist' match to
`erc-server-join-channel'.  Allow that function to decide for itself
which host to look up if necessary.
---
 lisp/erc/erc-join.el                |   2 +-
 lisp/erc/erc-services.el            |  53 +--
 lisp/erc/erc.el                     | 197 +++++++---
 test/lisp/erc/erc-services-tests.el | 574 ++++++++++++++++++++++++++++
 4 files changed, 754 insertions(+), 72 deletions(-)
 create mode 100644 test/lisp/erc/erc-services-tests.el

diff --git a/lisp/erc/erc-join.el b/lisp/erc/erc-join.el
index d4edca236d..b4044548e8 100644
--- a/lisp/erc/erc-join.el
+++ b/lisp/erc/erc-join.el
@@ -145,7 +145,7 @@ erc-autojoin--join
         (let ((buf (erc-get-buffer chan erc-server-process)))
           (unless (and buf (with-current-buffer buf
                              (erc--current-buffer-joined-p)))
-            (erc-server-join-channel match chan)))))))
+            (erc-server-join-channel nil chan)))))))
 
 (defun erc-autojoin-after-ident (_network _nick)
   "Autojoin channels in `erc-autojoin-channels-alist'.
diff --git a/lisp/erc/erc-services.el b/lisp/erc/erc-services.el
index cc5d5701e4..c43fac2f0a 100644
--- a/lisp/erc/erc-services.el
+++ b/lisp/erc/erc-services.el
@@ -174,6 +174,18 @@ erc-use-auth-source-for-nickserv-password
   :version "28.1"
   :type 'boolean)
 
+(defcustom erc-auth-source-services-function #'erc-auth-source-search
+  "Function to retrieve NickServ password from auth-source.
+Called with a subset of keyword parameters known to `auth-source-search'
+and relevant to authenticating to nickname services.  In return, ERC
+expects a string to send as the password, or nil, to fall through to the
+next method, such as prompting.  See info node `(erc) Connecting' for
+details."
+  :package-version '(ERC . "5.4.1") ; FIXME update when publishing to ELPA
+  :type '(choice (const erc-auth-source-search)
+                 (const nil)
+                 function))
+
 (defcustom erc-nickserv-passwords nil
   "Passwords used when identifying to NickServ automatically.
 `erc-prompt-for-nickserv-password' must be nil for these
@@ -202,7 +214,7 @@ erc-nickserv-passwords
 			(const QuakeNet)
 			(const Rizon)
 			(const SlashNET)
-			(symbol :tag "Network name"))
+                        (symbol :tag "Network name or session ID"))
 		(repeat :tag "Nickname and password"
 			(cons :tag "Identity"
 			      (string :tag "Nick")
@@ -431,31 +443,20 @@ erc-nickserv-get-password
 lookups stops and this function returns it (or returns nil if it
 is empty).  Otherwise, no corresponding password was found, and
 it returns nil."
-  (let (network server port)
-    ;; Fill in local vars, switching to the server buffer once only
-    (erc-with-server-buffer
-     (setq network erc-network
-           server erc-session-server
-           port erc-session-port))
-    (let ((ret
-           (or
-            (when erc-nickserv-passwords
-              (cdr (assoc nick
-                          (cl-second (assoc network
-                                            erc-nickserv-passwords)))))
-            (when erc-use-auth-source-for-nickserv-password
-              (auth-source-pick-first-password
-               :require '(:secret)
-               :host server
-               ;; Ensure a string for :port
-               :port (format "%s" port)
-               :user nick))
-            (when erc-prompt-for-nickserv-password
-              (read-passwd
-               (format "NickServ password for %s on %s (RET to cancel): "
-                       nick network))))))
-      (when (and ret (not (string= ret "")))
-        ret))))
+  (when-let*
+      ((nid (erc-networks--id-symbol erc-networks--id))
+       (ret (or (when erc-nickserv-passwords
+                  (assoc-default nick
+                                 (cadr (assq nid erc-nickserv-passwords))))
+                (when (and erc-use-auth-source-for-nickserv-password
+                           erc-auth-source-services-function)
+                  (funcall erc-auth-source-services-function :user nick))
+                (when erc-prompt-for-nickserv-password
+                  (read-passwd
+                   (format "NickServ password for %s on %s (RET to cancel): "
+                           nick nid)))))
+       ((not (string-empty-p ret))))
+    ret))
 
 (defvar erc-auto-discard-away)
 
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 463c497844..c2b7dacf84 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -228,9 +228,14 @@ erc-rename-buffers
                         "old behavior when t now permanent" "29.1")
 
 (defvar erc-password nil
-  "Password to use when authenticating to an IRC server.
-It is not strictly necessary to provide this, since ERC will
-prompt you for it.")
+  "Password to use when authenticating to an IRC server interactively.
+
+This variable only exists for legacy reasons.  It's not customizable and
+is limited to a single server password.  Users looking for similar
+functionality should consider auth-source instead.  See info
+node `(auth) Top' and info node `(erc) Connecting'.")
+
+(make-obsolete-variable 'erc-password "use auth-source instead" "29.1")
 
 (defcustom erc-user-mode "+i"
   ;; +i "Invisible".  Hides user from global /who and /names.
@@ -241,7 +246,7 @@ erc-user-mode
 
 
 (defcustom erc-prompt-for-password t
-  "Asks before using the default password, or whether to enter a new one."
+  "Ask for a server password when invoking `erc-tls' interactively."
   :group 'erc
   :type 'boolean)
 
@@ -2209,15 +2214,6 @@ erc-open
     (setq erc-logged-in nil)
     ;; The local copy of `erc-nick' - the list of nicks to choose
     (setq erc-default-nicks (if (consp erc-nick) erc-nick (list erc-nick)))
-    ;; password stuff
-    (setq erc-session-password
-          (or passwd
-              (auth-source-pick-first-password
-               :host server
-               :user nick
-               ;; secrets.el wouldn’t accept a number
-               :port (if (numberp port) (number-to-string port) port)
-               :require '(:secret))))
     ;; client certificate (only useful if connecting over TLS)
     (setq erc-session-client-certificate client-certificate)
     (setq erc-networks--id (if connect
@@ -2239,7 +2235,7 @@ erc-open
       (erc-display-prompt)
       (goto-char (point-max)))
 
-    (erc-determine-parameters server port nick full-name user)
+    (erc-determine-parameters server port nick full-name user passwd)
 
     ;; Saving log file on exit
     (run-hook-with-args 'erc-connect-pre-hook buffer)
@@ -2337,11 +2333,9 @@ erc-select-read-args
     (setq server user-input)
 
     (setq passwd (if erc-prompt-for-password
-                     (if (and erc-password
-                              (y-or-n-p "Use the default password? "))
-                         erc-password
-                       (read-passwd "Password: "))
-                   erc-password))
+                     (read-passwd "Server password: ")
+                   (with-suppressed-warnings ((obsolete erc-password))
+                     erc-password)))
     (when (and passwd (string= "" passwd))
       (setq passwd nil))
 
@@ -3354,18 +3348,130 @@ erc-cmd-HELP
 (defalias 'erc-cmd-H #'erc-cmd-HELP)
 (put 'erc-cmd-HELP 'process-not-needed t)
 
+(defcustom erc-auth-source-server-function #'erc-auth-source-search
+  "Function to query auth-source for a server password.
+Called with a subset of keyword parameters known to `auth-source-search'
+and relevant to an opening \"PASS\" command, if any.  In return, ERC
+expects a string to send as the server password, or nil, to skip the
+\"PASS\" command completely.  An explicit `:password' argument to
+entry-point commands `erc' and `erc-tls' also inhibits lookup, as does
+setting this option to nil.  See info node `(erc) Connecting' for
+details."
+  :package-version '(ERC . "5.4.1") ; FIXME update when publishing to ELPA
+  :group 'erc
+  :type '(choice (const erc-auth-source-search)
+                 (const nil)
+                 function))
+
+(defcustom erc-auth-source-join-function #'erc-auth-source-search
+  "Function to query auth-source on joining a channel.
+Called with a subset of keyword arguments known to `auth-source-search'
+and relevant to joining a password-protected channel.  In return, ERC
+expects a string to use as the channel \"key\", or nil to just join the
+channel normally.  Setting the option itself to nil tells ERC to always
+forgo consulting auth-source for channel keys.  For more information,
+see info node `(erc) Connecting'."
+  :package-version '(ERC . "5.4.1") ; FIXME update when publishing to ELPA
+  :group 'erc
+  :type '(choice (const erc-auth-source-search)
+                 (const nil)
+                 function))
+
+(defun erc--auth-source-determine-params-defaults ()
+  (let* ((net (and-let* ((esid (erc-networks--id-symbol erc-networks--id))
+                         ((symbol-name esid)))))
+         (localp (and erc--target (erc--target-channel-local-p erc--target)))
+         (hosts (if localp
+                    (list erc-server-announced-name erc-session-server net)
+                  (list net erc-server-announced-name erc-session-server)))
+         (ports (list (cl-typecase erc-session-port
+                        (integer (number-to-string erc-session-port))
+                        (string (and (string= erc-session-port "irc")
+                                     erc-session-port)) ; or nil
+                        (t erc-session-port))
+                      "irc")))
+    (list (cons :host (delq nil hosts))
+          (cons :port (delq nil ports))
+          (cons :require '(:secret)))))
+
+(defun erc--auth-source-determine-params-merge (&rest plist)
+  "Return a plist of merged keyword args to pass to `auth-source-search'.
+Combine items in PLIST with others derived from the current connection
+context, but prioritize the former.  For keys not present in PLIST,
+favor a network ID over an announced server unless `erc--target' is a
+local channel.  And treat the dialed server address as a fallback for
+the announced name in both cases."
+  (let ((defaults (erc--auth-source-determine-params-defaults)))
+    `(,@(cl-loop for (key value) on plist by #'cddr
+                 for default = (assq key defaults)
+                 do (when default (setq defaults (delq default defaults)))
+                 append `(,key ,(delete-dups
+                                 `(,@(if (consp value) value (list value))
+                                   ,@(cdr default)))))
+      ,@(cl-loop for (k . v) in defaults append (list k v)))))
+
+(defun erc--auth-source-search (&rest defaults)
+  "Ask auth-source for a secret and return it if found.
+Use DEFAULTS as keyword arguments for querying auth-source and as a
+guide for narrowing results.  Return a string if found or nil otherwise.
+The ordering of DEFAULTS influences how results are filtered, as does
+the ordering of the members of any individual composite values.  If
+necessary, the former takes priority.  For example, if DEFAULTS were to
+contain
+
+  :host (\"foo\" \"bar\") :port (\"123\" \"456\")
+
+the secret from an auth-source entry of host foo and port 456 would be
+chosen over another of host bar and port 123.  However, if DEFAULTS
+looked like
+
+  :port (\"123\" \"456\") :host (\"foo\" \"bar\")
+
+the opposite would be true.  In both cases, two entries with the same
+host but different ports would result in the one with port 123 getting
+the nod.  Much the same would happen for entries sharing only a port:
+the one with host foo would win."
+  (when-let*
+      ((priority (map-keys defaults))
+       (test (lambda (a b)
+               (catch 'done
+                 (dolist (key priority)
+                   (let* ((d (plist-get defaults key))
+                          (defval (if (listp d) d (list d)))
+                          ;; featurep 'seq via auth-source > json > map
+                          (p (seq-position defval (plist-get a key)))
+                          (q (seq-position defval (plist-get b key))))
+                     (unless (eql p q)
+                       (throw 'done (when p (or (not q) (< p q))))))))))
+       (plist (copy-sequence defaults)))
+    (unless (plist-get plist :max)
+      (setq plist (plist-put plist :max 5000))) ; `auth-source-netrc-parse'
+    (unless (plist-get defaults :require)
+      (setq plist (plist-put plist :require '(:secret))))
+    (when-let* ((sorted (sort (apply #'auth-source-search plist) test))
+                (secret (plist-get (car sorted) :secret)))
+      (if (functionp secret) (funcall secret) secret))))
+
+(defun erc-auth-source-search (&rest plist)
+  "Call `auth-source-search', possibly with keyword params in PLIST."
+  ;; These exist as separate helpers in case folks should find them
+  ;; useful.  If that's you, please request that they be exported.
+  (apply #'erc--auth-source-search
+         (apply #'erc--auth-source-determine-params-merge plist)))
+
 (defun erc-server-join-channel (server channel &optional secret)
-  (let ((password
-         (or secret
-             (auth-source-pick-first-password
-	      :host server
-	      :port "irc"
-	      :user channel))))
-    (erc-log (format "cmd: JOIN: %s" channel))
-    (erc-server-send (concat "JOIN " channel
-			     (if password
-				 (concat " " password)
-			       "")))))
+  "Join CHANNEL, optionally with SECRET.
+Without SECRET, consult auth-source, possibly passing SERVER as the
+`:host' query parameter."
+  (unless (or secret (not erc-auth-source-join-function))
+    (unless server
+      (when (and erc-server-announced-name
+                 (erc--valid-local-channel-p channel))
+        (setq server erc-server-announced-name)))
+    (setq secret (apply erc-auth-source-join-function
+                        `(,@(and server (list :host server)) :user ,channel))))
+  (erc-log (format "cmd: JOIN: %s" channel))
+  (erc-server-send (concat "JOIN " channel (and secret (concat " " secret)))))
 
 (defun erc--valid-local-channel-p (channel)
   "Non-nil when channel is server-local on a network that allows them."
@@ -3387,19 +3493,12 @@ erc-cmd-JOIN
       (setq chnl (erc-ensure-channel-name channel)))
     (when chnl
       ;; Prevent double joining of same channel on same server.
-      (let* ((joined-channels
-              (mapcar (lambda (chanbuf)
-                        (with-current-buffer chanbuf (erc-default-target)))
-                      (erc-channel-list erc-server-process)))
-             (server (with-current-buffer (process-buffer erc-server-process)
-		       (or erc-session-server erc-server-announced-name)))
-             (chnl-name (car (erc-member-ignore-case chnl joined-channels))))
-        (if chnl-name
-            (switch-to-buffer (if (get-buffer chnl-name)
-                                  chnl-name
-                                (concat chnl-name "/" server)))
-          (setq erc--server-last-reconnect-count 0)
-	  (erc-server-join-channel server chnl key)))))
+      (if-let* ((existing (erc-get-buffer chnl erc-server-process))
+                ((with-current-buffer existing
+                   (erc-get-channel-user (erc-current-nick)))))
+          (switch-to-buffer existing)
+        (setq erc--server-last-reconnect-count 0)
+        (erc-server-join-channel nil chnl key))))
   t)
 
 (defalias 'erc-cmd-CHANNEL #'erc-cmd-JOIN)
@@ -6355,7 +6454,7 @@ erc-login
 
 ;; connection properties' heuristics
 
-(defun erc-determine-parameters (&optional server port nick name user)
+(defun erc-determine-parameters (&optional server port nick name user passwd)
   "Determine the connection and authentication parameters.
 Sets the buffer local variables:
 
@@ -6364,12 +6463,14 @@ erc-determine-parameters
 - `erc-session-port'
 - `erc-session-user-full-name'
 - `erc-session-username'
+- `erc-session-password'
 - `erc-server-current-nick'"
   (setq erc-session-connector erc-server-connect-function
         erc-session-server (erc-compute-server server)
         erc-session-port (or port erc-default-port)
         erc-session-user-full-name (erc-compute-full-name name)
-        erc-session-username (erc-compute-user user))
+        erc-session-username (erc-compute-user user)
+        erc-session-password (erc--compute-server-password passwd nick))
   (erc-set-current-nick (erc-compute-nick nick)))
 
 (defun erc-compute-server (&optional server)
@@ -6406,6 +6507,12 @@ erc-compute-nick
       (getenv "IRCNICK")
       (user-login-name)))
 
+(defun erc--compute-server-password (password nick)
+  "Maybe provide a PASSWORD argument for the IRC \"PASS\" command.
+When `erc-auth-source-server-function' is non-nil, call it with NICK for
+the user field and use whatever it returns as the server password."
+  (or password (and erc-auth-source-server-function
+                    (funcall erc-auth-source-server-function :user nick))))
 
 (defun erc-compute-full-name (&optional full-name)
   "Return user's full name.
diff --git a/test/lisp/erc/erc-services-tests.el b/test/lisp/erc/erc-services-tests.el
new file mode 100644
index 0000000000..8e2b8d2927
--- /dev/null
+++ b/test/lisp/erc/erc-services-tests.el
@@ -0,0 +1,574 @@
+;;; erc-services-tests.el --- Tests for erc-services.  -*- lexical-binding:t -*-
+
+;; Copyright (C) 2020-2022 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+;;
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published
+;; by the Free Software Foundation, either version 3 of the License,
+;; or (at your option) any later version.
+;;
+;; GNU Emacs is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; TODO: move the auth-source tests somewhere else.  They've been
+;; stashed here for pragmatic reasons.
+
+;;; Code:
+
+(require 'ert-x)
+(require 'erc-services)
+(require 'erc-compat)
+(require 'secrets)
+
+;;;; Core auth-source
+
+(ert-deftest erc--auth-source-determine-params-merge ()
+  (let ((erc-session-server "irc.gnu.org")
+        (erc-server-announced-name "my.gnu.org")
+        (erc-session-port 6697)
+        (erc-network 'fake)
+        (erc-server-current-nick "tester")
+        (erc-networks--id (erc-networks--id-create 'GNU.chat)))
+
+    (should (equal (erc--auth-source-determine-params-merge)
+                   '(:host ("GNU.chat" "my.gnu.org" "irc.gnu.org")
+                           :port ("6697" "irc")
+                           :require (:secret))))
+
+    (should (equal (erc--auth-source-determine-params-merge :host "fake")
+                   '(:host ("fake" "GNU.chat" "my.gnu.org" "irc.gnu.org")
+                           :port ("6697" "irc")
+                           :require (:secret))))
+
+    (should (equal (erc--auth-source-determine-params-merge
+                    :host '("fake") :require :host)
+                   '(:host ("fake" "GNU.chat" "my.gnu.org" "irc.gnu.org")
+                           :require (:host :secret)
+                           :port ("6697" "irc"))))
+
+    (should (equal (erc--auth-source-determine-params-merge
+                    :host '("fake" "GNU.chat") :port "1234" :x "x")
+                   '(:host ("fake" "GNU.chat" "my.gnu.org" "irc.gnu.org")
+                           :port ("1234" "6697" "irc")
+                           :x ("x")
+                           :require (:secret))))))
+
+;; Some of the following may be related to bug#23438.
+
+(defun erc-services-tests--auth-source-standard (search)
+
+  (ert-info ("Session wins")
+    (let ((erc-session-server "irc.gnu.org")
+          (erc-server-announced-name "my.gnu.org")
+          (erc-session-port 6697)
+          (erc-network 'fake)
+          (erc-server-current-nick "tester")
+          (erc-networks--id (erc-networks--id-create 'GNU.chat)))
+      (should (string= (funcall search :user "#chan") "foo"))))
+
+  (ert-info ("Network wins")
+    (let* ((erc-session-server "irc.gnu.org")
+           (erc-server-announced-name "my.gnu.org")
+           (erc-session-port 6697)
+           (erc-network 'GNU.chat)
+           (erc-server-current-nick "tester")
+           (erc-networks--id (erc-networks--id-create nil)))
+      (should (string= (funcall search :user "#chan") "foo"))))
+
+  (ert-info ("Announced wins")
+    (let ((erc-session-server "irc.gnu.org")
+          (erc-server-announced-name "my.gnu.org")
+          (erc-session-port 6697)
+          erc-network
+          (erc-networks--id (erc-networks--id-create nil)))
+      (should (string= (funcall search :user "#chan") "baz")))))
+
+(defun erc-services-tests--auth-source-announced (search)
+  (let* ((erc--isupport-params (make-hash-table))
+         (erc-server-parameters '(("CHANTYPES" . "&#")))
+         (erc--target (erc--target-from-string "&chan")))
+
+    (ert-info ("Announced prioritized")
+
+      (ert-info ("Announced wins")
+        (let* ((erc-session-server "irc.gnu.org")
+               (erc-server-announced-name "my.gnu.org")
+               (erc-session-port 6697)
+               (erc-network 'GNU.chat)
+               (erc-server-current-nick "tester")
+               (erc-networks--id (erc-networks--id-create nil)))
+          (should (string= (funcall search :user "#chan") "baz"))))
+
+      (ert-info ("Peer next")
+        (let* ((erc-server-announced-name "irc.gnu.org")
+               (erc-session-port 6697)
+               (erc-network 'GNU.chat)
+               (erc-server-current-nick "tester")
+               (erc-networks--id (erc-networks--id-create nil)))
+          (should (string= (funcall search :user "#chan") "bar"))))
+
+      (ert-info ("Network used as fallback")
+        (let* ((erc-session-port 6697)
+               (erc-network 'GNU.chat)
+               (erc-server-current-nick "tester")
+               (erc-networks--id (erc-networks--id-create nil)))
+          (should (string= (funcall search :user "#chan") "foo")))))))
+
+(defun erc-services-tests--auth-source-overrides (search)
+  (let* ((erc-session-server "irc.gnu.org")
+         (erc-server-announced-name "my.gnu.org")
+         (erc-network 'GNU.chat)
+         (erc-server-current-nick "tester")
+         (erc-networks--id (erc-networks--id-create nil))
+         (erc-session-port 6667))
+
+    (ert-info ("Specificity and overrides")
+
+      (ert-info ("More specific port")
+        (let ((erc-session-port 6697))
+          (should (string= (funcall search :user "#chan") "spam"))))
+
+      (ert-info ("More specific user (network loses)")
+        (should (string= (funcall search :user '("#fsf")) "42")))
+
+      (ert-info ("Actual override")
+        (should (string= (funcall search :port "6667") "sesame")))
+
+      (ert-info ("Overrides don't interfere with post-processing")
+        (should (string= (funcall search :host "MyHost") "123"))))))
+
+;; auth-source netrc backend
+
+(defvar erc-services-tests--auth-source-entries
+  '("machine irc.gnu.org port irc user \"#chan\" password bar"
+    "machine my.gnu.org port irc user \"#chan\" password baz"
+    "machine GNU.chat port irc user \"#chan\" password foo"))
+
+;; FIXME explain what this is for
+(defun erc-services-tests--auth-source-shuffle (&rest extra)
+  (string-join `(,@(sort (append erc-services-tests--auth-source-entries extra)
+                         (lambda (&rest _) (zerop (random 2))))
+                 "")
+               "\n"))
+
+(ert-deftest erc--auth-source-search--netrc-standard ()
+  (ert-with-temp-file netrc-file
+    :prefix "erc--auth-source-search--standard"
+    :text (erc-services-tests--auth-source-shuffle)
+
+    (let ((auth-sources (list netrc-file))
+          (auth-source-do-cache nil))
+      (erc-services-tests--auth-source-standard #'erc-auth-source-search))))
+
+(ert-deftest erc--auth-source-search--netrc-announced ()
+  (ert-with-temp-file netrc-file
+    :prefix "erc--auth-source-search--announced"
+    :text (erc-services-tests--auth-source-shuffle)
+
+    (let ((auth-sources (list netrc-file))
+          (auth-source-do-cache nil))
+      (erc-services-tests--auth-source-announced #'erc-auth-source-search))))
+
+(ert-deftest erc--auth-source-search--netrc-overrides ()
+  (ert-with-temp-file netrc-file
+    :prefix "erc--auth-source-search--overrides"
+    :text (erc-services-tests--auth-source-shuffle
+           "machine GNU.chat port 6697 user \"#chan\" password spam"
+           "machine my.gnu.org port irc user \"#fsf\" password 42"
+           "machine irc.gnu.org port 6667 password sesame"
+           "machine MyHost port irc password 456"
+           "machine MyHost port 6667 password 123")
+
+    (let ((auth-sources (list netrc-file))
+          (auth-source-do-cache nil))
+      (erc-services-tests--auth-source-overrides #'erc-auth-source-search))))
+
+;; auth-source plstore backend
+
+(defun erc-services-test--call-with-plstore (&rest args)
+  (advice-add 'epg-decrypt-string :override
+              (lambda (&rest r) (prin1-to-string (cadr r)))
+              '((name . erc--auth-source-plstore)))
+  (advice-add 'epg-find-configuration :override
+              (lambda (&rest _) "" '((program . "/bin/true")))
+              '((name . erc--auth-source-plstore)))
+  (unwind-protect
+      (apply #'erc-auth-source-search args)
+    (advice-remove 'epg-decrypt-string 'erc--auth-source-plstore)
+    (advice-remove 'epg-find-configuration 'erc--auth-source-plstore)))
+
+(defvar erc-services-tests--auth-source-plstore-standard-entries
+  '(("ba950d38118a76d71f9f0591bb373d6cb366a512"
+     :secret-secret t
+     :host "irc.gnu.org"
+     :user "#chan"
+     :port "irc")
+    ("7f17ca445d11158065e911a6d0f4cbf52ca250e3"
+     :secret-secret t
+     :host "my.gnu.org"
+     :user "#chan"
+     :port "irc")
+    ("fcd3c8bd6daf4509de0ad6ee98e744ce0fca9377"
+     :secret-secret t
+     :host "GNU.chat"
+     :user "#chan"
+     :port "irc")))
+
+(defvar erc-services-tests--auth-source-plstore-standard-secrets
+  '(("ba950d38118a76d71f9f0591bb373d6cb366a512" :secret "bar")
+    ("7f17ca445d11158065e911a6d0f4cbf52ca250e3" :secret "baz")
+    ("fcd3c8bd6daf4509de0ad6ee98e744ce0fca9377" :secret "foo")))
+
+(ert-deftest erc--auth-source-search--plstore-standard ()
+  (ert-with-temp-file plstore-file
+    :suffix ".plist"
+    :text (concat ";;; public entries -*- mode: plstore -*- \n"
+                  (prin1-to-string
+                   erc-services-tests--auth-source-plstore-standard-entries)
+                  "\n;;; secret entries\n"
+                  (prin1-to-string
+                   erc-services-tests--auth-source-plstore-standard-secrets)
+                  "\n")
+
+    (let ((auth-sources (list plstore-file))
+          (auth-source-do-cache nil))
+      (erc-services-tests--auth-source-standard
+       #'erc-services-test--call-with-plstore))))
+
+(ert-deftest erc--auth-source-search--plstore-announced ()
+  (ert-with-temp-file plstore-file
+    :suffix ".plist"
+    :text (concat ";;; public entries -*- mode: plstore -*- \n"
+                  (prin1-to-string
+                   erc-services-tests--auth-source-plstore-standard-entries)
+                  "\n;;; secret entries\n"
+                  (prin1-to-string
+                   erc-services-tests--auth-source-plstore-standard-secrets)
+                  "\n")
+
+    (let ((auth-sources (list plstore-file))
+          (auth-source-do-cache nil))
+      (erc-services-tests--auth-source-announced
+       #'erc-services-test--call-with-plstore))))
+
+(ert-deftest erc--auth-source-search--plstore-overrides ()
+  (ert-with-temp-file plstore-file
+    :suffix ".plist"
+    :text (concat
+           ";;; public entries -*- mode: plstore -*- \n"
+           (prin1-to-string
+            `(,@erc-services-tests--auth-source-plstore-standard-entries
+              ("1b3fab249a8dff77a4d8fe7eb4b0171b25cc711a"
+               :secret-secret t :host "GNU.chat" :user "#chan" :port "6697")
+              ("6cbcdc39476b8cfcca6f3e9a7876f41ec3f708cc"
+               :secret-secret t :host "my.gnu.org" :user "#fsf" :port "irc")
+              ("a33e2b3bd2d6f33995a4b88710a594a100c5e41d"
+               :secret-secret t :host "irc.gnu.org" :port "6667")
+              ("ab2fd349b2b7d6a9215bb35a92d054261b0b1537"
+               :secret-secret t :host "MyHost" :port "irc")
+              ("61a6bd552059494f479ff720e8de33e22574650a"
+               :secret-secret t :host "MyHost" :port "6667")))
+           "\n;;; secret entries\n"
+           (prin1-to-string
+            `(,@erc-services-tests--auth-source-plstore-standard-secrets
+              ("1b3fab249a8dff77a4d8fe7eb4b0171b25cc711a" :secret "spam")
+              ("6cbcdc39476b8cfcca6f3e9a7876f41ec3f708cc" :secret "42")
+              ("a33e2b3bd2d6f33995a4b88710a594a100c5e41d" :secret "sesame")
+              ("ab2fd349b2b7d6a9215bb35a92d054261b0b1537" :secret "456")
+              ("61a6bd552059494f479ff720e8de33e22574650a" :secret "123")))
+           "\n")
+
+    (let ((auth-sources (list plstore-file))
+          (auth-source-do-cache nil))
+      (erc-services-tests--auth-source-overrides
+       #'erc-services-test--call-with-plstore))))
+
+;; auth-source JSON backend
+
+(defvar erc-services-tests--auth-source-json-standard-entries
+  [(:host "irc.gnu.org" :port "irc" :user "#chan" :secret "bar")
+   (:host "my.gnu.org" :port "irc" :user "#chan" :secret "baz")
+   (:host "GNU.chat" :port "irc" :user "#chan" :secret "foo")])
+
+(ert-deftest erc--auth-source-search--json-standard ()
+  (ert-with-temp-file json-store
+    :suffix ".json"
+    :text (let ((json-object-type 'plist))
+            (json-encode
+             erc-services-tests--auth-source-json-standard-entries))
+    (let ((auth-sources (list json-store))
+          (auth-source-do-cache nil))
+      (erc-services-tests--auth-source-standard #'erc-auth-source-search))))
+
+(ert-deftest erc--auth-source-search--json-announced ()
+  (ert-with-temp-file plstore-file
+    :suffix ".json"
+    :text (let ((json-object-type 'plist))
+            (json-encode
+             erc-services-tests--auth-source-json-standard-entries))
+
+    (let ((auth-sources (list plstore-file))
+          (auth-source-do-cache nil))
+      (erc-services-tests--auth-source-announced #'erc-auth-source-search))))
+
+(ert-deftest erc--auth-source-search--json-overrides ()
+  (ert-with-temp-file json-file
+    :suffix ".json"
+    :text (let ((json-object-type 'plist))
+            (json-encode
+             (vconcat
+              erc-services-tests--auth-source-json-standard-entries
+              [(:secret "spam" :host "GNU.chat" :user "#chan" :port "6697")
+               (:secret "42" :host "my.gnu.org" :user "#fsf" :port "irc")
+               (:secret "sesame" :host "irc.gnu.org" :port "6667")
+               (:secret "456" :host "MyHost" :port "irc")
+               (:secret "123" :host "MyHost" :port "6667")])))
+
+    (let ((auth-sources (list json-file))
+          (auth-source-do-cache nil))
+      (erc-services-tests--auth-source-overrides #'erc-auth-source-search))))
+
+;; auth-source-secrets backend
+
+(defvar erc-services-tests--auth-source-secrets-standard-entries
+  '(("#[email protected]:irc" ; label
+     (:host . "irc.gnu.org")
+     (:user . "#chan")
+     (:port . "irc")
+     (:xdg:schema . "org.freedesktop.Secret.Generic"))
+    ("#[email protected]:irc"
+     (:host . "my.gnu.org")
+     (:user . "#chan")
+     (:port . "irc")
+     (:xdg:schema . "org.freedesktop.Secret.Generic"))
+    ("#[email protected]:irc"
+     (:host . "GNU.chat")
+     (:user . "#chan")
+     (:port . "irc")
+     (:xdg:schema . "org.freedesktop.Secret.Generic"))))
+
+(defvar erc-services-tests--auth-source-secrets-standard-secrets
+  '(("#[email protected]:irc" . "bar")
+    ("#[email protected]:irc" . "baz")
+    ("#[email protected]:irc" . "foo")))
+
+(ert-deftest erc--auth-source-search--secrets-standard ()
+  (skip-unless (bound-and-true-p secrets-enabled))
+  (let ((auth-sources '("secrets:Test"))
+        (auth-source-do-cache nil)
+        (entries erc-services-tests--auth-source-secrets-standard-entries)
+        (secrets erc-services-tests--auth-source-secrets-standard-secrets))
+
+    (cl-letf (((symbol-function 'secrets-search-items)
+               (lambda (col &rest r)
+                 (should (equal col "Test"))
+                 (should (plist-get r :user))
+                 (map-keys entries)))
+              ((symbol-function 'secrets-get-secret)
+               (lambda (col label)
+                 (should (equal col "Test"))
+                 (assoc-default label secrets)))
+              ((symbol-function 'secrets-get-attributes)
+               (lambda (col label)
+                 (should (equal col "Test"))
+                 (assoc-default label entries))))
+
+      (erc-services-tests--auth-source-standard #'erc-auth-source-search))))
+
+(ert-deftest erc--auth-source-search--secrets-announced ()
+  (skip-unless (bound-and-true-p secrets-enabled))
+  (let ((auth-sources '("secrets:Test"))
+        (auth-source-do-cache nil)
+        (entries erc-services-tests--auth-source-secrets-standard-entries)
+        (secrets erc-services-tests--auth-source-secrets-standard-secrets))
+
+    (cl-letf (((symbol-function 'secrets-search-items)
+               (lambda (col &rest r)
+                 (should (equal col "Test"))
+                 (should (plist-get r :user))
+                 (map-keys entries)))
+              ((symbol-function 'secrets-get-secret)
+               (lambda (col label)
+                 (should (equal col "Test"))
+                 (assoc-default label secrets)))
+              ((symbol-function 'secrets-get-attributes)
+               (lambda (col label)
+                 (should (equal col "Test"))
+                 (assoc-default label entries))))
+
+      (erc-services-tests--auth-source-announced #'erc-auth-source-search))))
+
+(ert-deftest erc--auth-source-search--secrets-overrides ()
+  (skip-unless (bound-and-true-p secrets-enabled))
+  (let ((auth-sources '("secrets:Test"))
+        (auth-source-do-cache nil)
+        (entries `(,@erc-services-tests--auth-source-secrets-standard-entries
+                   ("#[email protected]:6697"
+                    (:host . "GNU.chat") (:user . "#chan") (:port . "6697")
+                    (:xdg:schema . "org.freedesktop.Secret.Generic"))
+                   ("#[email protected]:irc"
+                    (:host . "my.gnu.org") (:user . "#fsf") (:port . "irc")
+                    (:xdg:schema . "org.freedesktop.Secret.Generic"))
+                   ("irc.gnu.org:6667"
+                    (:host . "irc.gnu.org") (:port . "6667")
+                    (:xdg:schema . "org.freedesktop.Secret.Generic"))
+                   ("MyHost:irc"
+                    (:host . "MyHost") (:port . "irc")
+                    (:xdg:schema . "org.freedesktop.Secret.Generic"))
+                   ("MyHost:6667"
+                    (:host . "MyHost") (:port . "6667")
+                    (:xdg:schema . "org.freedesktop.Secret.Generic"))))
+        (secrets `(,@erc-services-tests--auth-source-secrets-standard-secrets
+                   ("#[email protected]:6697" . "spam")
+                   ("#[email protected]:irc" . "42" )
+                   ("irc.gnu.org:6667" . "sesame")
+                   ("MyHost:irc" . "456")
+                   ("MyHost:6667" . "123"))))
+
+    (cl-letf (((symbol-function 'secrets-search-items)
+               (lambda (col &rest _)
+                 (should (equal col "Test"))
+                 (map-keys entries)))
+              ((symbol-function 'secrets-get-secret)
+               (lambda (col label)
+                 (should (equal col "Test"))
+                 (assoc-default label secrets)))
+              ((symbol-function 'secrets-get-attributes)
+               (lambda (col label)
+                 (should (equal col "Test"))
+                 (assoc-default label entries))))
+
+      (erc-services-tests--auth-source-overrides #'erc-auth-source-search))))
+
+;; auth-source-pass backend
+
+(require 'auth-source-pass)
+
+;; `auth-source-pass--find-match-unambiguous' returns something like:
+;;
+;;   (list :host "irc.gnu.org"
+;;         :port "6697"
+;;         :user "rms"
+;;         :secret
+;;         #[0 "\301\302\300\"\207"
+;;             [((secret . "freedom")) auth-source-pass--get-attr secret] 3])
+;;
+;; This function gives ^ (faked here to avoid gpg and file IO).  See
+;; `auth-source-pass--with-store' in ../auth-source-pass-tests.el
+(defun erc-services-tests--asp-parse-entry (store entry)
+  (when-let ((found (cl-find entry store :key #'car :test #'string=)))
+    (list (assoc 'secret (cdr found)))))
+
+(defvar erc-join-tests--auth-source-pass-entries
+  '(("irc.gnu.org:irc/#chan"
+     ("port" . "irc") ("user" . "#chan") (secret . "bar"))
+    ("my.gnu.org:irc/#chan"
+     ("port" . "irc") ("user" . "#chan") (secret . "baz"))
+    ("GNU.chat:irc/#chan"
+     ("port" . "irc") ("user" . "#chan") (secret . "foo"))))
+
+(ert-deftest erc--auth-source-search--pass-standard ()
+  (ert-skip "Pass backend not yet supported")
+  (let ((store erc-join-tests--auth-source-pass-entries)
+        (auth-sources '(password-store))
+        (auth-source-do-cache nil))
+
+    (cl-letf (((symbol-function 'auth-source-pass-parse-entry)
+               (apply-partially #'erc-services-tests--asp-parse-entry store))
+              ((symbol-function 'auth-source-pass-entries)
+               (lambda () (mapcar #'car store))))
+
+      (erc-services-tests--auth-source-standard #'erc-auth-source-search))))
+
+(ert-deftest erc--auth-source-search--pass-announced ()
+  (ert-skip "Pass backend not yet supported")
+  (let ((store erc-join-tests--auth-source-pass-entries)
+        (auth-sources '(password-store))
+        (auth-source-do-cache nil))
+
+    (cl-letf (((symbol-function 'auth-source-pass-parse-entry)
+               (apply-partially #'erc-services-tests--asp-parse-entry store))
+              ((symbol-function 'auth-source-pass-entries)
+               (lambda () (mapcar #'car store))))
+
+      (erc-services-tests--auth-source-announced #'erc-auth-source-search))))
+
+(ert-deftest erc--auth-source-search--pass-overrides ()
+  (ert-skip "Pass backend not yet supported")
+  (let ((store
+         `(,@erc-join-tests--auth-source-pass-entries
+           ("GNU.chat:6697/#chan"
+            ("port" . "6697") ("user" . "#chan") (secret . "spam"))
+           ("my.gnu.org:irc/#fsf"
+            ("port" . "irc") ("user" . "#fsf") (secret . "42"))
+           ("irc.gnu.org:6667"
+            ("port" . "6667") (secret . "sesame"))
+           ("MyHost:irc"
+            ("port" . "irc") (secret . "456"))
+           ("MyHost:6667"
+            ("port" . "6667") (secret . "123"))))
+        (auth-sources '(password-store))
+        (auth-source-do-cache nil))
+
+    (cl-letf (((symbol-function 'auth-source-pass-parse-entry)
+               (apply-partially #'erc-services-tests--asp-parse-entry store))
+              ((symbol-function 'auth-source-pass-entries)
+               (lambda () (mapcar #'car store))))
+
+      (erc-services-tests--auth-source-overrides #'erc-auth-source-search))))
+
+;;;; The services module
+
+(ert-deftest erc-nickserv-get-password ()
+  (should erc-prompt-for-nickserv-password)
+  (ert-with-temp-file netrc-file
+    :prefix "erc-nickserv-get-password"
+    :text (mapconcat 'identity
+                     '("machine GNU/chat port 6697 user bob password spam"
+                       "machine FSF.chat port 6697 user bob password sesame"
+                       "machine MyHost port irc password 123")
+                     "\n")
+
+    (let* ((auth-sources (list netrc-file))
+           (auth-source-do-cache nil)
+           (erc-nickserv-passwords '((FSF.chat (("alice" . "foo")
+                                                ("joe" . "bar")))))
+           (erc-use-auth-source-for-nickserv-password t)
+           (erc-session-server "irc.gnu.org")
+           (erc-server-announced-name "my.gnu.org")
+           (erc-network 'FSF.chat)
+           (erc-server-current-nick "tester")
+           (erc-networks--id (erc-networks--id-create nil))
+           (erc-session-port 6697))
+
+      (ert-info ("Lookup custom option")
+        (should (string= (erc-nickserv-get-password "alice") "foo")))
+
+      (ert-info ("Auth source")
+        (ert-info ("Network")
+          (should (string= (erc-nickserv-get-password "bob") "sesame")))
+
+        (ert-info ("Network ID")
+          (let ((erc-networks--id (erc-networks--id-create 'GNU/chat)))
+            (should (string= (erc-nickserv-get-password "bob") "spam")))))
+
+      (ert-info ("Read input")
+        (should (string=
+                 (ert-simulate-keys "baz\r" (erc-nickserv-get-password "mike"))
+                 "baz")))
+
+      (ert-info ("Failed")
+        (should-not (ert-simulate-keys "\r"
+                      (erc-nickserv-get-password "fake")))))))
+
+
+;;; erc-services-tests.el ends here
-- 
2.36.1
0025-SQUASH-ME-Add-ERC-test-scenarios-involving-auth-sour.patch (text/x-patch, 18.6 KB)
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Wed, 29 Sep 2021 01:30:16 -0700
Subject: [PATCH 25/35] SQUASH-ME: Add ERC test scenarios involving auth-source

XXX this should be combined with the commit entitled "Make auth-source
searches session-ID aware in ERC".  It was split off for the sake of
flexibility during code review.

* test/lisp/erc/erc-scenarios/erc-scenarios-auth-source.el: Add
session-aware scenarios involving the auth-source queries.  See
bug#48598 for background.
---
 .../erc-scenarios-auth-source.el              | 178 ++++++++++++++++++
 .../resources/base/auth-source/foonet.eld     |  23 +++
 .../resources/base/auth-source/nopass.eld     |  22 +++
 .../resources/erc-scenarios-common.el         |   1 +
 .../resources/services/auth-source/libera.eld |  49 +++++
 5 files changed, 273 insertions(+)
 create mode 100644 test/lisp/erc/erc-scenarios/erc-scenarios-auth-source.el
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/auth-source/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/auth-source/nopass.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/services/auth-source/libera.eld

diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-auth-source.el b/test/lisp/erc/erc-scenarios/erc-scenarios-auth-source.el
new file mode 100644
index 0000000000..3d399a1815
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-auth-source.el
@@ -0,0 +1,178 @@
+;;; erc-scenarios-auth-source.el --- auth-source scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+;;
+;; This file is part of GNU Emacs.
+;;
+;; This program is free software: you can redistribute it and/or
+;; modify it under the terms of the GNU General Public License as
+;; published by the Free Software Foundation, either version 3 of the
+;; License, or (at your option) any later version.
+;;
+;; This program is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see
+;; <https://www.gnu.org/licenses/>.
+
+;; Commentary:
+;;
+;; For practical reasons (mainly lack of imagination), this file
+;; contains tests for both server-password and NickServ contexts.
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(eval-when-compile (require 'erc-join)
+                   (require 'erc-services))
+
+(defun erc-scenarios-common--auth-source (id dialog &rest rest)
+  (push "machine GNU.chat port %d user \"#chan\" password spam" rest)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/auth-source")
+       (dumb-server (erc-d-run "localhost" t dialog))
+       (port (process-contact dumb-server :service))
+       (ents `(,@(mapcar (lambda (fmt) (format fmt port)) rest)
+               "machine MyHost port irc password 123"))
+       (netrc-file (make-temp-file "auth-source-test" nil nil
+                                   (string-join ents "\n")))
+       (auth-sources (list netrc-file))
+       (auth-source-do-cache nil)
+       (erc-scenarios-common-extra-teardown (lambda ()
+                                              (delete-file netrc-file))))
+
+    (ert-info ("Connect")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :full-name "tester"
+                                :id id)
+        (should (string= (buffer-name) (if id
+                                           (symbol-name id)
+                                         (format "127.0.0.1:%d" port))))
+        (erc-d-t-wait-for 5 (eq erc-network 'FooNet))))))
+
+(ert-deftest erc-scenarios-base-auth-source-server--dialed ()
+  :tags '(:expensive-test)
+  (erc-scenarios-common--auth-source
+   nil 'foonet
+   "machine GNU.chat port %d user tester password fake"
+   "machine FooNet port %d user tester password fake"
+   "machine 127.0.0.1 port %d user tester password changeme"
+   "machine 127.0.0.1 port %d user imposter password fake"))
+
+(ert-deftest erc-scenarios-base-auth-source-server--netid ()
+  :tags '(:expensive-test)
+  (erc-scenarios-common--auth-source
+   'MySession 'foonet
+   "machine MySession port %d user tester password changeme"
+   "machine 127.0.0.1 port %d user tester password fake"
+   "machine FooNet port %d user tester password fake"))
+
+(ert-deftest erc-scenarios-base-auth-source-server--netid-custom ()
+  :tags '(:expensive-test)
+  (let ((erc-auth-source-server-function
+         (lambda (&rest _) (erc-auth-source-search :host "MyHost"))))
+    (erc-scenarios-common--auth-source
+     'MySession 'foonet
+     "machine 127.0.0.1 port %d user tester password fake"
+     "machine MyHost port %d user tester password changeme"
+     "machine MySession port %d user tester password fake")))
+
+(ert-deftest erc-scenarios-base-auth-source-server--nopass ()
+  :tags '(:expensive-test)
+  (let (erc-auth-source-server-function)
+    (erc-scenarios-common--auth-source nil 'nopass)))
+
+(ert-deftest erc-scenarios-base-auth-source-server--nopass-netid ()
+  :tags '(:expensive-test)
+  (let (erc-auth-source-server-function)
+    (erc-scenarios-common--auth-source 'MySession 'nopass)))
+
+;; Identify via auth source with no initial password
+
+(defun erc-scenarios-common--services-auth-source (&rest rest)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "services/auth-source")
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (erc-d-run "localhost" t 'libera))
+       (port (process-contact dumb-server :service))
+       (ents `(,@(mapcar (lambda (fmt) (format fmt port)) rest)
+               "machine MyHost port irc password 123"))
+       (netrc-file (make-temp-file "auth-source-test" nil nil
+                                   (string-join ents "\n")))
+       (auth-sources (list netrc-file))
+       (auth-source-do-cache nil)
+       (erc-modules (cons 'services erc-modules))
+       (erc-use-auth-source-for-nickserv-password t) ; do consult for NickServ
+       (expect (erc-d-t-make-expecter))
+       (erc-scenarios-common-extra-teardown (lambda ()
+                                              (delete-file netrc-file))))
+
+    (cl-letf (((symbol-function 'read-passwd)
+               (lambda (&rest _) (error "Unexpected read-passwd call"))))
+      (ert-info ("Connect without password")
+        (with-current-buffer (erc :server "127.0.0.1"
+                                  :port port
+                                  :nick "tester"
+                                  :full-name "tester")
+          (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+          (erc-d-t-wait-for 8 (eq erc-network 'Libera.Chat))
+          (funcall expect 3 "This nickname is registered.")
+          (funcall expect 3 "You are now identified")
+          (funcall expect 3 "Last login from")
+          (erc-cmd-QUIT ""))))
+
+    (erc-services-mode -1)
+
+    (should-not (memq 'services erc-modules))))
+
+;; These tests are about authenticating to nick services
+
+(ert-deftest erc-scenarios-services-auth-source--network ()
+  :tags '(:expensive-test)
+  ;; Skip consulting auth-source for the server password (PASS).
+  (let (erc-auth-source-server-function)
+    (erc-scenarios-common--services-auth-source
+     "machine 127.0.0.1 port %d user tester password spam"
+     "machine zirconium.libera.chat port %d user tester password fake"
+     "machine Libera.Chat port %d user tester password changeme")))
+
+(ert-deftest erc-scenarios-services-auth-source--network-connect-lookup ()
+  :tags '(:expensive-test)
+  ;; Do consult auth-source for the server password (and find nothing)
+  (erc-scenarios-common--services-auth-source
+   "machine zirconium.libera.chat port %d user tester password fake"
+   "machine Libera.Chat port %d user tester password changeme"))
+
+(ert-deftest erc-scenarios-services-auth-source--announced ()
+  :tags '(:expensive-test)
+  (let (erc-auth-source-server-function)
+    (erc-scenarios-common--services-auth-source
+     "machine 127.0.0.1 port %d user tester password spam"
+     "machine zirconium.libera.chat port %d user tester password changeme")))
+
+(ert-deftest erc-scenarios-services-auth-source--dialed ()
+  :tags '(:expensive-test)
+  ;; Support legacy host -> domain name
+  ;; (likely most common in real configs)
+  (let (erc-auth-source-server-function)
+    (erc-scenarios-common--services-auth-source
+     "machine 127.0.0.1 port %d user tester password changeme")))
+
+(ert-deftest erc-scenarios-services-auth-source--custom ()
+  :tags '(:expensive-test)
+  (let (erc-auth-source-server-function
+        (erc-auth-source-services-function
+         (lambda (&rest _) (erc-auth-source-search :host "MyAccount"))))
+    (erc-scenarios-common--services-auth-source
+     "machine zirconium.libera.chat port %d user tester password spam"
+     "machine MyAccount port %d user tester password changeme"
+     "machine 127.0.0.1 port %d user tester password fake")))
+
+;;; erc-scenarios-auth-source.el ends here
diff --git a/test/lisp/erc/erc-scenarios/resources/base/auth-source/foonet.eld b/test/lisp/erc/erc-scenarios/resources/base/auth-source/foonet.eld
new file mode 100644
index 0000000000..1fe772c7e2
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/auth-source/foonet.eld
@@ -0,0 +1,23 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.foonet.org 003 tester :This server was created Tue, 04 May 2021 05:06:18 UTC")
+ (0 ":irc.foonet.org 004 tester irc.foonet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.foonet.org 005 tester AWAYLEN=390 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# ELIST=U EXCEPTS EXTBAN=,m FORWARD=f INVEX KICKLEN=390 :are supported by this server")
+ (0 ":irc.foonet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=FooNet NICKLEN=32 PREFIX=(qaohv)~&@%+ STATUSMSG=~&@%+ TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 TOPICLEN=390 UTF8MAPPING=rfc8265 UTF8ONLY WHOX :are supported by this server")
+ (0 ":irc.foonet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.foonet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":irc.foonet.org 221 tester +i")
+ (0 ":irc.foonet.org NOTICE tester :This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect."))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/auth-source/nopass.eld b/test/lisp/erc/erc-scenarios/resources/base/auth-source/nopass.eld
new file mode 100644
index 0000000000..3fdb4ecf7b
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/auth-source/nopass.eld
@@ -0,0 +1,22 @@
+;; -*- mode: lisp-data; -*-
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.foonet.org 003 tester :This server was created Tue, 04 May 2021 05:06:18 UTC")
+ (0 ":irc.foonet.org 004 tester irc.foonet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.foonet.org 005 tester AWAYLEN=390 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# ELIST=U EXCEPTS EXTBAN=,m FORWARD=f INVEX KICKLEN=390 :are supported by this server")
+ (0 ":irc.foonet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=FooNet NICKLEN=32 PREFIX=(qaohv)~&@%+ STATUSMSG=~&@%+ TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 TOPICLEN=390 UTF8MAPPING=rfc8265 UTF8ONLY WHOX :are supported by this server")
+ (0 ":irc.foonet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.foonet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 1.2 "MODE tester +i")
+ (0 ":irc.foonet.org 221 tester +i")
+ (0 ":irc.foonet.org NOTICE tester :This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect."))
diff --git a/test/lisp/erc/erc-scenarios/resources/erc-scenarios-common.el b/test/lisp/erc/erc-scenarios/resources/erc-scenarios-common.el
index 3973bfbf35..028afa0d52 100644
--- a/test/lisp/erc/erc-scenarios/resources/erc-scenarios-common.el
+++ b/test/lisp/erc/erc-scenarios/resources/erc-scenarios-common.el
@@ -122,6 +122,7 @@ erc-scenarios-common--print-trace
       (erc-modules (copy-sequence erc-modules))
       (inhibit-interaction t)
       (auth-source-do-cache nil)
+      (erc-auth-source-parameters-join-function nil)
       (erc-autojoin-channels-alist nil)
       (erc-server-auto-reconnect nil)
       ,@bindings)))
diff --git a/test/lisp/erc/erc-scenarios/resources/services/auth-source/libera.eld b/test/lisp/erc/erc-scenarios/resources/services/auth-source/libera.eld
new file mode 100644
index 0000000000..c8dbc9d425
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/services/auth-source/libera.eld
@@ -0,0 +1,49 @@
+;; -*- mode: lisp-data; -*-
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0.26 ":zirconium.libera.chat NOTICE * :*** Checking Ident")
+ (0.01 ":zirconium.libera.chat NOTICE * :*** Looking up your hostname...")
+ (0.01 ":zirconium.libera.chat NOTICE * :*** No Ident response")
+ (0.02 ":zirconium.libera.chat NOTICE * :*** Found your hostname: static-198-54-131-100.cust.tzulo.com")
+ (0.02 ":zirconium.libera.chat 001 tester :Welcome to the Libera.Chat Internet Relay Chat Network tester")
+ (0.01 ":zirconium.libera.chat 002 tester :Your host is zirconium.libera.chat[46.16.175.175/6697], running version solanum-1.0-dev")
+ (0.03 ":zirconium.libera.chat 003 tester :This server was created Wed Jun 9 2021 at 01:38:28 UTC")
+ (0.02 ":zirconium.libera.chat 004 tester zirconium.libera.chat solanum-1.0-dev DGQRSZaghilopsuwz CFILMPQSbcefgijklmnopqrstuvz bkloveqjfI")
+ (0.00 ":zirconium.libera.chat 005 tester ETRACE WHOX FNC MONITOR=100 SAFELIST ELIST=CTU CALLERID=g KNOCK CHANTYPES=# EXCEPTS INVEX CHANMODES=eIbq,k,flj,CFLMPQScgimnprstuz :are supported by this server")
+ (0.03 ":zirconium.libera.chat 005 tester CHANLIMIT=#:250 PREFIX=(ov)@+ MAXLIST=bqeI:100 MODES=4 NETWORK=Libera.Chat STATUSMSG=@+ CASEMAPPING=rfc1459 NICKLEN=16 MAXNICKLEN=16 CHANNELLEN=50 TOPICLEN=390 DEAF=D :are supported by this server")
+ (0.02 ":zirconium.libera.chat 005 tester TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,PRIVMSG:4,NOTICE:4,ACCEPT:,MONITOR: EXTBAN=$,ajrxz CLIENTVER=3.0 :are supported by this server")
+ (0.02 ":zirconium.libera.chat 251 tester :There are 68 users and 37640 invisible on 25 servers")
+ (0.00 ":zirconium.libera.chat 252 tester 36 :IRC Operators online")
+ (0.01 ":zirconium.libera.chat 253 tester 5 :unknown connection(s)")
+ (0.00 ":zirconium.libera.chat 254 tester 19341 :channels formed")
+ (0.01 ":zirconium.libera.chat 255 tester :I have 3321 clients and 1 servers")
+ (0.01 ":zirconium.libera.chat 265 tester 3321 4289 :Current local users 3321, max 4289")
+ (0.00 ":zirconium.libera.chat 266 tester 37708 38929 :Current global users 37708, max 38929")
+ (0.01 ":zirconium.libera.chat 250 tester :Highest connection count: 4290 (4289 clients) (38580 connections received)")
+ (0.21 ":zirconium.libera.chat 375 tester :- zirconium.libera.chat Message of the Day - ")
+ (0.00 ":zirconium.libera.chat 372 tester :- This server provided by Seeweb <https://www.seeweb.it/>")
+ (0.01 ":zirconium.libera.chat 372 tester :- Welcome to Libera Chat, the IRC network for")
+ (0.01 ":zirconium.libera.chat 372 tester :- free & open-source software and peer directed projects.")
+ (0.00 ":zirconium.libera.chat 372 tester :-  ")
+ (0.00 ":zirconium.libera.chat 372 tester :- Use of Libera Chat is governed by our network policies.")
+ (0.00 ":zirconium.libera.chat 372 tester :-  ")
+ (0.01 ":zirconium.libera.chat 372 tester :- Please visit us in #libera for questions and support.")
+ (0.01 ":zirconium.libera.chat 372 tester :-  ")
+ (0.01 ":zirconium.libera.chat 372 tester :- Website and documentation:  https://libera.chat")
+ (0.01 ":zirconium.libera.chat 372 tester :- Webchat:                    https://web.libera.chat")
+ (0.01 ":zirconium.libera.chat 372 tester :- Network policies:           https://libera.chat/policies")
+ (0.01 ":zirconium.libera.chat 372 tester :- Email:                      [email protected]")
+ (0.00 ":zirconium.libera.chat 376 tester :End of /MOTD command."))
+
+((mode-user 1.2 "MODE tester +i")
+ (0.02 ":tester MODE tester :+Zi")
+ (0.02 ":[email protected] NOTICE tester :This nickname is registered. Please choose a different nickname, or identify via \2/msg NickServ IDENTIFY tester <password>\2"))
+
+((privmsg 2 "PRIVMSG NickServ :IDENTIFY changeme")
+ (0.96 ":[email protected] NOTICE tester :You are now identified for \2tester\2.")
+ (0.25 ":[email protected] NOTICE tester :Last login from: \[email protected]/tester\2 on Jun 18 01:15:56 2021 +0000."))
+
+((quit 5 "QUIT :\2ERC\2")
+ (0.19 ":[email protected] QUIT :Client Quit"))
+
+((linger 1 LINGER))
-- 
2.36.1
0026-SQUASH-ME-Add-ERC-test-scenarios-for-erc-cmd-JOIN.patch (text/x-patch, 24.5 KB)
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Thu, 30 Sep 2021 06:29:24 -0700
Subject: [PATCH 26/35] SQUASH-ME: Add ERC test scenarios for erc-cmd-JOIN

DELETE THIS NOTE: The reuse-buffers scenario belongs here because it
indirectly asserts that the changes to erc-cmd-JOIN work as intended.
See note atop the `ert-deftest' and helper.

The assertion involving the presence of an entry for the current user
in a defunct channel buffer has to do with trying to shift to a
cleaner means of checking whether a channel buffer is subscribed
to (whether it's JOINed or PARTed).  The old means of checking,
basically seeing whether `erc-default-target' is non-nil, depends on
`erc-default-recipients', whose purpose has never been well defined.
---
 .../erc-scenarios-base-reuse-buffers.el       | 130 ++++++++++++++++++
 .../erc-scenarios-join-auth-source.el         |  67 +++++++++
 .../reuse-buffers/channel-buffers/barnet.eld  |  68 +++++++++
 .../reuse-buffers/channel-buffers/foonet.eld  |  66 +++++++++
 .../resources/join/auth-source/foonet.eld     |  33 +++++
 5 files changed, 364 insertions(+)
 create mode 100644 test/lisp/erc/erc-scenarios/erc-scenarios-join-auth-source.el
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/reuse-buffers/channel-buffers/barnet.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/base/reuse-buffers/channel-buffers/foonet.eld
 create mode 100644 test/lisp/erc/erc-scenarios/resources/join/auth-source/foonet.eld

diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-base-reuse-buffers.el b/test/lisp/erc/erc-scenarios/erc-scenarios-base-reuse-buffers.el
index dafd4da855..57b0a29c15 100644
--- a/test/lisp/erc/erc-scenarios/erc-scenarios-base-reuse-buffers.el
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-base-reuse-buffers.el
@@ -109,4 +109,134 @@ erc-scenarios-base-reuse-buffers-server-buffers--disabled
           erc-reuse-buffers)
       (erc-scenarios-common--base-reuse-buffers-server-buffers nil))))
 
+;; This also asserts that `erc-cmd-JOIN' is no longer susceptible to a
+;; regression introduced in 28.1 (ERC 5.4) that caused phantom target
+;; buffers of the form target/server to be created via
+;; `switch-to-buffer' ("phantom" because they would go unused").  This
+;; would happen (in place of a JOIN being sent out) when a previously
+;; used (parted) target buffer existed and `erc-reuse-buffers' was
+;; nil.
+;;
+;; Note: All the `erc-get-channel-user' calls have to do with the fact
+;; that `erc-default-target' relies on the ambiguously defined
+;; `erc-default-recipients' (meaning it's overloaded in the sense of
+;; being used both for retrieving a target name and checking if a
+;; channel has been PARTed).  While not ideal, `erc-get-channel-user'
+;; can (also) be used to detect the latter.
+
+(defun erc-scenarios-common--base-reuse-buffers-channel-buffers (port)
+  "The option `erc-reuse-buffers' is still respected when nil.
+Adapted from scenario clash-of-chans/uniquify described in Bug#48598:
+28.0.50; buffer-naming collisions involving bouncers in ERC."
+  (let* ((expect (erc-d-t-make-expecter))
+         (server-buffer-foo
+          (get-buffer (format "127.0.0.1:%d/127.0.0.1" port)))
+         (server-buffer-bar
+          (get-buffer (format "127.0.0.1:%d/127.0.0.1<2>" port)))
+         (chan-buffer-foo (get-buffer "#chan/127.0.0.1"))
+         (chan-buffer-bar (get-buffer "#chan/127.0.0.1<2>"))
+         (server-process-foo (with-current-buffer server-buffer-foo
+                               erc-server-process))
+         (server-process-bar (with-current-buffer server-buffer-bar
+                               erc-server-process)))
+
+    (ert-info ("Unique #chan buffers exist")
+      (let ((chan-bufs (erc-scenarios-common-buflist "#chan"))
+            (known (list chan-buffer-bar chan-buffer-foo)))
+        (should (memq (pop chan-bufs) known))
+        (should (memq (pop chan-bufs) known))
+        (should-not chan-bufs)))
+
+    (ert-info ("#chan@foonet is exclusive and not contaminated")
+      (with-current-buffer chan-buffer-foo
+        (funcall expect 1 "<bob>")
+        (erc-d-t-absent-for 0.1 "<joe>")
+        (funcall expect 1 "strength to climb")
+        (should (eq erc-server-process server-process-foo))))
+
+    (ert-info ("#chan@barnet is exclusive and not contaminated")
+      (with-current-buffer chan-buffer-bar
+        (funcall expect 1 "<joe>")
+        (erc-d-t-absent-for 0.1 "<bob>")
+        (funcall expect 1 "the loudest noise")
+        (should (eq erc-server-process server-process-bar))))
+
+    (ert-info ("Part #chan@foonet")
+      (with-current-buffer chan-buffer-foo
+        (erc-d-t-search-for 1 "shake my sword")
+        (erc-cmd-PART "#chan")
+        (funcall expect 3 "You have left channel #chan")
+        (erc-cmd-JOIN "#chan")))
+
+    (ert-info ("Part #chan@barnet")
+      (with-current-buffer chan-buffer-bar
+        (funcall expect 3 "Arm it in rags")
+        (should (erc-get-channel-user (erc-current-nick)))
+        (erc-cmd-PART "#chan")
+        (funcall expect 3 "You have left channel #chan")
+        (should-not (erc-get-channel-user (erc-current-nick)))
+        (erc-cmd-JOIN "#chan")))
+
+    (erc-d-t-wait-for 3 "New unique target buffer for #chan@foonet created"
+      (get-buffer "#chan/127.0.0.1<3>"))
+
+    (ert-info ("Activity continues in new, <n>-suffixed #chan@foonet buffer")
+      (with-current-buffer chan-buffer-foo
+        (should-not (erc-get-channel-user (erc-current-nick))))
+      (with-current-buffer "#chan/127.0.0.1<3>"
+        (should (erc-get-channel-user (erc-current-nick)))
+        (funcall expect 2 "You have joined channel #chan")
+        (funcall expect 2 "#chan was created on")
+        (funcall expect 2 "<alice>")
+        (should (eq erc-server-process server-process-foo))
+        (erc-d-t-absent-for 0.2 "<joe>")))
+
+    (sit-for 3)
+    (erc-d-t-wait-for 5 "New unique target buffer for #chan@barnet created"
+      (get-buffer "#chan/127.0.0.1<4>"))
+
+    (ert-info ("Activity continues in new, <n>-suffixed #chan@barnet buffer")
+      (with-current-buffer chan-buffer-bar
+        (should-not (erc-get-channel-user (erc-current-nick))))
+      (with-current-buffer "#chan/127.0.0.1<4>"
+        (funcall expect 2 "You have joined channel #chan")
+        (funcall expect 1 "Users on #chan: @mike joe tester")
+        (funcall expect 2 "<mike>")
+        (should (eq erc-server-process server-process-bar))
+        (erc-d-t-absent-for 0.2 "<bob>")))
+
+    (ert-info ("Two new chans created for a total of four")
+      (let* ((bufs (erc-scenarios-common-buflist "#chan"))
+             (names (sort (mapcar #'buffer-name bufs) #'string<)))
+        (should
+         (equal names (mapcar (lambda (f) (concat "#chan/127.0.0.1" f))
+                              '("" "<2>" "<3>" "<4>"))))))
+
+    (ert-info ("All output sent")
+      (with-current-buffer "#chan/127.0.0.1<3>"
+        (while (accept-process-output server-process-foo))
+        (funcall expect 3 "most lively"))
+      (with-current-buffer "#chan/127.0.0.1<4>"
+        (while (accept-process-output server-process-bar))
+        (funcall expect 3 "soul black")))
+
+    ;; TODO ensure the exact <N>'s aren't reassigned during killing as
+    ;; they are when the option is on.
+    (ert-info ("Buffers are exempt from shortening")
+      (kill-buffer "#chan/127.0.0.1<4>")
+      (kill-buffer "#chan/127.0.0.1<3>")
+      (kill-buffer chan-buffer-bar)
+      (should-not (get-buffer "#chan"))
+      (should chan-buffer-foo))))
+
+(ert-deftest erc-scenarios-base-reuse-buffers-channel-buffers--disabled ()
+  :tags '(:expensive-test)
+  (with-suppressed-warnings ((obsolete erc-reuse-buffers))
+    (should erc-reuse-buffers)
+    (let ((erc-scenarios-common-dialog "base/reuse-buffers/channel-buffers")
+          (erc-server-flood-penalty 0.1)
+          erc-reuse-buffers)
+      (erc-scenarios-common--base-reuse-buffers-server-buffers
+       #'erc-scenarios-common--base-reuse-buffers-channel-buffers))))
+
 ;;; erc-scenarios-base-reuse-buffers.el ends here
diff --git a/test/lisp/erc/erc-scenarios/erc-scenarios-join-auth-source.el b/test/lisp/erc/erc-scenarios/erc-scenarios-join-auth-source.el
new file mode 100644
index 0000000000..94336db07c
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/erc-scenarios-join-auth-source.el
@@ -0,0 +1,67 @@
+;;; erc-scenarios-join-auth-source.el --- join-auth-source scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+;;
+;; This file is part of GNU Emacs.
+;;
+;; This program is free software: you can redistribute it and/or
+;; modify it under the terms of the GNU General Public License as
+;; published by the Free Software Foundation, either version 3 of the
+;; License, or (at your option) any later version.
+;;
+;; This program is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see
+;; <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; TODO add another test with autojoin and channel keys
+
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(ert-deftest erc-scenarios-join-auth-source--network ()
+  :tags '(:expensive-test)
+  (should erc-auth-source-join-function)
+  (erc-scenarios-common-with-cleanup
+      ((entries
+        '("machine 127.0.0.1 port %d login \"#foo\" password spam"
+          "machine irc.foonet.org port %d login tester password fake"
+          "machine irc.foonet.org login \"#spam\" password secret"
+          "machine foonet port %d login dummy password fake"
+          "machine 127.0.0.1 port %d login dummy password changeme"))
+       (erc-scenarios-common-dialog "join/auth-source")
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (erc-d-run "localhost" t 'foonet))
+       (port (process-contact dumb-server :service))
+       (ents (mapcar (lambda (fmt) (format fmt port)) entries))
+       (netrc-file (make-temp-file "auth-source-test" nil nil
+                                   (string-join ents "\n")))
+       (auth-sources (list netrc-file))
+       (auth-source-do-cache nil)
+       (expect (erc-d-t-make-expecter))
+       (erc-scenarios-common-extra-teardown (lambda ()
+                                              (delete-file netrc-file))))
+
+    (ert-info ("Connect without password")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "dummy"
+                                :full-name "dummy")
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))
+        (erc-d-t-wait-for 8 (eq erc-network 'foonet))
+        (funcall expect 10 "user modes")
+        (erc-scenarios-common-say "/JOIN #spam")))
+
+    (ert-info ("Join #spam")
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#spam"))
+        (funcall expect 10 "#spam was created on")))))
+
+;;; erc-scenarios-join-auth-source.el ends here
diff --git a/test/lisp/erc/erc-scenarios/resources/base/reuse-buffers/channel-buffers/barnet.eld b/test/lisp/erc/erc-scenarios/resources/base/reuse-buffers/channel-buffers/barnet.eld
new file mode 100644
index 0000000000..82700c5912
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/reuse-buffers/channel-buffers/barnet.eld
@@ -0,0 +1,68 @@
+;; -*- mode: lisp-data; -*-
+((pass 3 "PASS :barnet:changeme"))
+((nick 3 "NICK tester"))
+((user 3 "USER user 0 * :tester")
+ (0 ":irc.barnet.org 001 tester :Welcome to the barnet IRC Network tester")
+ (0 ":irc.barnet.org 002 tester :Your host is irc.barnet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.barnet.org 003 tester :This server was created Wed, 05 May 2021 09:05:33 UTC")
+ (0 ":irc.barnet.org 004 tester irc.barnet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.barnet.org 005 tester AWAYLEN=390 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# ELIST=U EXCEPTS EXTBAN=,m FORWARD=f INVEX KICKLEN=390 :are supported by this server")
+ (0 ":irc.barnet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=barnet NICKLEN=32 PREFIX=(qaohv)~&@%+ STATUSMSG=~&@%+ TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 TOPICLEN=390 UTF8MAPPING=rfc8265 UTF8ONLY WHOX :are supported by this server")
+ (0 ":irc.barnet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.barnet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.barnet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.barnet.org 254 tester 1 :channels formed")
+ (0 ":irc.barnet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.barnet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.barnet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.barnet.org 422 tester :MOTD File is missing"))
+
+((mode-user 10.2 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":[email protected] JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :joe @mike tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***[email protected] PRIVMSG #chan :Buffer Playback...")
+ (0 ":[email protected] PRIVMSG #chan :[09:09:16] joe: Tush! none but minstrels like of sonneting.")
+ (0 ":[email protected] PRIVMSG #chan :[09:09:19] mike: Prithee, nuncle, be contented; 'tis a naughty night to swim in. Now a little fire in a wide field were like an old lecher's heart; a small spark, all the rest on's body cold. Look! here comes a walking fire.")
+ (0 ":[email protected] PRIVMSG #chan :[09:09:22] joe: My name is Edgar, and thy father's son.")
+ (0 ":[email protected] PRIVMSG #chan :[09:09:26] mike: Good my lord, be good to me; your honour is accounted a merciful man; good my lord.")
+ (0 ":[email protected] PRIVMSG #chan :[09:09:31] joe: Thy child shall live, and I will see it nourish'd.")
+ (0 ":[email protected] PRIVMSG #chan :[09:09:33] mike: Quick, quick; fear nothing; I'll be at thy elbow.")
+ (0 ":***[email protected] PRIVMSG #chan :Playback Complete.")
+ (0 ":irc.barnet.org NOTICE tester :[09:05:35] This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.")
+ (0 ":irc.barnet.org 305 tester :You are no longer marked as being away"))
+
+((mode 3 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620205534")
+ (0.1 ":[email protected] PRIVMSG #chan :joe: That will be given to the loudest noise we make.")
+ (0.1 ":[email protected] PRIVMSG #chan :mike: If it please your honour, I am the poor duke's constable, and my name is Elbow: I do lean upon justice, sir; and do bring in here before your good honour two notorious benefactors.")
+ (0.1 ":[email protected] PRIVMSG #chan :joe: Following the signs, woo'd but the sign of she.")
+ (0.5 ":[email protected] PRIVMSG #chan :mike: That, sir, which I will not report after her.")
+ (0.1 ":[email protected] PRIVMSG #chan :joe: Boyet, prepare: I will away to-night.")
+ (0.1 ":[email protected] PRIVMSG #chan :mike: If the man be a bachelor, sir, I can; but if he be a married man, he is his wife's head, and I can never cut off a woman's head.")
+ (0.1 ":[email protected] PRIVMSG #chan :joe: Thyself upon thy virtues, they on thee.")
+ (0.1 ":[email protected] PRIVMSG #chan :mike: Arm it in rags, a pigmy's straw doth pierce it."))
+
+((part 5.1 "PART #chan :" quit)
+ (0 ":[email protected] PART #chan :" quit))
+
+((join 10.1 "JOIN #chan")
+ (0 ":[email protected] JOIN #chan")
+ (0 ":irc.barnet.org 353 tester = #chan :@mike joe tester")
+ (0 ":irc.barnet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":[email protected] PRIVMSG #chan :tester, welcome!")
+ (0 ":[email protected] PRIVMSG #chan :tester, welcome!"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.barnet.org 324 tester #chan +nt")
+ (0 ":irc.barnet.org 329 tester #chan 1620205534")
+ (0.1 ":[email protected] PRIVMSG #chan :joe: Chi non te vede, non te pretia.")
+ (0.1 ":[email protected] PRIVMSG #chan :mike: Well, if ever thou dost fall from this faith, thou wilt prove a notable argument.")
+ (0.1 ":[email protected] PRIVMSG #chan :joe: Of heavenly oaths, vow'd with integrity.")
+ (0.1 ":[email protected] PRIVMSG #chan :mike: These herblets shall, which we upon you strew.")
+ (0.1 ":[email protected] PRIVMSG #chan :joe: Aaron will have his soul black like his face."))
+
+((linger 0.5 LINGER))
diff --git a/test/lisp/erc/erc-scenarios/resources/base/reuse-buffers/channel-buffers/foonet.eld b/test/lisp/erc/erc-scenarios/resources/base/reuse-buffers/channel-buffers/foonet.eld
new file mode 100644
index 0000000000..a11cfac2e7
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/base/reuse-buffers/channel-buffers/foonet.eld
@@ -0,0 +1,66 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :foonet:changeme"))
+((nick 1 "NICK tester"))
+((user 1 "USER user 0 * :tester")
+ (0 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version oragono-2.6.0-7481bf0385b95b16")
+ (0 ":irc.foonet.org 003 tester :This server was created Wed, 05 May 2021 09:05:34 UTC")
+ (0 ":irc.foonet.org 004 tester irc.foonet.org oragono-2.6.0-7481bf0385b95b16 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0 ":irc.foonet.org 005 tester AWAYLEN=390 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# ELIST=U EXCEPTS EXTBAN=,m FORWARD=f INVEX KICKLEN=390 :are supported by this server")
+ (0 ":irc.foonet.org 005 tester MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=foonet NICKLEN=32 PREFIX=(qaohv)~&@%+ STATUSMSG=~&@%+ TARGMAX=NAMES:1,LIST:1,KICK:1,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 TOPICLEN=390 UTF8MAPPING=rfc8265 UTF8ONLY WHOX :are supported by this server")
+ (0 ":irc.foonet.org 005 tester draft/CHATHISTORY=100 :are supported by this server")
+ (0 ":irc.foonet.org 251 tester :There are 0 users and 3 invisible on 1 server(s)")
+ (0 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0 ":irc.foonet.org 254 tester 1 :channels formed")
+ (0 ":irc.foonet.org 255 tester :I have 3 clients and 0 servers")
+ (0 ":irc.foonet.org 265 tester 3 3 :Current local users 3, max 3")
+ (0 ":irc.foonet.org 266 tester 3 3 :Current global users 3, max 3")
+ (0 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 12 "MODE tester +i")
+ ;; No mode answer
+ (0 ":irc.znc.in 306 tester :You have been marked as being away")
+ (0 ":[email protected] JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :alice @bob tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of /NAMES list.")
+ (0 ":***[email protected] PRIVMSG #chan :Buffer Playback...")
+ (0 ":[email protected] PRIVMSG #chan :[09:07:19] bob: Is this; she hath bought the name of whore thus dearly.")
+ (0 ":[email protected] PRIVMSG #chan :[09:07:24] alice: He sent to me, sir,Here he comes.")
+ (0 ":[email protected] PRIVMSG #chan :[09:07:26] bob: Till I torment thee for this injury.")
+ (0 ":[email protected] PRIVMSG #chan :[09:07:29] alice: There's an Italian come; and 'tis thought, one of Leonatus' friends.")
+ (0 ":[email protected] PRIVMSG #chan :[09:09:33] bob: Ay, and the particular confirmations, point from point, to the full arming of the verity.")
+ (0 ":[email protected] PRIVMSG #chan :[09:09:35] alice: Kneel in the streets and beg for grace in vain.")
+ (0 ":***[email protected] PRIVMSG #chan :Playback Complete.")
+ (0 ":irc.foonet.org NOTICE tester :[09:06:05] This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.")
+ (0 ":irc.foonet.org 305 tester :You are no longer marked as being away"))
+
+((mode 10 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620205534")
+ (0.5 ":[email protected] PRIVMSG #chan :alice: Nor I no strength to climb without thy help.")
+ (0.1 ":[email protected] PRIVMSG #chan :bob: Nothing, but let him have thanks. Demand of him my condition, and what credit I have with the duke.")
+ (0.1 ":[email protected] PRIVMSG #chan :alice: Show me this piece. I am joyful of your sights.")
+ (0.2 ":[email protected] PRIVMSG #chan :bob: Whilst I can shake my sword or hear the drum."))
+
+((part 5 "PART #chan :" quit)
+ (0 ":[email protected] PART #chan :" quit))
+
+((join 10 "JOIN #chan")
+ (0 ":[email protected] JOIN #chan")
+ (0 ":irc.foonet.org 353 tester = #chan :@bob alice tester")
+ (0 ":irc.foonet.org 366 tester #chan :End of NAMES list")
+ (0.1 ":[email protected] PRIVMSG #chan :tester, welcome!")
+ (0 ":[email protected] PRIVMSG #chan :tester, welcome!"))
+
+((mode 1 "MODE #chan")
+ (0 ":irc.foonet.org 324 tester #chan +nt")
+ (0 ":irc.foonet.org 329 tester #chan 1620205534")
+ (0.1 ":[email protected] PRIVMSG #chan :alice: Thou desirest me to stop in my tale against the hair.")
+ (0.1 ":[email protected] PRIVMSG #chan :bob: And dar'st not stand, nor look me in the face.")
+ (0.1 ":[email protected] PRIVMSG #chan :alice: It should not be, by the persuasion of his new feasting.")
+ (0.1 ":[email protected] PRIVMSG #chan :bob: It was not given me, nor I did not buy it.")
+ (0.1 ":[email protected] PRIVMSG #chan :alice: He that would vouch it in any place but here.")
+ (0.1 ":[email protected] PRIVMSG #chan :bob: In everything I wait upon his will.")
+ (0.1 ":[email protected] PRIVMSG #chan :alice: Thou counterfeit'st most lively."))
+
+((linger 8 LINGER))
diff --git a/test/lisp/erc/erc-scenarios/resources/join/auth-source/foonet.eld b/test/lisp/erc/erc-scenarios/resources/join/auth-source/foonet.eld
new file mode 100644
index 0000000000..f2d7715d1e
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios/resources/join/auth-source/foonet.eld
@@ -0,0 +1,33 @@
+;; -*- mode: lisp-data; -*-
+((pass 1 "PASS :changeme"))
+((nick 1 "NICK dummy"))
+((user 1 "USER user 0 * :dummy")
+ (0.00 ":irc.foonet.org 001 dummy :Welcome to the foonet IRC Network dummy")
+ (0.01 ":irc.foonet.org 002 dummy :Your host is irc.foonet.org, running version ergo-v2.8.0")
+ (0.00 ":irc.foonet.org 003 dummy :This server was created Tue, 24 May 2022 05:28:42 UTC")
+ (0.00 ":irc.foonet.org 004 dummy irc.foonet.org ergo-v2.8.0 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.00 ":irc.foonet.org 005 dummy AWAYLEN=390 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# ELIST=U EXCEPTS EXTBAN=,m FORWARD=f INVEX KICKLEN=390 :are supported by this server")
+ (0.01 ":irc.foonet.org 005 dummy MAXLIST=beI:60 MAXTARGETS=4 MODES MONITOR=100 NETWORK=foonet NICKLEN=32 PREFIX=(qaohv)~&@%+ STATUSMSG=~&@%+ TARGMAX=NAMES:1,LIST:1,KICK:,WHOIS:1,USERHOST:10,PRIVMSG:4,TAGMSG:4,NOTICE:4,MONITOR:100 TOPICLEN=390 UTF8MAPPING=rfc8265 UTF8ONLY WHOX :are supported by this server")
+ (0.01 ":irc.foonet.org 005 dummy draft/CHATHISTORY=100 :are supported by this server")
+ (0.00 ":irc.foonet.org 251 dummy :There are 0 users and 4 invisible on 1 server(s)")
+ (0.00 ":irc.foonet.org 252 dummy 0 :IRC Operators online")
+ (0.00 ":irc.foonet.org 253 dummy 0 :unregistered connections")
+ (0.00 ":irc.foonet.org 254 dummy 2 :channels formed")
+ (0.00 ":irc.foonet.org 255 dummy :I have 4 clients and 0 servers")
+ (0.00 ":irc.foonet.org 265 dummy 4 4 :Current local users 4, max 4")
+ (0.00 ":irc.foonet.org 266 dummy 4 4 :Current global users 4, max 4")
+ (0.00 ":irc.foonet.org 422 dummy :MOTD File is missing"))
+
+((mode 1 "MODE dummy +i")
+ (0.00 ":irc.foonet.org 221 dummy +i")
+ (0.00 ":irc.foonet.org NOTICE dummy :This server is in debug mode and is logging all user I/O. If you do not wish for everything you send to be readable by the server owner(s), please disconnect.")
+ (0.02 ":irc.foonet.org 221 dummy +i"))
+
+((join 6.47 "JOIN #spam secret")
+ (0.03 ":[email protected] JOIN #spam"))
+
+((mode 1 "MODE #spam")
+ (0.01 ":irc.foonet.org 353 dummy = #spam :~tester dummy")
+ (0.00 ":irc.foonet.org 366 dummy #spam :End of NAMES list")
+ (0.01 ":irc.foonet.org 324 dummy #spam +knt secret")
+ (0.03 ":irc.foonet.org 329 dummy #spam 1653370308"))
-- 
2.36.1
0027-Update-ERC-s-Info-doc-with-network-ID-related-change.patch (text/x-patch, 16 KB)
From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <[email protected]>
Date: Fri, 18 Jun 2021 04:25:44 -0700
Subject: [PATCH 27/35] Update ERC's Info doc with network-ID related changes

* doc/misc/erc.texi: Update the `erc' and `erc-tls' entry-point
sections with the new :id keyword parameter.  Expand the auth-info
related information in the passwords section.  Remove all mention of
the variable `erc-rename-buffers', whose "on" behavior has been made
permanent.

* etc/ERC-NEWS: Add new section for future 5.5 release.
---
 doc/misc/erc.texi | 192 ++++++++++++++++++++++++++++++++++++++--------
 etc/ERC-NEWS      |  86 +++++++++++++++++++++
 2 files changed, 244 insertions(+), 34 deletions(-)

diff --git a/doc/misc/erc.texi b/doc/misc/erc.texi
index b9297738ea..6daa54d956 100644
--- a/doc/misc/erc.texi
+++ b/doc/misc/erc.texi
@@ -545,20 +545,26 @@ Connecting
 @item @var{server}
 @item @var{port}
 @item @var{nick}
+@item @var{user}
 @item @var{password}
 @item @var{full-name}
+@item @var{id}
 @end itemize
 
-That is, if called with the following arguments, @var{server} and
-@var{full-name} will be set to those values, whereas
-@code{erc-compute-port} and @code{erc-compute-nick} will be invoked
-for the values of the other parameters.
+For example, calling the command like so
 
-@example
+@example lisp
 (erc :server "irc.libera.chat" :full-name "J. Random Hacker")
 @end example
+
+sets @var{server} and @var{full-name} directly while leaving the rest
+up to functions like @code{erc-compute-port}.  Note that some
+arguments can't be specified interactively.  @var{id}, in particular,
+is rarely needed (@pxref{Network Identifier}).
+
 @end defun
 
+@noindent
 To connect securely over an encrypted TLS connection, use @kbd{M-x
 erc-tls}.
 
@@ -570,21 +576,24 @@ Connecting
 @item @var{server}
 @item @var{port}
 @item @var{nick}
+@item @var{user}
 @item @var{password}
 @item @var{full-name}
+@item @var{id}
 @item @var{client-certificate}
 @end itemize
 
-That is, if called with the following arguments, @var{server} and
-@var{full-name} will be set to those values, whereas
-@code{erc-compute-port} and @code{erc-compute-nick} will be invoked
-for the values of the other parameters, and @code{client-certificate}
-will be @code{nil}.
+That is, if called in the following manner
 
-@example
+@example lisp
 (erc-tls :server "irc.libera.chat" :full-name "J. Random Hacker")
 @end example
 
+the command will set @var{server} and @var{full-name} accordingly,
+while helpers, like @code{erc-compute-nick}, will determine other
+parameters, and some, like @code{client-certificate}, will just be
+@code{nil}.
+
 To use a certificate with @code{erc-tls}, specify the optional
 @var{client-certificate} keyword argument, whose value should be as
 described in the documentation of @code{open-network-stream}: if
@@ -719,29 +728,134 @@ Connecting
 You can manually set another nickname with the /NICK command.
 @end defopt
 
+@subheading User
+@defun erc-compute-user &optional user
+Determine a suitable value to send for the first argument to the
+opening @samp{USER} IRC command by consulting the following sources:
+
+@itemize
+@item @var{user}, the argument passed to this function
+@item The option @code{erc-email-userid}, assuming @code{erc-anonymous-login}
+is non-@code{nil}
+@item The result of calling the function @code{user-login-name}
+@end itemize
+
+@end defun
+
+@defopt erc-email-userid
+A permanent username value to send for all connections.  It should be
+a string abiding by the rules of the network.
+@end defopt
+
 @subheading Password
 @cindex password
 
 @defopt erc-prompt-for-password
-If non-@code{nil} (the default), @kbd{M-x erc} prompts for a password.
+If non-@code{nil} (the default), @kbd{M-x erc} prompts for a server
+password.  This only affects interactive invocations of @code{erc} and
+@code{erc-tls}.
 @end defopt
 
+@noindent
 If you prefer, you can set this option to @code{nil} and use the
 @code{auth-source} mechanism to store your password.  For instance, if
-you use @file{~/.authinfo} as your auth-source backend, then put
+the option @code{auth-sources} contains @file{~/.authinfo}, put
 something like the following in that file:
 
 @example
-machine irc.example.net login "#fsf" password sEcReT
+machine irc.example.net login mynick password sEcReT
+@end example
+
+@noindent
+For server passwords, that is, passwords sent for the IRC @samp{PASS}
+command, the @samp{host} field, here @code{machine irc.example.net},
+corresponds to the @var{server} parameter used by @code{erc} and
+@code{erc-tls}.  Unfortunately, specifying a network, like
+@samp{Libera.Chat}, or a specific network server, like
+@samp{platinum.libera.chat}, won't work OOTB for looking up a server
+password because such information isn't available during opening
+introductions.  Actually, ERC @emph{can} find entries with arbitrary
+@samp{host} values for any context, including server passwords, but
+that requires messing with the more advanced options below.
+
+If ERC can't find a suitable server password, it'll just skip the IRC
+@samp{PASS} command altogether, something users may want when using
+CertFP or engaging NickServ via ERC's ``services'' module.  If that
+sounds like you, you can also set the option
+@code{erc-auth-source-server-function} to @code{nil} to skip
+server-passwork lookup for all servers.  Note that some networks and
+IRCds may accept account-services authentication via server password
+using the nonstandard ``mynick:sEcReT'' convention.
+
+As just mentioned, you can also use @code{auth-source} to authenticate
+to account services the traditional way, through a bot called
+``NickServ''.  To tell ERC to do that, set
+@code{erc-use-auth-source-for-nickserv-password} to @code{t}.  For
+these and most other queries, entries featuring custom identifiers and
+networks are matched first, followed by network-specific servers and
+dialed endpoints (typically, the @var{SERVER} passed to
+@code{erc}). The following netrc-style entries appear in order of
+precedence:
+
+@example
+machine Libera/cellphone login MyNick password sEcReT
+machine Libera.Chat login MyNick password sEcReT
+machine zirconium.libera.chat login MyNick password sEcReT
+machine irc.libera.chat login MyNick password sEcReT
 @end example
 
 @noindent
-ERC also consults @code{auth-source} to find any channel keys required
-for the channels that you wish to autojoin, as specified by the
-variable @code{erc-autojoin-channels-alist}.
+Remember that field labels vary per backend, so @samp{machine} (in
+netrc's case) maps to auth-source's generalized notion of a host,
+hence the @samp{:host} keyword property.  Also, be sure and mind the
+syntax of your chosen backend medium.  For example, always quote
+channel names in a netrc file.
+
+If this all seems overly nuanced or just plain doesn't appeal to you,
+see options @code{erc-auth-source-services-function} and friends just
+below.  These let you query auth-source your way.  Most users can
+simply ignore the passed-in arguments and get by with something like
+the following:
 
-For more details, @pxref{Top,,auth-source, auth, Emacs auth-source Library}.
+@lisp
+(defun my-fancy-auth-source-func (&rest _)
+  (let* ((host (read-string "host: " nil nil "default"))
+         (pass (auth-source-pick-first-password :host host)))
+    (if (and pass (string-search "libera" host))
+        (concat "MyNick:" pass)
+      pass)))
+@end lisp
+
+Lastly, ERC also consults @code{auth-source} to find ``keys'' that may
+be required by certain channels you join.  When modifying a
+traditional @code{auth-source} entry for this purpose, put the channel
+name in the @samp{user} field (for example, @samp{login "#fsf"}, in
+netrc's case). The actual key goes in the @samp{password} (or
+@samp{secret}) field.
+
+@noindent
+For details, @pxref{Top,,auth-source, auth, Emacs auth-source Library}.
 
+@defopt erc-auth-source-server-function
+@end defopt
+@defopt erc-auth-source-services-function
+@end defopt
+@defopt erc-auth-source-join-function
+
+ERC calls these functions with keyword arguments recognized by
+@code{auth-source-search}, namely, those deemed most relevant to the
+current context, if any.  For example, with NickServ queries,
+@code{:user} is the ``desired'' nickname rather than the current one.
+Generalized names, like @code{:user} and @code{:host}, are always used
+over back-end specific ones, like @code{:login} or @code{:machine}.
+ERC expects a string to use as the secret or nil, if the search fails.
+
+The default value for all three options is the function
+@code{erc-auth-source-search}.  It tries to merge relevant contextual
+params with those provided or discovered from the logical connection
+or the underlying transport.  Some auth-source back ends may not be
+compatible; netrc, plstore, json, and secrets are currently supported.
+@end defopt
 
 @subheading Full name
 
@@ -766,6 +880,31 @@ Connecting
 This can be either a string or a function to call.
 @end defopt
 
+
+@subheading ID
+@anchor{Network Identifier}
+
+ERC uses an abstract designation called a @dfn{network context
+identifier} for referring to a connection internally.  While normally
+derived from a combination of logical and physical connection
+parameters, an ID can also be explicitly provided via an entry-point
+command (like @code{erc-tls}). Use this in rare situations where ERC
+would otherwise have trouble discerning between connections.
+
+One such situation might arise when using multiple connections to the
+same network with the same nick but different (nonstandard) "device"
+identifiers, which some bouncers may support.  Another might be when
+mimicking the experience offered by popular standalone clients, which
+normally offer ``named'' persistent configurations with server buffers
+reflecting those names.  Yet another use case might involve
+third-party code needing to identify a connection unequivocally but in
+a human-friendly way suitable for UI components.
+
+When providing an ID as an entry-point argument, strings and symbols
+make the most sense, but any reasonably printable object is
+acceptable.
+
+
 @node Sample Configuration
 @section Sample Configuration
 @cindex configuration, sample
@@ -827,12 +966,6 @@ Sample Configuration
 (setq erc-autojoin-channels-alist
       '(("Libera.Chat" "#emacs" "#erc")))
 
-;; Rename server buffers to reflect the current network name instead
-;; of SERVER:PORT (e.g., "Libera.Chat" instead of
-;; "irc.libera.chat:6667").  This is useful when using a bouncer like
-;; ZNC where you have multiple connections to the same server.
-(setq erc-rename-buffers t)
-
 ;; Interpret mIRC-style color commands in IRC chats
 (setq erc-interpret-mirc-color t)
 
@@ -891,15 +1024,6 @@ Options
 nickname is considered a lurker.
 @end defopt
 
-@defopt erc-rename-buffers
-If non, @code{nil}, this will rename server buffers to reflect the
-current network name instead of IP:PORT
-
-@example
-(setq erc-rename-buffers t)
-@end example
-@end defopt
-
 @node Getting Help and Reporting Bugs
 @chapter Getting Help and Reporting Bugs
 @cindex help, getting
@@ -924,7 +1048,7 @@ Getting Help and Reporting Bugs
 questions.
 
 @item
-To report a bug in ERC, use @kbd{M-x report-emacs-bug}.
+To report a bug in ERC, use @kbd{M-x erc-bug}.
 
 @end itemize
 
diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS
index bdcd943c37..0ac3df1ba1 100644
--- a/etc/ERC-NEWS
+++ b/etc/ERC-NEWS
@@ -11,6 +11,92 @@ This file is about changes in ERC, the powerful, modular, and
 extensible IRC (Internet Relay Chat) client distributed with
 GNU Emacs since Emacs version 22.1.
 
+
+* Changes in ERC 5.5
+
+** Smarter buffer naming for withstanding collisions.
+ERC buffers now remain tied to their logical network contexts, even
+while offline.  These associations can survive regional server changes
+and the intercession of proxies.  As has long been practiced in other
+areas of Emacs, "uniquified" buffer renaming prevents collisions
+between buffers of different contexts.  Potential avenues for
+confusion remain but will die out with the adoption of emerging
+protocol extensions.
+
+** Option 'erc-rename-buffers' deprecated.
+The promise of its old "on" state are now fully realized and enabled
+permanently by default.  Its old behavior when disabled has been
+preserved and will remain available (with warnings) for years to come.
+
+** Option 'erc-reuse-buffers' deprecated.
+This ancient option has been a constant source of confusion, as
+exhibited most recently when its "disabled" meaning was partially
+inverted.  Introduced in ERC 5.4 (Emacs 28.1), this regression saw
+existing channel buffers transparently reassociated instead of created
+anew.  The pre-5.4 "disabled" behavior has been restored and will
+remain accessible for the foreseeable future, warts and all (e.g.,
+with its often superfluous "/DIALED-HOST" suffixing always present).
+
+** Convention adopted for auditioning edge functionality.
+Objects bearing names like "erc--X-foo" or "erc-somemod--X-foo" will
+be offered as trial balloons for potential export and as stopgaps for
+overdue but immature fixes and features.  (If you dare try them,
+please give feedback!)  This move comes out of desperation as we focus
+on much needed internal remediation while trying to meet baseline
+expectations in the face of mounting pressure from networks and users.
+
+** Tighter auth-source integration with bigger changes on the horizon.
+The days of hit-and-miss auth-source queries are hopefully behind us.
+With the overhaul of the services module temporarily shelved and the
+transition to SASL-based authentication still underway, users may feel
+left in the lurch to endure yet another release cycle of backtick
+hell.  For some, auth-source may provide a workaround in the form of
+nonstandard server passwords.  See the "Connection" node in the manual
+under the subheading "Password".  If you require SASL immediately,
+please participate in ERC development by volunteering to try (and give
+feedback on) edge features, one of which is SASL.  All known external
+offerings, past and present, are makeshift efforts whose use is
+discouraged.  This includes the instructions on Libera.Chat's website.
+
+** Username argument for entry point commands.
+Commands 'erc' and 'erc-tls' now accept a ':user' keyword argument,
+which, when present, becomes the first argument passed to the "USER"
+IRC command.  The traditional way of setting this globally, via
+'erc-email-userid', is still honored.
+
+** Additional display options for updated buffers.
+Additional flexibility is now available for controlling the behavior
+of newly created target buffers, especially during reconnection.
+
+** Miscellaneous behavioral changes impacting the user experience.
+A bug has been fixed that saw prompts being mangled, doubled, or
+erased in server buffers upon disconnection.  Instead, prompts now
+collapse into an alternate form designated by the option
+'erc-prompt-hidden'.  Behavior differs for query and channel buffers
+but can be fine-tuned via the repurposed, formerly abandoned option
+'erc-hide-prompt'.
+
+A bug has been fixed affecting users of the Soju bouncer: outgoing
+messages during periods of heavy traffic no longer disappear.
+
+Although rare, server passwords containing white space are now handled
+correctly.
+
+** Miscellaneous behavioral changes in the library API.
+The function 'erc-network' always returns non-nil in server and target
+buffers belonging to a successfully established IRC connection, even
+after that connection is closed.  In 5.4, support for network symbols
+as keys was added for 'erc-autojoin-channels-alist'.  This has been
+extended to include explicit symbols passed to 'erc-tls' and 'erc' as
+so-called network-context identifiers via a new ':id' keyword.  The
+latter carries wider significance beyond autojoin and can be used for
+uniquely and unequivocally identifying a connection.  The function
+'erc-auto-query', unused internally, and basically inscrutable when
+read, has been deprecated with no public replacement.  Which raises
+the issue: if you use ERC as a library and need something that's only
+offered internally, please lobby for its exporting by writing to
[email protected].
+
 
 * Changes in ERC 5.4.1
 
-- 
2.36.1
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.