unofficial mirror of bug-gnu-emacs@gnu.org 
 help / color / mirror / code / Atom feed
* bug#70928: 30.0.50; ERC 5.6: Reuse query buffers for round-trip nick changes in ERC
@ 2024-05-14  1:00 J.P.
  2024-05-25  3:13 ` J.P.
  0 siblings, 1 reply; 3+ messages in thread
From: J.P. @ 2024-05-14  1:00 UTC (permalink / raw)
  To: 70928; +Cc: emacs-erc

[-- Attachment #1: Type: text/plain, Size: 5447 bytes --]

From emacs -Q

  1. Start a new session, connecting as nick A
  2. Start another session, connecting as nick B
  3. As nick B, open a query with A, say something, and quit
  4. Start another session, connecting as nick C
  5. As nick C, open a query with A, say something, then issue a /nick B
  6. As B, open a query with A, say something
  7. In A's session, notice B's new query buffer is named B<2>

To put it another way: someone you have a query with disconnects and
reconnects under a new nick, perhaps due to their client appending a
backtick or an underscore. They then engage you in another query before
renicking to their original nick. Weechat and others deal with this by
reusing existing query buffers, but ERC has always added a uniquifying
suffix of the form A<2> to the new, renicked query buffer [1].

While it's true we can't technically be certain revived queries are
being piloted by the same individuals (at least not until ERC becomes
account aware), enough folks have complained about this over the years
that I think it's legitimate to disregard that uncertainty by default.
And though we're not obliged to address this for this release in
particular, I do think it's worth doing so soonish, especially given
that bug#48598 (ERC 5.5) was supposed to do away with these buffer
association issues. The second of the attached patches attempts to get
us closer to making good on that.

Thanks.

[1] Note that in the scenario above, attempting to "merge" buffers B and
    C, like ERC does after changing its own nick, may seem desirable,
    but it's tricky business because timelines may be interleaved rather
    than simply overlap at a single boundary interval. This is one of
    the many things a "backing store" or "replay" module could help
    solve, at least in cases brought about by unwanted backtick
    suffixing. A suitable back end would allow us to perform the merge
    in-store, perhaps ephemerally, and re-ingest it as clean, simulated
    input.


In GNU Emacs 30.0.50 (build 3, x86_64-pc-linux-gnu, GTK+ Version
 3.24.41, cairo version 1.18.0) of 2024-05-08 built on localhost
Repository revision: 36c68e7e34df996bbde4cc82c04ea1619349b64a
Repository branch: master
Windowing system distributor 'The X.Org Foundation', version 11.0.12302004
System Description: Fedora Linux 39 (Workstation Edition)

Configured using:
 'configure --enable-check-lisp-object-type --enable-checking=yes,glyphs
 'CFLAGS=-O0 -g3'
 PKG_CONFIG_PATH=:/usr/lib64/pkgconfig:/usr/share/pkgconfig'

Configured features:
ACL CAIRO DBUS FREETYPE GIF GLIB GMP GNUTLS GPM GSETTINGS HARFBUZZ JPEG
LCMS2 LIBOTF LIBSELINUX LIBSYSTEMD LIBXML2 M17N_FLT MODULES NATIVE_COMP
NOTIFY INOTIFY PDUMPER PNG RSVG SECCOMP SOUND SQLITE3 THREADS TIFF
TOOLKIT_SCROLL_BARS WEBP X11 XDBE XIM XINPUT2 XPM GTK3 ZLIB

Important settings:
  value of $LANG: en_US.UTF-8
  value of $XMODIFIERS: @im=ibus
  locale-coding-system: utf-8-unix

Major mode: Lisp Interaction

Minor modes in effect:
  tooltip-mode: t
  global-eldoc-mode: t
  eldoc-mode: t
  show-paren-mode: t
  electric-indent-mode: t
  mouse-wheel-mode: t
  tool-bar-mode: t
  menu-bar-mode: t
  file-name-shadow-mode: t
  global-font-lock-mode: t
  font-lock-mode: t
  blink-cursor-mode: t
  minibuffer-regexp-mode: t
  line-number-mode: t
  indent-tabs-mode: t
  transient-mark-mode: t
  auto-composition-mode: t
  auto-encryption-mode: t
  auto-compression-mode: t

Load-path shadows:
None found.

Features:
(shadow sort mail-extr emacsbug message mailcap yank-media puny dired
dired-loaddefs rfc822 mml mml-sec epa derived epg rfc6068 epg-config
gnus-util time-date mm-decode mm-bodies mm-encode mail-parse rfc2231
mailabbrev gmm-utils mailheader sendmail rfc2047 rfc2045 ietf-drums
mm-util mail-prsvr mail-utils compile text-property-search comint
ansi-osc ansi-color ring comp-run comp-common rx erc auth-source cl-seq
eieio eieio-core cl-macs icons password-cache json subr-x map
format-spec cl-loaddefs cl-lib erc-backend erc-networks byte-opt gv
bytecomp byte-compile erc-common erc-compat compat erc-loaddefs rmc
iso-transl tooltip cconv eldoc paren electric uniquify ediff-hook
vc-hooks lisp-float-type elisp-mode mwheel term/x-win x-win
term/common-win x-dnd touch-screen tool-bar dnd fontset image regexp-opt
fringe tabulated-list replace newcomment text-mode lisp-mode prog-mode
register page tab-bar menu-bar rfn-eshadow isearch easymenu timer select
scroll-bar mouse jit-lock font-lock syntax font-core term/tty-colors
frame minibuffer nadvice seq simple cl-generic indonesian philippine
cham georgian utf-8-lang misc-lang vietnamese tibetan thai tai-viet lao
korean japanese eucjp-ms cp51932 hebrew greek romanian slovak czech
european ethiopic indian cyrillic chinese composite emoji-zwj charscript
charprop case-table epa-hook jka-cmpr-hook help abbrev obarray oclosure
cl-preloaded button loaddefs theme-loaddefs faces cus-face macroexp
files window text-properties overlay sha1 md5 base64 format env
code-pages mule custom widget keymap hashtable-print-readable backquote
threads dbusbind inotify lcms2 dynamic-setting system-font-setting
font-render-setting cairo gtk x-toolkit xinput2 x multi-tty move-toolbar
make-network-process native-compile emacs)

Memory information:
((conses 16 91614 10878) (symbols 48 9861 0) (strings 32 26671 5011)
 (string-bytes 1 770713) (vectors 16 17713)
 (vector-slots 8 189327 10467) (floats 8 26 13) (intervals 56 323 0)
 (buffers 984 11))


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-5.6-Reconcile-erc-stamp-date-stamps-when-merging-buf.patch --]
[-- Type: text/x-patch, Size: 26166 bytes --]

From 94b5f02dc92b6600f14ab207464558c5de8969d8 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Wed, 8 May 2024 19:03:58 -0700
Subject: [PATCH 1/2] [5.6] Reconcile erc-stamp--date-stamps when merging
 buffers

* etc/ERC-NEWS: Mention new face `erc-info'.
* lisp/erc/erc-button.el (erc-button-add-buttons): Skip buttonization
when the "msg prop" `erc--skip' is present and contains the symbol
`button'.
* lisp/erc/erc-networks.el (erc--insert-admin-message-before): Forward
declaration.
(erc-networks--insert-transplanted-content)
(erc-networks--transplant-buffer-content): Rename former to latter.
Change signature to take source and destination buffers as parameters.
(erc-networks--transplant-target-buffer-function): New function-valued
variable.
(erc-networks--reclaim-orphaned-target-buffers): Call
`erc-networks--transplant-target-buffer-function' to handle transplant
business.
(erc-networks--copy-over-server-buffer-contents): Pass old and new
buffers to `erc-networks--insert-transplanted-content'.
* lisp/erc/erc-stamp.el
(erc-stamp--defer-date-insertion-on-post-modify): Set `fn' slot of
`erc-stamp--date' instance to `ignore' when running the actual
callback in order to conserve a little space.
(erc-stamp--date-mode): Add and remove hook members for
`erc-networks--copy-server-buffer-functions' and
`erc-networks--transplant-target-buffer-function'.
(erc-insert-timestamp-left-and-right): Always clear
`erc-timestamp-last-inserted-right' to ensure a right stamp
accompanies every date stamp.
(erc-stamp--dedupe-date-stamps)
(erc-stamp--dedupe-date-stamps-from-buffer)
(erc-stamp--dedupe-date-stamps-from-target-buffer): New functions.
Date stamps were revamped as part of bug#60936.
* lisp/erc/erc.el (erc-informational): New face.
(erc--insert-admin-message-before): New function to hide some "msg
prop" complexity from "upstream" libraries, like erc-networks and thus
avoid more forward-declarations.  A less smelly approach would be to
devise a general interface owned by such libraries that `erc-mode'
could then hook into on init.
(erc-display-message-highlight): Make face-matching more limber.
(erc-message-english-grafted): New variable.
* test/lisp/erc/erc-networks-tests.el
(erc-networks--rename-server-buffer--no-existing--orphan)
(erc-networks--rename-server-buffer--existing--reuse)
(erc-networks--rename-server-buffer--local-match)
(erc-networks--rename-server-buffer--local-nomatch): Use helper to
initialize markers.
* test/lisp/erc/erc-stamp-tests.el
(erc-stamp--dedupe-date-stamps): New test.
---
 etc/ERC-NEWS                        |   7 ++
 lisp/erc/erc-button.el              |   4 +-
 lisp/erc/erc-networks.el            |  91 ++++++++++++----------
 lisp/erc/erc-stamp.el               |  60 ++++++++++++++
 lisp/erc/erc.el                     |  22 +++++-
 test/lisp/erc/erc-networks-tests.el |   4 +
 test/lisp/erc/erc-stamp-tests.el    | 116 ++++++++++++++++++++++++++++
 7 files changed, 259 insertions(+), 45 deletions(-)

diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS
index b66ea6a7a02..4562e8a8117 100644
--- a/etc/ERC-NEWS
+++ b/etc/ERC-NEWS
@@ -339,6 +339,13 @@ Also available as the library functions 'erc-cmd-AME', 'erc-cmd-GME',
 and 'erc-cmd-GMSG', these new slash commands can prove handy in test
 environments.
 
+** New face 'erc-informational' for local administrative messages.
+Messages that don't originate from a server have historically been
+shown in a combination of 'erc-notice-face' and 'erc-error-face',
+which can be distracting for those of low to moderate importance.
+Such messages now look more subdued and retain the familiar
+'erc-notice-prefix' stars.
+
 ** Miscellaneous UX changes.
 Some minor quality-of-life niceties have finally made their way to
 ERC.  For example, fool visibility has become togglable with the new
diff --git a/lisp/erc/erc-button.el b/lisp/erc/erc-button.el
index 1f9d6fd64c0..8cf8991e57c 100644
--- a/lisp/erc/erc-button.el
+++ b/lisp/erc/erc-button.el
@@ -309,7 +309,9 @@ erc-button-add-buttons
             regexp)
         (erc-button-remove-old-buttons)
         (unless (or erc-button--has-nickname-entry
-                    (not erc-button-buttonize-nicks))
+                    (not erc-button-buttonize-nicks)
+                    (and (erc--memq-msg-prop 'erc--skip 'button)
+                         (not (setq alist nil))))
           (erc-button-add-nickname-buttons
            `(_ _ erc-button--modify-nick-function
                ,erc-button-nickname-callback-function)))
diff --git a/lisp/erc/erc-networks.el b/lisp/erc/erc-networks.el
index 1b26afa1164..9c8cdf3b0e4 100644
--- a/lisp/erc/erc-networks.el
+++ b/lisp/erc/erc-networks.el
@@ -50,6 +50,7 @@ erc-server-connected
 (defvar erc-server-process)
 
 (declare-function erc--get-isupport-entry "erc-backend" (key &optional single))
+(declare-function erc--insert-admin-message-before "erc" (&rest args))
 (declare-function erc-buffer-filter "erc" (predicate &optional proc))
 (declare-function erc-current-nick "erc" nil)
 (declare-function erc-display-error-notice "erc" (parsed string))
@@ -1345,24 +1346,35 @@ erc-unset-network-name
   (setq erc-network nil)
   nil)
 
-;; TODO add note in Commentary saying that this module is considered a
-;; core module and that it's as much about buffer naming and network
-;; identity as anything else.
-
-(defun erc-networks--insert-transplanted-content (content)
-  (let ((inhibit-read-only t)
-        (buffer-undo-list t))
-    (save-excursion
-      (save-restriction
-        (widen)
-        (goto-char (point-min))
-        (insert-before-markers content)))))
+(defun erc-networks--transplant-buffer-content (src dest)
+  "Insert buffer SRC's contents into DEST, above its contents."
+  (with-silent-modifications
+    (let ((content (with-current-buffer src
+                     (cl-assert (not (buffer-narrowed-p)))
+                     (erc--insert-admin-message-before 'grafted ?n dest ?o src)
+                     (buffer-substring (point-min) erc-insert-marker))))
+      (with-current-buffer dest
+        (save-excursion
+          (save-restriction
+            (cl-assert (not (buffer-narrowed-p)))
+            (goto-char (point-min))
+            (while (and (eql ?\n (char-after (point)))
+                        (null (text-properties-at (point))))
+              (delete-char 1))
+            (insert-before-markers content)))))))
+
+(defvar erc-networks--transplant-target-buffer-function
+  #'erc-networks--transplant-buffer-content
+  "Function to rename and merge the contents of two target buffers.
+Called with the donating buffer to be killed and buffer to receive the
+transplant.  Consuming modules can leave a marker at the beginning of
+the latter buffer to access the insertion point, if needing to do things
+like adjust invisibility properties, etc.")
 
 ;; This should run whenever a network identity is updated.
-
 (defun erc-networks--reclaim-orphaned-target-buffers (new-proc nid announced)
   "Visit disowned buffers for same NID and associate with NEW-PROC.
-ANNOUNCED is the server's reported host name."
+Expect ANNOUNCED to be the server's reported host name."
   (erc-buffer-filter
    (lambda ()
      (when (and erc--target
@@ -1372,20 +1384,25 @@ erc-networks--reclaim-orphaned-target-buffers
                     (string= erc-server-announced-name announced)))
        ;; If a target buffer exists for the current process, kill this
        ;; stale one after transplanting its content; else reinstate.
-       (if-let ((existing (erc-get-buffer
-                           (erc--target-string erc--target) new-proc)))
+       (if-let ((actual (erc-get-buffer (erc--target-string erc--target)
+                                        new-proc)))
            (progn
-             (widen)
-             (let ((content (buffer-substring (point-min)
-                                              erc-insert-marker)))
-               (kill-buffer) ; allow target-buf renaming hook to run
-               (with-current-buffer existing
-                 (erc-networks--ensure-unique-target-buffer-name)
-                 (erc-networks--insert-transplanted-content content))))
+             (funcall erc-networks--transplant-target-buffer-function
+                      (current-buffer) actual)
+             (kill-buffer (current-buffer))
+             (with-current-buffer actual
+               (erc-networks--ensure-unique-target-buffer-name)))
          (setq erc-server-process new-proc
                erc-server-connected t
                erc-networks--id nid))))))
 
+;; For existing buffers, `erc-open' reinitializes a core set of local
+;; variables in addition to some text, such as the prompt.  It expects
+;; module activation functions to do the same for assets they manage.
+;; However, "stateful" modules, whose functionality depends on the
+;; evolution of a buffer's content, may need to reconcile state during
+;; a merge.  An example might be a module that provides consistent
+;; timestamps: it should ensure time values don't decrease.
 (defvar erc-networks--copy-server-buffer-functions nil
   "Abnormal hook run in new server buffers when deduping.
 Passed the existing buffer to be killed, whose contents have
@@ -1393,26 +1410,18 @@ erc-networks--copy-server-buffer-functions
 
 (defun erc-networks--copy-over-server-buffer-contents (existing name)
   "Kill off existing server buffer after copying its contents.
-Must be called from the replacement buffer."
+Expect to be called from the replacement buffer."
   (defvar erc-kill-buffer-hook)
   (defvar erc-kill-server-hook)
-  ;; ERC expects `erc-open' to be idempotent when setting up local
-  ;; vars and other context properties for a new identity.  Thus, it's
-  ;; unlikely we'll have to copy anything else over besides text.  And
-  ;; no reconciling of user tables, etc. happens during a normal
-  ;; reconnect, so we should be fine just sticking to text. (Right?)
-  (let ((text (with-current-buffer existing
-                ;; This `erc-networks--id' should be
-                ;; `erc-networks--id-equal-p' to caller's network
-                ;; identity and older if not eq.
-                ;;
-                ;; `erc-server-process' should be set but dead
-                ;; and eq `get-buffer-process' unless latter nil
-                (delete-process erc-server-process)
-                (buffer-substring (point-min) erc-insert-marker)))
-        erc-kill-server-hook
-        erc-kill-buffer-hook)
-    (erc-networks--insert-transplanted-content text)
+  ;; The following observations from ERC 5.5 regarding the buffer
+  ;; `existing' were thought at the time to be invariants:
+  ;; - `erc-networks--id' is `erc-networks--id-equal-p' to the
+  ;;    caller's network identity and older if not `eq'.
+  ;; - `erc-server-process' should be set (local) but dead and `eq' to
+  ;;    the result of `get-buffer-process' unless the latter is nil.
+  (delete-process (buffer-local-value 'erc-server-process existing))
+  (erc-networks--transplant-buffer-content existing (current-buffer))
+  (let (erc-kill-server-hook erc-kill-buffer-hook)
     (run-hook-with-args 'erc-networks--copy-server-buffer-functions existing)
     (kill-buffer name)))
 
diff --git a/lisp/erc/erc-stamp.el b/lisp/erc/erc-stamp.el
index fd137c0548a..8571c836ba2 100644
--- a/lisp/erc/erc-stamp.el
+++ b/lisp/erc/erc-stamp.el
@@ -730,6 +730,7 @@ erc-stamp--defer-date-insertion-on-post-modify
     (fset symbol
           (lambda (&rest _)
             (remove-hook hook-var symbol)
+            (setf (erc-stamp--date-fn data) #'ignore)
             (when (buffer-live-p buffer)
               (with-current-buffer buffer
                 (setq erc-stamp--date-stamps
@@ -773,11 +774,20 @@ erc-stamp--date-mode
   :interactive nil
   (if erc-stamp--date-mode
       (progn
+        (add-function :around
+                      (local 'erc-networks--transplant-target-buffer-function)
+                      #'erc-stamp--dedupe-date-stamps-from-target-buffer)
+        (add-hook 'erc-networks--copy-server-buffer-functions
+                  #'erc-stamp--dedupe-date-stamps-from-buffer 0 t)
         (add-hook 'erc-insert-post-hook
                   #'erc-stamp--defer-date-insertion-on-post-insert 0 t)
         (add-hook 'erc-send-post-hook
                   #'erc-stamp--defer-date-insertion-on-post-send 0 t))
     (kill-local-variable 'erc-timestamp-last-inserted-left)
+    (remove-function (local 'erc-networks--transplant-target-buffer-function)
+                     #'erc-stamp--dedupe-date-stamps-from-target-buffer)
+    (remove-hook 'erc-networks--copy-server-buffer-functions
+                 #'erc-stamp--dedupe-date-stamps-from-buffer t)
     (remove-hook 'erc-insert-post-hook
                  #'erc-stamp--defer-date-insertion-on-post-insert t)
     (remove-hook 'erc-send-post-hook
@@ -841,6 +851,8 @@ erc-insert-timestamp-left-and-right
            ((not (string-equal rendered erc-timestamp-last-inserted-left)))
            ((null (cl-find rendered erc-stamp--date-stamps
                            :test #'string= :key #'erc-stamp--date-str))))
+        ;; Force `erc-insert-timestamp-right' to stamp this message.
+        (setq erc-timestamp-last-inserted-right nil)
         (setq erc-stamp--deferred-date-stamp
               (make-erc-stamp--date :ts ct :str rendered))))
     ;; insert right timestamp
@@ -1040,6 +1052,54 @@ erc-stamp--reset-on-clear
           erc-timestamp-last-inserted-left nil
           erc-timestamp-last-inserted-right nil)))
 
+(defun erc-stamp--dedupe-date-stamps (old-stamps)
+  "Update `erc-stamp--date-stamps' from its counterpart OLD-STAMPS.
+Assume the contents of the buffer for OLD-STAMPS have just been inserted
+above the current buffer's and that the old buffer still exists so that
+markers still point somewhere.  For each duplicate, update the existing
+marker to match the transplanted timestamp with the same date.  Also
+copy non-duplicate `erc-stamp--date' objects from OLD-STAMPS to the
+current buffer's, maintaining order."
+  (let (need)
+    (dolist (old old-stamps)
+      (cl-assert (marker-position (erc-stamp--date-marker old)))
+      (if-let ((new (cl-find (erc-stamp--date-str old) erc-stamp--date-stamps
+                             :test #'string= :key #'erc-stamp--date-str))
+               (new-marker (erc-stamp--date-marker new)))
+          ;; The new buffer now has a duplicate stamp, so remove the
+          ;; "newer" one from the buffer.
+          (progn
+            (erc--delete-inserted-message-naively new-marker)
+            (set-marker new-marker (erc-stamp--date-marker old)))
+        ;; The new buffer doesn't have this stamp, so add its data
+        ;; object to the sorted list.
+        (push old need)
+        ;; Update the old marker position to point to the new buffer.
+        (set-marker (erc-stamp--date-marker old)
+                    (erc-stamp--date-marker old))))
+    ;; These *should* already be sorted (right?).
+    (setq erc-stamp--date-stamps
+          (nconc (nreverse need) erc-stamp--date-stamps))
+    (let ((last 0))
+      (dolist (stamp erc-stamp--date-stamps)
+        (cl-assert (eq (current-buffer)
+                       (marker-buffer (erc-stamp--date-marker stamp))))
+        (cl-assert (< last (erc-stamp--date-marker stamp)))
+        (setq last (erc-stamp--date-marker stamp))))))
+
+(defun erc-stamp--dedupe-date-stamps-from-buffer (old-buffer)
+  "Merge date stamps from OLD-BUFFER into in the current buffer."
+  (let ((old-stamps (buffer-local-value 'erc-stamp--date-stamps old-buffer)))
+    (erc-stamp--dedupe-date-stamps old-stamps)))
+
+(defun erc-stamp--dedupe-date-stamps-from-target-buffer (orig old-buffer
+                                                              new-buffer)
+  "Merge date stamps from OLD-BUFFER into NEW-BUFFER after calling ORIG."
+  (let ((old-stamps (buffer-local-value 'erc-stamp--date-stamps old-buffer)))
+    (prog1 (funcall orig old-buffer new-buffer)
+      (with-current-buffer new-buffer
+        (erc-stamp--dedupe-date-stamps old-stamps)))))
+
 (provide 'erc-stamp)
 
 ;;; erc-stamp.el ends here
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index c92fd42322a..7bc480c8a92 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1513,6 +1513,10 @@ erc-error-face
   "ERC face for errors."
   :group 'erc-faces)
 
+(defface erc-informational '((t :inherit shadow))
+  "Face for local administrative messages of low to moderate importance."
+  :group 'erc-faces)
+
 ;; same default color as `erc-input-face'
 (defface erc-my-nick-face '((t :weight bold :foreground "brown"))
   "ERC face for your current nickname in messages sent by you.
@@ -3518,6 +3522,14 @@ erc-display-line
         (push '(erc--msg . notice) erc--msg-prop-overrides)))
     (erc-display-message nil nil buffer string)))
 
+(defun erc--insert-admin-message-before (msg &rest args)
+  "Call `erc-display-message' with MSG and ARGS, inserting before point.
+Inhibit all buttonizing."
+  (let ((erc--msg-prop-overrides `((erc--skip . (stamp track button))
+                                   ,@erc--msg-prop-overrides)))
+    (apply #'erc-display-message nil '(notice informational)
+           (current-buffer) msg args)))
+
 (defvar erc--merge-text-properties-p nil
   "Non-nil when `erc-put-text-property' defers to `erc--merge-prop'.")
 
@@ -3724,9 +3736,12 @@ erc-display-message-highlight
         (t
          (erc-put-text-property
           0 (length string)
-          'font-lock-face (or (intern-soft
-			       (concat "erc-" (symbol-name type) "-face"))
-                              'erc-default-face)
+          'font-lock-face
+          (let* ((name (symbol-name type))
+                 (symbol (or (intern-soft (concat "erc-" name "-face"))
+                             (intern-soft (concat "erc-" name))
+                             type)))
+            (or (and (facep symbol) symbol) 'erc-default-face))
           string)
          string)))
 
@@ -9426,6 +9441,7 @@ english
    (finished . "\n\n*** ERC finished ***\n")
    (terminated . "\n\n*** ERC terminated: %e\n")
    (login . "Logging in as `%n'...")
+   (grafted . "Grafted buffer `%n' onto `%o'")
    (nick-in-use . "%n is in use. Choose new nickname: ")
    (nick-too-long
     . "WARNING: Nick length (%i) exceeds max NICKLEN(%l) defined by server")
diff --git a/test/lisp/erc/erc-networks-tests.el b/test/lisp/erc/erc-networks-tests.el
index 0d8861f2167..90d6f13f2f6 100644
--- a/test/lisp/erc/erc-networks-tests.el
+++ b/test/lisp/erc/erc-networks-tests.el
@@ -1243,6 +1243,7 @@ erc-networks--rename-server-buffer--no-existing--orphan
 
   (with-current-buffer (get-buffer-create "irc.foonet.org")
     (erc-mode)
+    (erc--initialize-markers (point) nil)
     (setq erc-network 'FooNet
           erc-server-current-nick "tester"
           erc-server-process (erc-networks-tests--create-live-proc)
@@ -1282,6 +1283,7 @@ erc-networks--rename-server-buffer--existing--reuse
     (ert-info ("New buffer steals name, content")
       (with-current-buffer (get-buffer-create "irc.foonet.org")
         (erc-mode)
+        (erc--initialize-markers (point) nil)
         (setq erc-network 'FooNet
               erc-server-current-nick "tester"
               erc-server-process (erc-networks-tests--create-live-proc)
@@ -1522,6 +1524,7 @@ erc-networks--rename-server-buffer--local-match
     (ert-info ("New server buffer steals name, content")
       (with-current-buffer (get-buffer-create "irc.foonet.org")
         (erc-mode)
+        (erc--initialize-markers (point) nil)
         (setq erc-network 'FooNet
               erc-server-current-nick "tester"
               erc-server-announced-name "us-east.foonet.org"
@@ -1574,6 +1577,7 @@ erc-networks--rename-server-buffer--local-nomatch
     (ert-info ("New server buffer steals name, content")
       (with-current-buffer (get-buffer-create "irc.foonet.org")
         (erc-mode)
+        (erc--initialize-markers (point) nil)
         (setq erc-network 'FooNet
               erc-server-current-nick "tester"
               erc-server-announced-name "us-east.foonet.org" ; east
diff --git a/test/lisp/erc/erc-stamp-tests.el b/test/lisp/erc/erc-stamp-tests.el
index 5fee21ec28f..f4209c5d668 100644
--- a/test/lisp/erc/erc-stamp-tests.el
+++ b/test/lisp/erc/erc-stamp-tests.el
@@ -349,4 +349,120 @@ erc--get-inserted-msg-bounds/readonly/stamp
    (lambda (arg)
      (should (equal '(3 . 19) (erc--get-inserted-msg-bounds arg))))))
 
+(ert-deftest erc-stamp--dedupe-date-stamps-from-target-buffer ()
+  (let ((erc-modules erc-modules)
+        (erc-stamp--tz t))
+    (erc-tests-common-make-server-buf)
+    (erc-stamp-mode +1)
+
+    ;; Create two buffers with an overlapping date stamp.
+    (with-current-buffer (erc--open-target "#chan@old")
+      (let ((erc-stamp--current-time '(1690761600001 . 1000)))
+        (erc-tests-common-display-message nil 'notice (current-buffer)
+                                          "2023-07-31T00:00:00.001Z"))
+      (let ((erc-stamp--current-time '(1690761601001 . 1000)))
+        (erc-tests-common-display-message nil 'notice (current-buffer) "0.0"))
+
+      (let ((erc-stamp--current-time '(1690848000001 . 1000)))
+        (erc-tests-common-display-message nil 'notice (current-buffer)
+                                          "2023-08-01T00:00:00.001Z"))
+      (let ((erc-stamp--current-time '(1690848001001 . 1000)))
+        (erc-tests-common-display-message nil 'notice (current-buffer) "1.0"))
+      (let ((erc-stamp--current-time '(1690848060001 . 1000)))
+        (erc-tests-common-display-message nil 'notice (current-buffer) "1.1"))
+
+      (let ((erc-stamp--current-time '(1690934400001 . 1000)))
+        (erc-tests-common-display-message nil 'notice (current-buffer)
+                                          "2023-08-02T00:00:00.001Z"))
+      (let ((erc-stamp--current-time '(1690934401001 . 1000)))
+        (erc-tests-common-display-message nil 'notice (current-buffer) "2.0"))
+      (let ((erc-stamp--current-time '(1690956000001 . 1000)))
+        (erc-tests-common-display-message nil 'notice (current-buffer) "2.6")))
+
+    (with-current-buffer (erc--open-target "#chan@new")
+      (let ((erc-stamp--current-time '(1690956001001 . 1000)))
+        (erc-tests-common-display-message nil 'notice (current-buffer)
+                                          "2023-08-02T06:00:01.001Z"))
+      (let ((erc-stamp--current-time '(1690963200001 . 1000)))
+        (erc-tests-common-display-message nil 'notice (current-buffer) "2.8"))
+
+      (let ((erc-stamp--current-time '(1691020800001 . 1000)))
+        (erc-tests-common-display-message nil 'notice (current-buffer)
+                                          "2023-08-03T00:00:00.001Z"))
+      (let ((erc-stamp--current-time '(1691020801001 . 1000)))
+        (erc-tests-common-display-message nil 'notice (current-buffer) "3.0"))
+      (let ((erc-stamp--current-time '(1691053200001 . 1000)))
+        (erc-tests-common-display-message nil 'notice (current-buffer) "3.9"))
+
+      (let ((erc-stamp--current-time '(1691107200001 . 1000)))
+        (erc-tests-common-display-message nil 'notice (current-buffer)
+                                          "2023-08-04T00:00:00.001Z"))
+      (let ((erc-stamp--current-time '(1691107201001 . 1000)))
+        (erc-tests-common-display-message nil 'notice (current-buffer) "4.0"))
+      (let ((erc-stamp--current-time '(1691110800001 . 1000)))
+        (erc-tests-common-display-message nil 'notice (current-buffer) "4.1")))
+
+    (erc-stamp--dedupe-date-stamps-from-target-buffer
+     #'erc-networks--transplant-buffer-content
+     (get-buffer "#chan@old")
+     (get-buffer "#chan@new"))
+
+    ;; Ensure the "model", `erc-stamp--date-stamps', matches reality
+    ;; in the buffer's contents.
+    (with-current-buffer "#chan@new"
+      (let ((stamps erc-stamp--date-stamps))
+        (goto-char 3)
+        (should (looking-at (rx "\n[Mon Jul 31 2023]")))
+        (should (= (erc--get-inserted-msg-beg (point))
+                   (erc-stamp--date-marker (pop stamps))))
+        (goto-char (1+ (match-end 0)))
+        (should (looking-at (rx "*** 2023-07-31T00:00:00.001Z")))
+        (forward-line 1)
+        (should (looking-at (rx "*** 0.0")))
+        (forward-line 1)
+
+        (should (looking-at (rx "\n[Tue Aug  1 2023]")))
+        (should (= (erc--get-inserted-msg-beg (point))
+                   (erc-stamp--date-marker (pop stamps))))
+        (goto-char (1+ (match-end 0)))
+        (should (looking-at (rx "*** 2023-08-01T00:00:00.001Z")))
+        (forward-line 1)
+        (should (looking-at (rx "*** 1.0")))
+        (forward-line 1)
+        (should (looking-at (rx "*** 1.1")))
+        (forward-line 1)
+
+        (should (looking-at (rx "\n[Wed Aug  2 2023]")))
+        (should (= (erc--get-inserted-msg-beg (point))
+                   (erc-stamp--date-marker (pop stamps))))
+        (goto-char (1+ (match-end 0)))
+        (should (looking-at (rx "*** 2023-08-02T00:00:00.001Z")))
+        (forward-line 1)
+        (should (looking-at (rx "*** 2.0")))
+        (forward-line 1)
+        (should (looking-at (rx "*** 2.6")))
+        (forward-line 1)
+        (should (looking-at
+                 (rx "*** Grafted buffer `#chan@new' onto `#chan@old'")))
+        (forward-line 1)
+        (should (looking-at (rx "*** 2023-08-02T06:00:01.001Z")))
+        (forward-line 1)
+        (should (looking-at (rx "*** 2.8")))
+        (forward-line 1)
+
+        (should (looking-at (rx "\n[Thu Aug  3 2023]")))
+        (should (= (erc--get-inserted-msg-beg (point))
+                   (erc-stamp--date-marker (pop stamps))))
+        (goto-char (1+ (match-end 0)))
+        (should (looking-at (rx "*** 2023-08-03T00:00:00.001Z")))
+        (forward-line 3) ; ...
+
+        (should (looking-at (rx "\n[Fri Aug  4 2023]")))
+        (should (= (erc--get-inserted-msg-beg (point))
+                   (erc-stamp--date-marker (pop stamps))))
+        (should-not stamps))))
+
+  (when noninteractive
+    (erc-tests-common-kill-buffers)))
+
 ;;; erc-stamp-tests.el ends here
-- 
2.44.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0002-5.6-Reuse-old-query-buffers-for-round-trip-renicks-i.patch --]
[-- Type: text/x-patch, Size: 31789 bytes --]

From 8e7ebca1ea1e23d4751cfa41e7856d68985a345d Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Wed, 8 May 2024 19:04:13 -0700
Subject: [PATCH 2/2] [5.6] Reuse old query buffers for round-trip renicks in
 ERC

* lisp/erc/erc-backend.el
(erc--wrangle-query-buffers-on-nick-change): New function for handling
the buffer-renaming and message-routing business that formerly resided
in `erc-server-NICK.  It stands apart so that modules overriding
`erc-server-NICK' can make use of it.  It now favors reusing an
existing query buffer when possible instead of creating a new,
<N>-suffixed buffer.  This addresses a missing aspect of the mission
originally pursued by bug#48598.
(erc-server-NICK): Fix erroneous call to `erc-update-user-nick' that
passed the sender's login as the INFO argument.  Factor out buffer
renaming logic and move to `erc--wrangle-query-buffers-on-nick-change'
for use by "NICK" handlers controlled by modules.
* lisp/erc/erc.el (erc-generate-new-buffer-name): Fix typo in doc
string.
(erc--open-target): Ensure a user for the client's current nick exists
in the target buffer's new `erc-channel-members' table so that query
buffers will be found when printing "QUIT" messages and similar.
* test/lisp/erc/erc-scenarios-base-renick.el
(erc-scenarios-base-renick-queries-solo): Revise slightly to use
updated helpers.
(erc-scenarios-base-renick-queries/round-trip/default): New test.
(erc-scenarios-base-renick-queries/round-trip/merge-query): New test.
* test/lisp/erc/resources/base/renick/queries/roundtrip.eld: New file.
* test/lisp/erc/resources/base/renick/self/manual.eld: Update timeouts.
* test/lisp/erc/resources/base/renick/self/merge-query-a.eld: New file.
* test/lisp/erc/resources/base/renick/self/merge-query-b.eld: New file.
---
 lisp/erc/erc-backend.el                       |  43 ++--
 lisp/erc/erc.el                               |  32 +--
 test/lisp/erc/erc-scenarios-base-renick.el    | 192 ++++++++++++++++--
 .../base/renick/queries/roundtrip.eld         |  64 ++++++
 .../erc/resources/base/renick/self/manual.eld |   8 +-
 .../base/renick/self/merge-query-a.eld        |  46 +++++
 .../base/renick/self/merge-query-b.eld        |  48 +++++
 7 files changed, 384 insertions(+), 49 deletions(-)
 create mode 100644 test/lisp/erc/resources/base/renick/queries/roundtrip.eld
 create mode 100644 test/lisp/erc/resources/base/renick/self/merge-query-a.eld
 create mode 100644 test/lisp/erc/resources/base/renick/self/merge-query-b.eld

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index ab419d2b018..55e67884da0 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -1829,6 +1829,35 @@ erc--server-determine-join-display-context
                                  ?h host ?t tgt ?m mode)))
       (erc-banlist-update proc parsed))))
 
+(defun erc--wrangle-query-buffers-on-nick-change (old new buffers)
+  "Create or reuse a query buffer for NEW nick after considering OLD nick.
+Return a (possibly updated) list of BUFFERS in which to announce the
+change."
+  (let ((new-buffer (erc-get-buffer new erc-server-process))
+        (old-buffer (erc-get-buffer old erc-server-process))
+        (selfp (string= old (erc-current-nick))))
+    (when new-buffer
+      (push new-buffer buffers))
+    (when old-buffer
+      (cl-pushnew old-buffer buffers)
+      ;; Ensure the new nick is absent from the old query.
+      (unless selfp
+        (erc-remove-channel-member old-buffer old))
+      (when (or selfp (null new-buffer))
+        (let ((target (erc--target-from-string new))
+              (id (erc-networks--id-given erc-networks--id)))
+          (with-current-buffer old-buffer
+            (setq erc-default-recipients (cons new
+                                               (cdr erc-default-recipients))
+                  erc--target target))
+          (setq new-buffer (erc-get-buffer-create erc-session-server
+                                                  erc-session-port
+                                                  nil target id)))))
+    (when new-buffer
+      (with-current-buffer new-buffer
+        (erc-update-mode-line)))
+    buffers))
+
 (define-erc-response-handler (NICK)
   "Handle nick change messages." nil
   (let ((nn (erc-response.contents parsed))
@@ -1843,18 +1872,8 @@ erc--server-determine-join-display-context
       ;; erc-channel-users won't contain it
       ;;
       ;; Possibly still relevant: bug#12002
-      (when-let ((buf (erc-get-buffer nick erc-server-process))
-                 (tgt (erc--target-from-string nn)))
-        (with-current-buffer buf
-          (setq erc-default-recipients (cons nn (cdr erc-default-recipients))
-                erc--target tgt))
-        (with-current-buffer (erc-get-buffer-create erc-session-server
-                                                    erc-session-port nil tgt
-                                                    (erc-networks--id-given
-                                                     erc-networks--id))
-          ;; Current buffer is among bufs
-          (erc-update-mode-line)))
-      (erc-update-user-nick nick nn host nil nil login)
+      (setq bufs (erc--wrangle-query-buffers-on-nick-change nick nn bufs))
+      (erc-update-user-nick nick nn host login)
       (cond
        ((string= nick (erc-current-nick))
         (cl-pushnew (erc-server-buffer) bufs)
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 7bc480c8a92..49debef7154 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1982,7 +1982,7 @@ erc-generate-new-buffer-name
 `erc-server-connect-function's.
 
 When TGT-INFO is non-nil, expect its string field to match the redundant
-param TARGET (retained for compatibility).  Whenever possibly, prefer
+param TARGET (retained for compatibility).  Whenever possible, prefer
 returning TGT-INFO's string unmodified.  But when a case-insensitive
 collision prevents that, return target@ID when ID is non-nil or
 target@network otherwise after renaming the conflicting buffer in the
@@ -5894,19 +5894,23 @@ erc-debug-missing-hooks
   nil)
 
 (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)))
+  "Open an ERC buffer on TARGET and return the buffer.
+Ensure own nick is present in the buffer's `erc-channel-members'."
+  (let ((buffer (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))))
+    (prog1 buffer
+      (when (erc-query-buffer-p buffer)
+        (erc-update-channel-member target (erc-current-nick) nil t)))))
 
 (defun erc-query (target server-buffer)
   "Open a query buffer on TARGET using SERVER-BUFFER.
diff --git a/test/lisp/erc/erc-scenarios-base-renick.el b/test/lisp/erc/erc-scenarios-base-renick.el
index 3001fde6da0..290230259cb 100644
--- a/test/lisp/erc/erc-scenarios-base-renick.el
+++ b/test/lisp/erc/erc-scenarios-base-renick.el
@@ -160,6 +160,7 @@ erc-scenarios-base-renick-queries-solo
        (erc-server-flood-penalty 0.1)
        (erc-server-flood-margin 20)
        (dumb-server (erc-d-run "localhost" t 'solo))
+       (expect (erc-d-t-make-expecter))
        (port (process-contact dumb-server :service))
        erc-autojoin-channels-alist
        erc-server-buffer-foo)
@@ -175,33 +176,186 @@ erc-scenarios-base-renick-queries-solo
 
     (erc-d-t-wait-for 10 (get-buffer "foonet"))
 
-    (ert-info ("Joined by bouncer to #foo, pal persent")
+    (ert-info ("Joined by bouncer to #foo, pal Lal is present")
       (with-current-buffer (erc-d-t-wait-for 1 (get-buffer "#foo"))
-        (erc-d-t-search-for 5 "On Thursday")
+        (funcall expect 10 "<bob> alice: On Thursday")
         (erc-scenarios-common-say "hi")))
 
-    (erc-d-t-wait-for 10 "Query buffer appears with message from pal"
-      (get-buffer "Lal"))
-
-    (ert-info ("Chat with pal, who changes name")
-      (with-current-buffer "Lal"
-        (erc-d-t-search-for 3 "hello")
+    (ert-info ("Query buffer appears from Lal, who renicks")
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "Lal"))
+        (funcall expect 10 "<Lal> hello")
         (erc-scenarios-common-say "hi")
-        (erc-d-t-search-for 10 "is now known as Linguo")
-        (should-not (search-forward "is now known as Linguo" nil t))))
-
-    (erc-d-t-wait-for 1 (get-buffer "Linguo"))
-    (should-not (get-buffer "Lal"))
-
-    (with-current-buffer "Linguo" (erc-scenarios-common-say "howdy Linguo"))
+        (funcall expect 10 "is now known as Linguo")
+        ;; No duplicate message.
+        (funcall expect -0.1 "is now known as Linguo")
+        ;; No duplicate buffer.
+        (erc-d-t-wait-for 1 (equal (buffer-name) "Linguo"))
+        (should-not (get-buffer "Lal"))
+        (erc-scenarios-common-say "howdy Linguo")))
 
     (with-current-buffer "#foo"
-      (erc-d-t-search-for 10 "is now known as Linguo")
-      (should-not (search-forward "is now known as Linguo" nil t))
-      (erc-cmd-PART ""))
+      (funcall expect 10 "is now known as Linguo")
+      (funcall expect -0.1 "is now known as Linguo")
+      (erc-scenarios-common-say "/part"))
 
     (with-current-buffer "Linguo"
-      (erc-d-t-search-for 10 "get along"))))
+      (funcall expect 10 "get along"))))
+
+;; Someone you have a query with disconnects and reconnects under a
+;; new nick (perhaps due to their client appending a backtick or
+;; underscore).  They then engage you in another query before
+;; renicking to their original nick.  Prior to 5.5, ERC would add a
+;; uniquifying suffix of the form bob<2> to the new, post-renick
+;; query.  ERC 5.6+ acts differently.  It mimics popular standalone
+;; clients in reusing existing query buffers.
+(ert-deftest erc-scenarios-base-renick-queries/round-trip/default ()
+  :tags '(:expensive-test)
+
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/renick/queries")
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (erc-d-run "localhost" t 'roundtrip))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       (erc-autojoin-channels-alist '((foonet "#chan"))))
+
+    (ert-info ("Connect to foonet")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester")
+        (funcall expect 10 "This server is in debug mode")))
+
+    (ert-info ("User dummy opens a query with you")
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "dummy"))
+        (funcall expect 10 "hi")))
+
+    (ert-info ("User dummy quits, reconnects as user warwick")
+      (with-current-buffer "#chan"
+        (funcall expect 10 "has quit")
+        (should-not (erc-get-channel-member "dummy"))
+        (with-current-buffer "dummy"
+          (should-not (erc-get-channel-member "dummy")))
+        (funcall expect 10 "<bob> Alas! sir")
+        (funcall expect 10 "<bob> warwick, welcome")
+        (funcall expect 10 "<warwick> hola")))
+
+    (ert-info ("User warwick queries you, creating a new buffer")
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "warwick"))
+        (should (get-buffer "dummy")) ; not reused
+        (funcall expect 10 "<warwick> howdy")
+        (funcall expect 10 "is now known as dummy")
+        (should-not (erc-get-channel-member "warwick"))
+        (should-not (erc-get-channel-member "dummy"))))
+
+    (ert-info ("User warwick renicks as user dummy")
+      (with-current-buffer "#chan"
+        (funcall expect 10 "is now known as dummy")
+        (should-not (erc-get-channel-member "warwick"))))
+
+    (with-current-buffer "dummy"
+      (should-not (get-buffer "dummy<2>"))
+      (funcall expect 10 "has quit" (point-min))
+      (funcall expect -0.1 "merging buffer")
+      (funcall expect 10 "is now known as dummy")
+      (should (erc-get-channel-member "dummy"))
+      (funcall expect 10 "<dummy> hey"))
+
+    (with-current-buffer "#chan"
+      (funcall expect 10 "<alice> bob: Than those that"))))
+
+;; This test asserts behavior for the other side of the conversation
+;; described by `erc-scenarios-base-renick-queries/round-trip/default'
+;; above.  After speaking with someone in a query, you disconnect and
+;; reconnect under a new nick.  You then open a new query with the
+;; same person before changing your nick back to the previous one.
+;; The buffers for the two session should then be merged with the help
+;; of `erc-networks--transplant-target-buffer-function' and
+;; `erc-networks--copy-server-buffer-functions'.
+(ert-deftest erc-scenarios-base-renick-self/merge-query ()
+  :tags '(:expensive-test)
+
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/renick/self")
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (erc-d-run "localhost" t 'merge-query-a 'merge-query-b))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       (erc-autojoin-channels-alist '((foonet "#chan"))))
+
+    (ert-info ("Connect to foonet as tester")
+      (with-current-buffer (erc :server "127.0.0.1" :port port :nick "tester")
+        (funcall expect 10 "This server is in debug mode")))
+
+    (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#chan"))
+      (funcall expect 10 "<alice> bob: Speak to the people")
+      (erc-scenarios-common-say "/query observer"))
+
+    (with-current-buffer "observer"
+      (erc-scenarios-common-say "hi")
+      (funcall expect 10 "<observer> hi?"))
+
+    (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#chan"))
+      (erc-scenarios-common-say "/quit"))
+
+    (with-current-buffer "foonet"
+      (funcall expect 10 "*** ERC finished ***"))
+
+    (ert-info ("Reconnect to foonet as dummy")
+      (with-current-buffer (erc :server "127.0.0.1" :port port :nick "dummy")
+        (funcall expect 10 "This server is in debug mode")))
+
+    (with-current-buffer
+        (erc-d-t-wait-for 10 (get-buffer "#chan@foonet/dummy"))
+      ;; Uniquification has been performed.
+      (should-not (get-buffer "#chan"))
+      (should (get-buffer "#chan@foonet/tester"))
+      (should-not (get-buffer "foonet"))
+      (should (get-buffer "foonet/tester"))
+      (should (get-buffer "foonet/dummy"))
+      (funcall expect 10 "<alice> bob: Pray you")
+      (erc-scenarios-common-say "/query observer"))
+
+    (with-current-buffer "observer@foonet/dummy"
+      (should-not (get-buffer "observer"))
+      (should (get-buffer "observer@foonet/tester"))
+      (erc-scenarios-common-say "hola")
+      (funcall expect 10 "<observer> whodis?"))
+
+    (with-current-buffer
+        (erc-d-t-wait-for 10 (get-buffer "#chan@foonet/dummy"))
+      (erc-scenarios-common-say "/nick tester"))
+
+    ;; All buffers have been merged.
+    (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "observer"))
+      (should-not (get-buffer "observer@foonet/dummy"))
+      (should-not (get-buffer "observer@foonet/tester"))
+      ;; Goto last message from previous session.
+      (funcall expect 10 "has quit" (point-min))
+      (funcall expect -0.01 "\n\n[") ; duplicate date stamp removed
+      (funcall expect 1 (concat "*** Grafted buffer `observer@foonet/dummy'"
+                                " onto `observer@foonet/tester'"))
+      (funcall expect 1 "<dummy> hola")
+      (funcall expect 1 "<observer> whodis?")
+      (funcall expect 1 "*** Your new nickname is tester"))
+
+    (with-current-buffer "foonet"
+      (should-not (get-buffer "foonet/dummy"))
+      (should-not (get-buffer "foonet/tester"))
+      ;; Goto last assertion.
+      (funcall expect 10 "*** ERC finished ***" (point-min))
+      (funcall expect -0.01 "\n\n[") ; duplicate date stamp removed
+      (funcall expect 10 "Grafted buffer `foonet/dummy' onto `foonet/tester'"))
+
+    (with-current-buffer "#chan"
+      (should-not (get-buffer "#chan@foonet/dummy"))
+      (should-not (get-buffer "#chan@foonet/tester"))
+      (funcall expect 10 "has quit" (point-min))
+      (funcall expect -0.01 "\n\n[") ; duplicate date stamp removed
+      (funcall expect 1 (concat "*** Grafted buffer `#chan@foonet/dummy'"
+                                " onto `#chan@foonet/tester'"))
+      (funcall expect 1 "You have joined channel #chan")
+      (funcall expect 1 "<bob> alice: Have here bereft")
+      (funcall expect 1 "*** Your new nickname is tester"))))
 
 ;; You share a channel and a query buffer with a user on two different
 ;; networks (through a proxy).  The user changes their nick on both
diff --git a/test/lisp/erc/resources/base/renick/queries/roundtrip.eld b/test/lisp/erc/resources/base/renick/queries/roundtrip.eld
new file mode 100644
index 00000000000..50764a143b6
--- /dev/null
+++ b/test/lisp/erc/resources/base/renick/queries/roundtrip.eld
@@ -0,0 +1,64 @@
+;; -*- mode: lisp-data; -*-
+((nick 10 "NICK tester"))
+((user 10 "USER user 0 * :unknown")
+ (0.00 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0.00 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version ergo-v2.11.1")
+ (0.00 ":irc.foonet.org 003 tester :This server was created Thu, 09 May 2024 05:19:24 UTC")
+ (0.00 ":irc.foonet.org 004 tester irc.foonet.org ergo-v2.11.1 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.00 ":irc.foonet.org 005 tester AWAYLEN=390 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# CHATHISTORY=25 ELIST=U EXCEPTS EXTBAN=,m FORWARD=f INVEX :are supported by this server")
+ (0.00 ":irc.foonet.org 005 tester KICKLEN=390 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 UTF8ONLY WHOX :are supported by this server")
+ (0.01 ":irc.foonet.org 005 tester draft/CHATHISTORY=25 :are supported by this server")
+ (0.00 ":irc.foonet.org 251 tester :There are 0 users and 6 invisible on 1 server(s)")
+ (0.00 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0.00 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0.00 ":irc.foonet.org 254 tester 2 :channels formed")
+ (0.00 ":irc.foonet.org 255 tester :I have 6 clients and 0 servers")
+ (0.00 ":irc.foonet.org 265 tester 6 6 :Current local users 6, max 6")
+ (0.00 ":irc.foonet.org 266 tester 6 6 :Current global users 6, max 6")
+ (0.00 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 10 "MODE tester +i")
+ (0.00 ":irc.foonet.org 221 tester +i")
+ (0.00 ":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 10 "JOIN #chan")
+ (0.03 ":irc.foonet.org 221 tester +i") ; dupe
+ (0.00 ":tester!~u@s8ceryiqkkcxk.irc JOIN #chan")
+ (0.04 ":irc.foonet.org 353 tester = #chan :@fsbot bob alice dummy tester")
+ (0.00 ":irc.foonet.org 366 tester #chan :End of NAMES list")
+ (0.00 ":alice!~u@68v4mpismdues.irc PRIVMSG #chan :tester, welcome!")
+ (0.00 ":bob!~u@68v4mpismdues.irc PRIVMSG #chan :tester, welcome!")
+ (0.03 ":bob!~u@68v4mpismdues.irc PRIVMSG #chan :That eye that told you so look'd but a-squint."))
+
+((mode-chan 10 "MODE #chan")
+ (0.00 ":irc.foonet.org 324 tester #chan +Cnt")
+ (0.01 ":irc.foonet.org 329 tester #chan 1715231970")
+
+ ;; existing query with dummy
+ (0.05 ":dummy!~u@s8ceryiqkkcxk.irc PRIVMSG tester :hi")
+ (0.02 ":bob!~u@68v4mpismdues.irc PRIVMSG #chan :alice: Villains, forbear! we are the empress' sons.")
+ (0.01 ":alice!~u@68v4mpismdues.irc PRIVMSG #chan :bob: This matter of marrying his king's daughter,wherein he must be weighed rather by her value than his own,words him, I doubt not, a great deal from the matter.")
+
+ ;; dummy quits
+ (0.07 ":dummy!~u@s8ceryiqkkcxk.irc QUIT :Quit: \2ERC\2 5.5.0.29.1 (IRC client for GNU Emacs 29.3.50)")
+ (0.03 ":bob!~u@68v4mpismdues.irc PRIVMSG #chan :We will afflict the emperor in his pride.")
+ (0.03 ":alice!~u@68v4mpismdues.irc PRIVMSG #chan :bob: Why, then, is my pump well flowered.")
+ (0.05 ":bob!~u@68v4mpismdues.irc PRIVMSG #chan :Alas! sir, I know not Jupiter; I never drank with him in all my life.")
+
+ ;; rejoins as warwick
+ (0.03 ":warwick!~u@s8ceryiqkkcxk.irc JOIN #chan")
+ (0.00 ":bob!~u@68v4mpismdues.irc PRIVMSG #chan :warwick, welcome!")
+ (0.00 ":alice!~u@68v4mpismdues.irc PRIVMSG #chan :warwick, welcome!")
+ (0.03 ":warwick!~u@s8ceryiqkkcxk.irc PRIVMSG #chan :hola")
+ (0.03 ":alice!~u@68v4mpismdues.irc PRIVMSG #chan :bob: And stint thou too, I pray thee, nurse, say I.")
+
+ ;; Makes contact in a query
+ (0.02 ":warwick!~u@s8ceryiqkkcxk.irc PRIVMSG tester :howdy")
+ (0.03 ":alice!~u@68v4mpismdues.irc PRIVMSG #chan :bob: Nor more willingly leaves winter; such summer-birds are men. Gentlemen, our dinner will not recompense this long stay: feast your ears with the music awhile, if they will fare so harshly o' the trumpet's sound; we shall to 't presently.")
+ (0.03 ":bob!~u@68v4mpismdues.irc PRIVMSG #chan :If it please your honour, I know not well what they are; but precise villains they are, that I am sure of, and void of all profanation in the world that good Christians ought to have.")
+
+ ;; warwick renicks back to dummy
+ (0.08 ":warwick!~u@s8ceryiqkkcxk.irc NICK dummy")
+ (0.04 ":bob!~u@68v4mpismdues.irc PRIVMSG #chan :Pleasure and action make the hours seem short.")
+ (0.01 ":dummy!~u@s8ceryiqkkcxk.irc PRIVMSG tester :hey")
+ (0.02 ":alice!~u@68v4mpismdues.irc PRIVMSG #chan :bob: Than those that have more cunning to be strange."))
diff --git a/test/lisp/erc/resources/base/renick/self/manual.eld b/test/lisp/erc/resources/base/renick/self/manual.eld
index dd107b806d5..a6220ffc2e6 100644
--- a/test/lisp/erc/resources/base/renick/self/manual.eld
+++ b/test/lisp/erc/resources/base/renick/self/manual.eld
@@ -1,5 +1,5 @@
 ;; -*- mode: lisp-data; -*-
-((pass 1 "PASS :foonet:changeme"))
+((pass 10 "PASS :foonet:changeme"))
 ((nick 1 "NICK tester"))
 ((user 1 "USER user 0 * :tester")
  (0 ":irc.foonet.org 001 tester :Welcome to the FooNet Internet Relay Chat Network tester")
@@ -24,7 +24,7 @@
  (0 ":irc.foonet.org 372 tester :- Please visit us in #libera for questions and support.")
  (0 ":irc.foonet.org 376 tester :End of /MOTD command."))
 
-((mode-user 1.2 "MODE tester +i")
+((mode-user 10 "MODE tester +i")
  (0 ":tester!~u@gq7yjr7gsu7nn.irc MODE tester :+RZi")
  (0 ":irc.znc.in 306 tester :You have been marked as being away")
  (0 ":tester!~u@gq7yjr7gsu7nn.irc JOIN #foo")
@@ -38,13 +38,13 @@
  (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 1 "MODE #foo")
+((mode-foo 10 "MODE #foo")
  (0 ":irc.foonet.org 324 tester #foo +nt")
  (0 ":irc.foonet.org 329 tester #foo 1622454985")
  (0.1 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :bob: Farewell, pretty lady: you must hold the credit of your father.")
  (0.1 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :alice: On Thursday, sir ? the time is very short."))
 
-((nick 2 "NICK dummy")
+((nick 10 "NICK dummy")
  (0 ":tester!~u@gq7yjr7gsu7nn.irc NICK :dummy")
  (0.1 ":dummy!~u@gq7yjr7gsu7nn.irc MODE dummy :+RZi")
  (0.1 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :dummy: Hi."))
diff --git a/test/lisp/erc/resources/base/renick/self/merge-query-a.eld b/test/lisp/erc/resources/base/renick/self/merge-query-a.eld
new file mode 100644
index 00000000000..27ef7ecd2ff
--- /dev/null
+++ b/test/lisp/erc/resources/base/renick/self/merge-query-a.eld
@@ -0,0 +1,46 @@
+;; -*- mode: lisp-data; -*-
+((nick 10 "NICK tester"))
+((user 10 "USER user 0 * :unknown")
+ (0.00 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0.01 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version ergo-v2.11.1")
+ (0.00 ":irc.foonet.org 003 tester :This server was created Sun, 12 May 2024 00:41:10 UTC")
+ (0.00 ":irc.foonet.org 004 tester irc.foonet.org ergo-v2.11.1 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.00 ":irc.foonet.org 005 tester AWAYLEN=390 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# CHATHISTORY=25 ELIST=U EXCEPTS EXTBAN=,m FORWARD=f INVEX :are supported by this server")
+ (0.01 ":irc.foonet.org 005 tester KICKLEN=390 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 UTF8ONLY WHOX :are supported by this server")
+ (0.01 ":irc.foonet.org 005 tester draft/CHATHISTORY=25 :are supported by this server")
+ (0.00 ":irc.foonet.org 251 tester :There are 0 users and 6 invisible on 1 server(s)")
+ (0.00 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0.02 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0.00 ":irc.foonet.org 254 tester 2 :channels formed")
+ (0.01 ":irc.foonet.org 255 tester :I have 6 clients and 0 servers")
+ (0.00 ":irc.foonet.org 265 tester 6 6 :Current local users 6, max 6")
+ (0.00 ":irc.foonet.org 266 tester 6 6 :Current global users 6, max 6")
+ (0.00 ":irc.foonet.org 422 tester :MOTD File is missing")
+ (0.00 ":irc.foonet.org 221 tester +i")
+ (0.00 ":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."))
+
+((mode-user 10 "MODE tester +i"))
+
+((join 10 "JOIN #chan")
+ (0.00 ":irc.foonet.org 221 tester +i")
+ (0.00 ":tester!~u@hyyensdmcrjxc.irc JOIN #chan")
+ (0.02 ":irc.foonet.org 353 tester = #chan :someone tester @fsbot alice bob observer")
+ (0.01 ":irc.foonet.org 366 tester #chan :End of NAMES list")
+ (0.00 ":bob!~u@zb3s8yrduykma.irc PRIVMSG #chan :tester, welcome!")
+ (0.01 ":alice!~u@zb3s8yrduykma.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode-chan 10 "MODE #chan")
+ (0.00 ":irc.foonet.org 324 tester #chan +Cnt")
+ (0.02 ":irc.foonet.org 329 tester #chan 1715474476")
+ (0.09 ":bob!~u@zb3s8yrduykma.irc PRIVMSG #chan :alice: And, uncle, so will I, an if I live.")
+ (0.03 ":alice!~u@zb3s8yrduykma.irc PRIVMSG #chan :bob: Speak to the people, and they pity her."))
+
+((privmsg-observer 10 "PRIVMSG observer :hi")
+ (0.04 ":observer!~u@hyyensdmcrjxc.irc PRIVMSG tester :hi?")
+ (0.07 ":bob!~u@zb3s8yrduykma.irc PRIVMSG #chan :To ask of whence you are: report it."))
+
+((quit 10 "QUIT :\2ERC\2")
+ (0.03 ":tester!~u@hyyensdmcrjxc.irc QUIT :Quit: \2ERC\2 5.6-git (IRC client for GNU Emacs 30.0.50)")
+ (0.03 "ERROR :Quit: \2ERC\2 5.6-git (IRC client for GNU Emacs 30.0.50)"))
+
+((drop 0 DROP))
diff --git a/test/lisp/erc/resources/base/renick/self/merge-query-b.eld b/test/lisp/erc/resources/base/renick/self/merge-query-b.eld
new file mode 100644
index 00000000000..4d7581b3884
--- /dev/null
+++ b/test/lisp/erc/resources/base/renick/self/merge-query-b.eld
@@ -0,0 +1,48 @@
+;; -*- mode: lisp-data; -*-
+((nick 10 "NICK dummy"))
+((user 10 "USER user 0 * :unknown")
+ (0.01 ":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.11.1")
+ (0.01 ":irc.foonet.org 003 dummy :This server was created Sun, 12 May 2024 00:41:10 UTC")
+ (0.00 ":irc.foonet.org 004 dummy irc.foonet.org ergo-v2.11.1 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.03 ":irc.foonet.org 005 dummy AWAYLEN=390 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# CHATHISTORY=25 ELIST=U EXCEPTS EXTBAN=,m FORWARD=f INVEX :are supported by this server")
+ (0.03 ":irc.foonet.org 005 dummy KICKLEN=390 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 UTF8ONLY WHOX :are supported by this server")
+ (0.01 ":irc.foonet.org 005 dummy draft/CHATHISTORY=25 :are supported by this server")
+ (0.00 ":irc.foonet.org 251 dummy :There are 0 users and 6 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 6 clients and 0 servers")
+ (0.00 ":irc.foonet.org 265 dummy 6 6 :Current local users 6, max 6")
+ (0.00 ":irc.foonet.org 266 dummy 6 6 :Current global users 6, max 6")
+ (0.03 ":irc.foonet.org 422 dummy :MOTD File is missing")
+ (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."))
+
+((mode-user 10 "MODE dummy +i"))
+
+((join-chan 10 "JOIN #chan")
+ (0.01 ":irc.foonet.org 221 dummy +i")
+ (0.00 ":dummy!~u@hyyensdmcrjxc.irc JOIN #chan")
+ (0.02 ":irc.foonet.org 353 dummy = #chan :@fsbot alice bob observer someone dummy")
+ (0.01 ":irc.foonet.org 366 dummy #chan :End of NAMES list")
+ (0.00 ":bob!~u@zb3s8yrduykma.irc PRIVMSG #chan :dummy, welcome!")
+ (0.01 ":alice!~u@zb3s8yrduykma.irc PRIVMSG #chan :dummy, welcome!"))
+
+((mode-chan 10 "MODE #chan")
+ (0.00 ":irc.foonet.org 324 dummy #chan +Cnt")
+ (0.02 ":irc.foonet.org 329 dummy #chan 1715474476")
+ (0.09 ":bob!~u@zb3s8yrduykma.irc PRIVMSG #chan :alice: Indeed, sir, he that sleeps feels not the toothache; but a man that were to sleep your sleep, and a hangman to help him to bed, I think he would change places with his officer; for look you, sir, you know not which way you shall go.")
+ (0.03 ":alice!~u@zb3s8yrduykma.irc PRIVMSG #chan :bob: Pray you, sir, deliver me this paper."))
+
+((privmsg-observer 10 "PRIVMSG observer :hola")
+ (0.01 ":bob!~u@zb3s8yrduykma.irc PRIVMSG #chan :alice: In manner and form following, sir; all those three: I was seen with her in the manor-house, sitting with her upon the form, and taken following her into the park; which, put together, is, in manner and form following. Now, sir, for the manner,it is the manner of a man to speak to a woman, for the form,in some form.")
+ (0.05 ":alice!~u@zb3s8yrduykma.irc PRIVMSG #chan :In Isbel's case and mine own. Service is no heritage; and I think I shall never have the blessing of God till I have issue o' my body, for they say barnes are blessings.")
+ (0.01 ":observer!~u@hyyensdmcrjxc.irc PRIVMSG dummy :whodis?")
+ (0.02 ":bob!~u@zb3s8yrduykma.irc PRIVMSG #chan :alice: Have here bereft my brother of his life."))
+
+((nick-tester 10 "NICK tester")
+ (0.02 ":dummy!~u@hyyensdmcrjxc.irc NICK tester")
+
+ (0.04 ":alice!~u@zb3s8yrduykma.irc PRIVMSG #chan :bob: You have too courtly a wit for me: I'll rest.")
+ (0.07 ":bob!~u@zb3s8yrduykma.irc PRIVMSG #chan :alice: And abstinence engenders maladies."))
-- 
2.44.0


^ permalink raw reply related	[flat|nested] 3+ messages in thread

* bug#70928: 30.0.50; ERC 5.6: Reuse query buffers for round-trip nick changes in ERC
  2024-05-14  1:00 bug#70928: 30.0.50; ERC 5.6: Reuse query buffers for round-trip nick changes in ERC J.P.
@ 2024-05-25  3:13 ` J.P.
  2024-05-28 13:37   ` J.P.
  0 siblings, 1 reply; 3+ messages in thread
From: J.P. @ 2024-05-25  3:13 UTC (permalink / raw)
  To: 70928; +Cc: emacs-erc

[-- Attachment #1: Type: text/plain, Size: 3643 bytes --]

"J.P." <jp@neverwas.me> writes:

> While it's true we can't technically be certain revived queries are
> being piloted by the same individuals (at least not until ERC becomes
> account aware), enough folks have complained about this over the years
> that I think it's legitimate to disregard that uncertainty by default.

Looking into this further, I'm getting the unfortunate impression
there's a deeper, underlying problem afoot and that it's rooted in how
ERC updates participant tables in query buffers. In short: it's
haphazard and difficult to predict. And the end result is not only that
the party you're conversing with may not be reachable [1] but that the
manifest model itself doesn't regard their presence or absence as
particularly important enough to attend to consistently. And while it's
true that the behavior in question is rather ancient, I don't think it's
too deeply ingrained to resist addressing, mainly because the degree to
which it's unwieldy and unreliable makes it unlikely any serious third
party package would bank on it. [2]

Anyway, here's the breakdown of the current behavior:

  - Issuing a "/QUERY bob" won't create a user for bob in the new
    buffer's `erc-channel-members' table nor in the server-wide table,
    `erc-server-users', even if you share a channel with bob.

  - A /WHOIS creates a user for bob if they're online. But the same
    doesn't apply to a /WHO because ERC doesn't currently handle
    responses for non-channel targets.

  - A direct query message from a user will also add them.

  - If a user is joined to a channel and then quits, an existing entry
    will be removed from both tables. However, the same doesn't apply if
    they part all channels or are kicked.

  - If they (re)join a channel, the user *won't* be added to any
    existing query buffer's table, only the server-wide and channel
    tables.

  - An ERR_NOSUCHNICK (401) won't remove a user from any tables.

Here's how I imagine things working in a saner ERC:

  - A user's presence in a channel will dictate whether they exist in
    the server buffer's `erc-server-users' table.

  - Issuing a /query will create a user entry in the query buffer's
    `erc-channel-members' table if they exist in the server-wide table
    (meaning they're present in some channel).

  - Users parting or being kicked from a channel will see their data
    removed from all query tables (and the server table) if they're no
    longer joined to any other channels.

  - Insertion hooks running in query buffers can always expect to see a
    speaker's user's in its `erc-channel-members' table. If they're
    absent, a temporary user will be created for the duration of
    response handling.

  - A new, optional module will be added to mimic the effect of the
    Monitor extension and to serve as a fallback after ERC adds support
    (see bug#49860). When it's active, users in queries who aren't also
    in a channel will be periodically polled for and kept up to date.

  - A client's own user for its current nick will be absent in all query
    tables but present, once discovered, in the server-wide table for
    the remainder of the session.

The attached patches attempt to implement the proposed changes. Comments
welcome.

Thanks.


[1] This bookkeeping issue is particularly pertinent in regard to future
    support for the IRCv3 Monitor extension, whose express purpose is to
    offer a pub-sub service that keeps clients abreast of such changes
    as they happen on the server.

[2] Although, as usual, I think we should make the legacy behavior
    accessible by compat flag for a couple versions.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0000-v1-v2.diff --]
[-- Type: text/x-patch, Size: 69662 bytes --]

From 6635456239fbb7ac0ac818992129114288c0f7f5 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Fri, 24 May 2024 20:09:31 -0700
Subject: [PATCH 0/7] *** NOT A PATCH ***

*** BLURB HERE ***

F. Jason Park (7):
  [5.6] Return nil from more ERC response handlers
  [5.6] Delete original speedbar frame in erc-nickbar-mode
  [5.6] Reuse old query buffers for round-trip renicks in ERC
  [5.6] Mention if an ERC module is local in its doc string
  [5.6] Update ERC query participants on JOIN and after NAMES
  [5.6] Retain client's own user in erc-server-users
  [5.6] Add ERC module querypoll as monitor fallback

 etc/ERC-NEWS                                  |  20 ++
 lisp/erc/erc-backend.el                       | 166 ++++++++++----
 lisp/erc/erc-common.el                        |  23 +-
 lisp/erc/erc-goodies.el                       | 187 +++++++++++++++
 lisp/erc/erc-sasl.el                          |   3 +-
 lisp/erc/erc-speedbar.el                      | 216 +++++++++---------
 lisp/erc/erc.el                               | 123 ++++++----
 test/lisp/erc/erc-goodies-tests.el            |  57 +++++
 test/lisp/erc/erc-networks-tests.el           |   2 +-
 test/lisp/erc/erc-scenarios-base-renick.el    | 210 +++++++++++++++--
 test/lisp/erc/erc-scenarios-status-sidebar.el |  16 +-
 test/lisp/erc/erc-tests.el                    |  21 +-
 .../base/reconnect/options-again.eld          |   2 +-
 .../base/renick/queries/roundtrip.eld         |  64 ++++++
 .../erc/resources/base/renick/self/manual.eld |   8 +-
 .../base/renick/self/merge-query-a.eld        |  46 ++++
 .../base/renick/self/merge-query-b.eld        |  48 ++++
 17 files changed, 972 insertions(+), 240 deletions(-)
 create mode 100644 test/lisp/erc/resources/base/renick/queries/roundtrip.eld
 create mode 100644 test/lisp/erc/resources/base/renick/self/merge-query-a.eld
 create mode 100644 test/lisp/erc/resources/base/renick/self/merge-query-b.eld

Interdiff:
diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS
index 62970f52396..0341bcc6d04 100644
--- a/etc/ERC-NEWS
+++ b/etc/ERC-NEWS
@@ -100,6 +100,18 @@ one's optionally accessible from the keyboard, just like any other
 side window.  Hit '<RET>' over a nick to spawn a "/QUERY" or a
 "Lastlog" (Occur) session.  See 'erc-nickbar-mode' for more.
 
+** New module to keep tabs on query pals who aren't in your channels.
+ERC has gotten a bit pickier about managing participants in query
+buffers.  "Untracked" correspondents no longer appear automatically in
+membership tables, even if you respond or initiate contact.  Instead,
+ERC only adds and removes participant data when these same users join
+and leave channels.  Anyone uncomfortable with the apparent
+uncertainty this brings can look to the new 'querypoll' module, which
+periodically sends WHO requests to keep track of correspondents.
+Those familiar with the IRCv3 Monitor extension can think of this as
+"fallback code" and a temporary placeholder for the real thing.
+Add 'querypoll' (and 'nickbar') to 'erc-modules' to try it out.
+
 ** Option 'erc-timestamp-use-align-to' more versatile.
 While this option has always offered to right-align stamps via the
 'display' text property, it's now more effective at doing so when set
@@ -685,6 +697,14 @@ The option 'erc-format-nick-function' has been renamed to
 actual role.  So too has the related function 'erc-format-nick', which
 is now 'erc-determine-speaker-from user'.
 
+*** All default response handlers return nil.
+Actually, this isn't yet true, but ERC is moving in this direction,
+with the aim of guaranteeing all response-handler hook members
+directly following a default handler always run.  In service of this
+goal, default handlers like 'erc-server-PONG' and 'erc-server-904'
+that may previously have returned non-nil have been updated to return
+nil in all cases.  User-defined default handlers should do the same.
+
 *** A template-based approach to formatting inserted chat messages.
 Predicting and influencing how ERC formats messages containing a
 leading "<speaker>" has never been straightforward.  The characters
diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index b644417e1ed..89e40ac8374 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -118,9 +118,13 @@ erc-nick
 (defvar erc-nick-change-attempt-count)
 (defvar erc-verbose-server-ping)
 
+(declare-function erc--ensure-query-member "erc" (name))
+(declare-function erc--ensure-query-members "erc" ())
 (declare-function erc--init-channel-modes "erc" (channel raw-args))
 (declare-function erc--open-target "erc" (target))
 (declare-function erc--parse-nuh "erc" (string))
+(declare-function erc--query-list "erc" ())
+(declare-function erc--remove-channel-users-but "erc" (nick))
 (declare-function erc--target-from-string "erc" (string))
 (declare-function erc--update-modes "erc" (raw-args))
 (declare-function erc-active-buffer "erc" nil)
@@ -1566,13 +1570,23 @@ define-erc-response-handler
    `erc-server-NAME'.
  - a function `erc-server-NAME' with body FN-BODY.
 
+\(Note that here, NAME merely refers to the parameter NAME rather than
+an actual IRC response or server-sent command.)
+
 If ALIASES is non-nil, each alias in ALIASES is `defalias'ed to
 `erc-server-NAME'.
 Alias hook variables are created as `erc-server-ALIAS-functions' and
 initialized to the same default value as `erc-server-NAME-functions'.
 
-FN-BODY is the body of `erc-server-NAME' it may refer to the two
-function arguments PROC and PARSED.
+ERC uses FN-BODY as the body of the default response handler
+`erc-server-NAME', which handles all incoming IRC \"NAME\" responses,
+unless overridden (see below).  ERC calls the function with two
+arguments, PROC and PARSED, whose symbols (lowercase) are bound to the
+current `erc-server-process' and `erc-response' instance within FN-BODY.
+Implementers should take care not to shadow them inadvertently.  In all
+cases, FN-BODY should return nil to allow third parties to run code
+after `erc-server-NAME' returns.  For historical reasons, ERC does not
+currently enforce this, however future versions very well may.
 
 If EXTRA-FN-DOC is non-nil, it is inserted at the beginning of the
 defined function's docstring.
@@ -1770,6 +1784,8 @@ erc--server-determine-join-display-context
                       (list 'JOIN ?n nick ?u login ?h host ?c chnl)))))
           (when buffer (set-buffer buffer))
           (erc-update-channel-member chnl nick nick t nil nil nil nil nil host login)
+          (unless (erc-current-nick-p nick)
+            (erc--ensure-query-member nick))
           ;; on join, we want to stay in the new channel buffer
           ;;(set-buffer ob)
           (apply #'erc-display-message parsed 'notice buffer args))))))
@@ -1782,7 +1798,6 @@ erc--server-determine-join-display-context
          (buffer (erc-get-buffer ch proc)))
     (pcase-let ((`(,nick ,login ,host)
                  (erc-parse-user (erc-response.sender parsed))))
-      (erc-remove-channel-member buffer tgt)
       (cond
        ((string= tgt (erc-current-nick))
         (erc-display-message
@@ -1791,17 +1806,20 @@ erc--server-determine-join-display-context
         (run-hook-with-args 'erc-kick-hook buffer)
         (erc-with-buffer
             (buffer)
-          (erc-remove-channel-users))
+          (erc--remove-channel-users-but tgt))
         (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
          parsed 'notice buffer
-         'KICK-by-you ?k tgt ?c ch ?r reason))
+         'KICK-by-you ?k tgt ?c ch ?r reason)
+        (erc-remove-channel-member buffer tgt))
        (t (erc-display-message
-             parsed 'notice buffer
-             'KICK ?k tgt ?n nick ?u login ?h host ?c ch ?r reason))))))
+           parsed 'notice buffer
+           'KICK ?k tgt ?n nick ?u login ?h host ?c ch ?r reason)
+          (erc-remove-channel-member buffer tgt)))))
+  nil)
 
 (define-erc-response-handler (MODE)
   "Handle server mode changes." nil
@@ -1829,17 +1847,19 @@ erc--server-determine-join-display-context
                                  ?h host ?t tgt ?m mode)))
       (erc-banlist-update proc parsed))))
 
-(defun erc--wrangle-query-buffers-on-nick-change (old new buffers)
+(defun erc--wrangle-query-buffers-on-nick-change (old new)
   "Create or reuse a query buffer for NEW nick after considering OLD nick.
-Return a (possibly updated) list of BUFFERS in which to announce the
-change."
+Return a list of buffers in which to announce the change."
+  ;; Note that `new-buffer' may be older than `old-buffer', e.g., if
+  ;; the query target is switching to a previously used nick.
   (let ((new-buffer (erc-get-buffer new erc-server-process))
         (old-buffer (erc-get-buffer old erc-server-process))
-        (selfp (string= old (erc-current-nick))))
+        (selfp (erc-current-nick-p old)) ; e.g., for note taking, etc.
+        buffers)
     (when new-buffer
       (push new-buffer buffers))
     (when old-buffer
-      (cl-pushnew old-buffer buffers)
+      (push old-buffer buffers)
       ;; Ensure the new nick is absent from the old query.
       (unless selfp
         (erc-remove-channel-member old-buffer old))
@@ -1872,11 +1892,14 @@ erc--wrangle-query-buffers-on-nick-change
       ;; erc-channel-users won't contain it
       ;;
       ;; Possibly still relevant: bug#12002
-      (setq bufs (erc--wrangle-query-buffers-on-nick-change nick nn bufs))
+      (dolist (buf (erc--wrangle-query-buffers-on-nick-change nick nn))
+        (cl-pushnew buf bufs))
       (erc-update-user-nick nick nn host login)
       (cond
        ((string= nick (erc-current-nick))
         (cl-pushnew (erc-server-buffer) bufs)
+        ;; Show message in all query buffers.
+        (setq bufs (append (erc--query-list) bufs))
         (erc-set-current-nick nn)
         ;; Rename session, possibly rename server buf and all targets
         (when erc-server-connected
@@ -1890,7 +1913,8 @@ erc--wrangle-query-buffers-on-nick-change
         (run-hook-with-args 'erc-nick-changed-functions nn nick))
        (t
         (when erc-server-connected
-          (erc-networks--id-reload erc-networks--id proc parsed))
+          (erc-networks--id-reload erc-networks--id proc parsed)
+          (erc--ensure-query-member nn))
         (erc-handle-user-status-change 'nick (list nick login host) (list nn))
         (erc-display-message parsed 'notice bufs 'NICK ?n nick
                              ?u login ?h host ?N nn))))))
@@ -1905,15 +1929,15 @@ erc--wrangle-query-buffers-on-nick-change
       ;; When `buffer' is nil, `erc-remove-channel-member' and
       ;; `erc-remove-channel-users' do almost nothing, and the message
       ;; is displayed in the server buffer.
-      (erc-remove-channel-member buffer nick)
       (erc-display-message parsed 'notice buffer
                            'PART ?n nick ?u login
                            ?h host ?c chnl ?r (or reason ""))
-      (when (string= nick (erc-current-nick))
+      (cond
+       ((string= nick (erc-current-nick))
         (run-hook-with-args 'erc-part-hook buffer)
         (erc-with-buffer
             (buffer)
-          (erc-remove-channel-users))
+          (erc--remove-channel-users-but nick))
         (with-suppressed-warnings ((obsolete erc-delete-default-channel))
           (erc-delete-default-channel chnl buffer))
         (erc-update-mode-line buffer)
@@ -1921,7 +1945,9 @@ erc--wrangle-query-buffers-on-nick-change
         (when (and erc-kill-buffer-on-part buffer)
           (defvar erc-killing-buffer-on-part-p)
           (let ((erc-killing-buffer-on-part-p t))
-            (kill-buffer buffer)))))))
+            (kill-buffer buffer))))
+       (t (erc-remove-channel-member buffer nick)))))
+  nil)
 
 (define-erc-response-handler (PING)
   "Handle ping messages." nil
@@ -1933,7 +1959,8 @@ erc--wrangle-query-buffers-on-nick-change
       (erc-display-message
        parsed 'error proc
        'PING ?s (erc-time-diff erc-server-last-ping-time (erc-current-time))))
-    (setq erc-server-last-ping-time (erc-current-time))))
+    (setq erc-server-last-ping-time (erc-current-time)))
+  nil)
 
 (define-erc-response-handler (PONG)
   "Handle pong messages." nil
@@ -2036,7 +2063,7 @@ erc--speaker-status-prefix-wanted-p
              (erc--speaker-status-prefix-wanted-p nil)
              (erc-current-message-catalog erc--message-speaker-catalog)
              ;;
-             buffer statusmsg cmem-prefix fnick)
+             finalize buffer statusmsg cmem-prefix fnick)
         (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))
@@ -2063,10 +2090,12 @@ erc--speaker-status-prefix-wanted-p
               (setq buffer (erc--open-target tgt))))))
         (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.
+            (when privp
+              (erc--unhide-prompt)
+              ;; Remove untracked query partners after display.
+              (unless (erc--get-server-user nick)
+                (setq finalize  (lambda ()
+                                  (erc-remove-channel-member buffer nick)))))
             (erc-update-channel-member (if privp nick tgt) nick nick
                                        privp nil nil nil nil nil host login nil nil t)
             (defvar erc--cmem-from-nick-function)
@@ -2105,20 +2134,27 @@ erc--speaker-status-prefix-wanted-p
                   (run-hook-with-args 'erc-echo-notice-always-hook
                                       fmtmsg parsed buffer nick)
                   (run-hook-with-args-until-success
-                   'erc-echo-notice-hook fmtmsg parsed buffer nick))))))))))
+                   'erc-echo-notice-hook fmtmsg parsed buffer nick)))))
+          (when finalize (funcall finalize)))
+        nil))))
 
 (define-erc-response-handler (QUIT)
   "Another user has quit IRC." nil
   (let ((reason (erc-response.contents parsed))
+        (erc--msg-prop-overrides erc--msg-prop-overrides)
         bufs)
     (pcase-let ((`(,nick ,login ,host)
                  (erc-parse-user (erc-response.sender parsed))))
       (setq bufs (erc-buffer-list-with-nick nick proc))
-      (erc-remove-user nick)
+      (when (erc-current-nick-p nick)
+        (setq bufs (append (erc--query-list) bufs))
+        (push '(erc--skip . (track)) erc--msg-prop-overrides))
       (setq reason (erc-wash-quit-reason reason nick login host))
       (erc-display-message parsed 'notice bufs
                            'QUIT ?n nick ?u login
-                           ?h host ?r reason))))
+                           ?h host ?r reason)
+      (erc-remove-user nick)))
+  nil)
 
 (define-erc-response-handler (TOPIC)
   "The channel topic has changed." nil
@@ -2312,6 +2348,9 @@ erc--with-isupport-data
 See `erc-display-server-message'." nil
   (erc-display-server-message proc parsed))
 
+(define-erc-response-handler (263) "RPL_TRYAGAIN." nil
+  (erc-handle-unknown-server-response proc parsed))
+
 (define-erc-response-handler (275)
   "Display secure connection message." nil
   (pcase-let ((`(,nick ,_user ,_message)
@@ -2364,7 +2403,7 @@ erc--with-isupport-data
         (catalog-entry (intern (format "s%s" (erc-response.command parsed)))))
     (pcase-let ((`(,nick ,user ,host)
                  (cdr (erc-response.command-args parsed))))
-      (erc-update-user-nick nick nick host nil fname user)
+      (erc-update-user-nick nick nick host user fname)
       (erc-display-message
        parsed 'notice 'active catalog-entry
        ?n nick ?f fname ?u user ?h host))))
@@ -2526,18 +2565,28 @@ erc-server-322-message
     (erc-display-message parsed 'notice (erc-get-buffer channel proc)
                          's341 ?n nick ?c channel)))
 
-;; FIXME update or add server user instead when channel is "*".
+(defun erc--extract-352-full-name (contents)
+  "Return full name from 352 trailing param, discarding hop count."
+  (pcase contents
+    ((rx (: bot (+ (any "0-9")) " ") (let full-name (group (* nonl))) eot)
+     full-name)
+    (_ contents)))
+
 (define-erc-response-handler (352)
-  "WHO notice." nil
-  (pcase-let ((`(,channel ,user ,host ,_server ,nick ,away-flag)
-               (cdr (erc-response.command-args parsed))))
-    (let ((full-name (erc-response.contents parsed)))
-      (when (string-match "\\(^[0-9]+ \\)\\(.*\\)$" full-name)
-        (setq full-name (match-string 2 full-name)))
-      (erc-update-channel-member channel nick nick nil nil nil nil nil nil host user full-name)
-      (erc-display-message parsed 'notice 'active 's352
-                           ?c channel ?n nick ?a away-flag
-                           ?u user ?h host ?f full-name))))
+  "RPL_WHOREPLY response." nil
+  (pcase-let*
+      ((`(,_ ,channel ,user ,host ,_server ,nick ,flags, hop-real)
+        (erc-response.command-args parsed))
+       (full-name (erc--extract-352-full-name hop-real))
+       (selfp (string= channel "*"))
+       (template (if selfp 's352-you 's352)))
+    (if selfp
+        (erc-update-user-nick nick nick host user full-name)
+      (erc-update-channel-member channel nick nick nil nil nil nil nil nil
+                                 host user full-name))
+    (erc-display-message parsed 'notice 'active template
+                         ?c channel ?n nick ?a flags
+                         ?u user ?h host ?f full-name)))
 
 (define-erc-response-handler (353)
   "NAMES notice." nil
@@ -2552,7 +2601,9 @@ erc-server-322-message
 (define-erc-response-handler (366)
   "End of NAMES." nil
   (erc-with-buffer ((cadr (erc-response.command-args parsed)) proc)
-    (erc-channel-end-receiving-names)))
+    (erc-channel-end-receiving-names))
+  (erc--ensure-query-members)
+  nil)
 
 (define-erc-response-handler (367)
   "Channel ban list entries." nil
@@ -2618,7 +2669,9 @@ erc-server-322-message
       (erc-log (format "cmd: WHOWAS: %s" nick/channel))
       (erc-server-send (format "WHOWAS %s 1" nick/channel)))
     (erc-display-message parsed '(notice error) 'active
-                         's401 ?n nick/channel)))
+                         's401 ?n nick/channel)
+    (unless (erc-channel-p nick/channel)
+      (erc-remove-user nick/channel))))
 
 (define-erc-response-handler (402)
   "No such server." nil
diff --git a/lisp/erc/erc-common.el b/lisp/erc/erc-common.el
index 4115e314b39..4ba7990ab98 100644
--- a/lisp/erc/erc-common.el
+++ b/lisp/erc/erc-common.el
@@ -216,7 +216,7 @@ erc--assemble-toggle
     `(defun ,ablsym ,(if localp `(&optional ,arg) '())
        ,(erc--fill-module-docstring
          (if val "Enable" "Disable")
-         " ERC " (symbol-name name) " mode."
+         " ERC " (symbol-name name) " mode" (and localp " locally") "."
          (when localp
            (concat "\nWhen called interactively,"
                    " do so in all buffers for the current connection.")))
@@ -413,11 +413,11 @@ define-erc-module
     `(progn
        (define-minor-mode
          ,mode
-         ,(erc--fill-module-docstring (format "Toggle ERC %s mode.
-With a prefix argument ARG, enable %s if ARG is positive,
+         ,(erc--fill-module-docstring (format "Toggle ERC %s mode%s.
+If called interactively, enable `%s' if ARG is positive,
 and disable it otherwise.  If called from Lisp, enable the mode
 if ARG is omitted or nil.
-\n%s" name name doc))
+\n%s" name (if local-p " locally" "") mode doc))
          :global ,(not local-p)
          :group (erc--find-group ',name ,(and alias (list 'quote alias)))
          ,@(unless local-p `(:require ',(erc--find-feature name alias)))
@@ -557,6 +557,21 @@ erc-get-server-user
      (gethash (erc-downcase ,nick)
               (erc-with-server-buffer erc-server-users)))))
 
+(defun erc--get-server-user (nick)
+  (erc-get-server-user nick))
+
+(define-inline erc--remove-user-from-targets (downcased-nick buffers)
+  "Remove DOWNCASED-NICK from `erc-channel-members' in BUFFERS."
+  (inline-quote
+   (progn
+     (defvar erc-channel-members-changed-hook)
+     (dolist (buffer ,buffers)
+       (when (buffer-live-p buffer)
+         (with-current-buffer buffer
+           (remhash ,downcased-nick erc-channel-users)
+           (when erc-channel-members-changed-hook
+             (run-hooks 'erc-channel-members-changed-hook))))))))
+
 (defmacro erc--with-dependent-type-match (type &rest features)
   "Massage Custom :type TYPE with :match function that pre-loads FEATURES."
   `(backquote-list* ',(car type)
diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index fe44c3bdfcb..180c5c3758e 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -1114,6 +1114,193 @@ erc-occur
              nil erc-server-process)))
   (multi-occur (erc-buffer-list nil proc) string))
 
+
+;;;; querypoll
+
+(declare-function ring-empty-p "ring" (ring))
+(declare-function ring-insert "ring" (ring item))
+(declare-function ring-insert+extend "ring" (ring item))
+(declare-function ring-length "ring" (ring))
+(declare-function ring-member "ring" (ring item))
+(declare-function ring-ref "ring" (ring index))
+(declare-function ring-remove "ring" (ring &optional index))
+
+(defvar-local erc--querypoll-ring nil)
+(defvar-local erc--querypoll-timer nil)
+
+(defcustom erc-querypoll-exclude-regexp
+  (rx bot (or (: "*" (+ nonl)) (: (+ (in "A-Za-z")) "Serv")) eot)
+  "Pattern to skip polling for bots and services you regularly query."
+  :group 'erc
+  :package-version '(ERC . "5.6")
+  :type 'regexp)
+
+;;;###autoload(autoload 'erc-querypoll-mode "erc-goodies" nil t)
+(define-erc-module querypoll nil
+  "Send periodic \"WHO\" requests for each query buffer.
+But omit query participants who share a channel with the client.
+
+Once ERC implements the `monitor' extension, this module will serve as
+an optional fallback for keeping query-participant rolls up to date on
+servers that lack support or are stingy with their allotments.  Until
+such time, this module should be considered experimental.
+
+This is a local ERC module, so selectively polling only a subset of
+query targets is possible but cumbersome.  To do so, ensure
+`erc-querypoll-mode' is enabled in the server buffer, and then toggle it
+as appropriate in desired query buffers.  To stop polling for the
+current connection, toggle off the command \\[erc-querypoll-mode] from a
+server buffer, or run \\`M-x C-u erc-querypoll-disable RET' from a
+target buffer."
+  ((if erc--target
+       (if (erc-query-buffer-p)
+           (progn ; accommodate those who eschew `erc-modules'
+             (erc-with-server-buffer
+               (unless erc-querypoll-mode
+                 (erc-querypoll-mode +1)))
+             (erc--querypoll-subscribe (current-buffer)))
+         (erc-querypoll-mode -1))
+     (setq-local erc--querypoll-ring (make-ring 5))
+     (erc-with-all-buffers-of-server erc-server-process nil
+       (unless erc-querypoll-mode
+         (erc-querypoll-mode +1)))))
+  ((when erc--querypoll-timer
+     (cancel-timer erc--querypoll-timer))
+   (if erc--target
+       (when-let (((erc-query-buffer-p))
+                  (ring (erc-with-server-buffer erc--querypoll-ring))
+                  (index (ring-member ring (current-buffer)))
+                  ((not (erc--querypoll-target-in-chan-p (current-buffer)))))
+         (ring-remove ring index)
+         (unless (erc-current-nick-p (erc-target))
+           (erc-remove-current-channel-member (erc-target))))
+     (erc-with-all-buffers-of-server erc-server-process #'erc-query-buffer-p
+       (erc-querypoll-mode -1)))
+   (kill-local-variable 'erc--querypoll-ring)
+   (kill-local-variable 'erc--querypoll-timer))
+  'local)
+
+(cl-defmethod erc--queries-current-p (&context (erc-querypoll-mode (eql t))) t)
+
+(defvar erc-querypoll-period-params '(10 10 1)
+  "Parameters affecting the delay with respect to the number of buffers.
+The elements represent some parameters of an exponential decay function,
+a(e)^{-x/b}+c.  The first number (a) affects the overall scaling.  A
+higher value means longer delays for all query buffers relative to queue
+length.  The second number (b) determines how quickly the delay
+decreases as the queue length increases.  Larger values make the delay
+taper off more gradually.  The last number (c) sets the minimum delay
+between updates regardless of queue length.")
+
+(defun erc--querypoll-compute-period (queue-size)
+  "Calculate delay based on QUEUE-SIZE."
+  (let ((scale (nth 0 erc-querypoll-period-params))
+        (rate (* 1.0 (nth 1 erc-querypoll-period-params)))
+        (min (nth 2 erc-querypoll-period-params)))
+    (+ (* scale (exp (/ (- queue-size) rate))) min)))
+
+(defun erc--querypoll-target-in-chan-p (buffer)
+  "Determine whether buffer's target, as a user, is joined to any channels."
+  (and-let*
+      ((target (erc--target-string (buffer-local-value 'erc--target buffer)))
+       (user (erc-get-server-user target))
+       (buffers (erc-server-user-buffers user))
+       ((seq-some #'erc-channel-p buffers)))))
+
+(defun erc--querypoll-get-length (ring)
+  "Return the effective length of RING, discounting chan members."
+  (let ((count 0))
+    (dotimes (i (ring-length ring))
+      (unless (erc--querypoll-target-in-chan-p (ring-ref ring i))
+        (cl-incf count 1)))
+    count))
+
+(defun erc--querypoll-get-next (ring)
+  (let ((n (ring-length ring)))
+    (catch 'found
+      (while (natnump (cl-decf n))
+        (when-let ((buffer (ring-remove ring))
+                   ((buffer-live-p buffer)))
+          ;; Push back buffers for users joined to some chan.
+          (if (erc--querypoll-target-in-chan-p buffer)
+              (ring-insert ring buffer)
+            (throw 'found buffer)))))))
+
+(defun erc--querypoll-subscribe (query-buffer &optional penalty)
+  "Add QUERY-BUFFER to FIFO and ensure timer is running."
+  (when query-buffer
+    (cl-assert (erc-query-buffer-p query-buffer)))
+  (erc-with-server-buffer
+    (when (and query-buffer
+               (not (with-current-buffer query-buffer
+                      (or (erc-current-nick-p (erc-target))
+                          (string-match erc-querypoll-exclude-regexp
+                                        (erc-target)))))
+               (not (ring-member erc--querypoll-ring query-buffer)))
+      (ring-insert+extend erc--querypoll-ring query-buffer))
+    (unless erc--querypoll-timer
+      (setq erc--querypoll-timer
+            (let* ((length (erc--querypoll-get-length erc--querypoll-ring))
+                   (period (erc--querypoll-compute-period length)))
+              (run-at-time (+ (or penalty 0) period)
+                           nil #'erc--querypoll-send (current-buffer)))))))
+
+(defun erc--querypoll-on-352 (target-nick args)
+  "Add or update `erc-server-users' data for TARGET-NICK from ARGS.
+Then add user to participant rolls in any existing query buffers."
+  (pcase-let
+      ((`(,_ ,channel ,login ,host ,_server ,nick ,_flags, hop-real) args))
+    (when (and (string= channel "*") (erc-nick-equal-p nick target-nick))
+      (if-let ((user (erc-get-server-user nick)))
+          (erc-update-user user nick host login
+                           (erc--extract-352-full-name hop-real))
+        ;; Don't add unless target is already known.
+        (when (erc-get-buffer nick erc-server-process)
+          (erc-add-server-user
+           nick (make-erc-server-user
+                 :nickname nick :login login :host host
+                 :full-name (erc--extract-352-full-name hop-real)))))
+      (erc--ensure-query-member nick)
+      t)))
+
+;; This uses heuristics to associate replies to the initial request
+;; because ERC does not yet support `labeled-response'.
+(defun erc--querypoll-send (server-buffer)
+  "Send a captive \"WHO\" in SERVER-BUFFER."
+  (when (and (buffer-live-p server-buffer)
+             (buffer-local-value 'erc-server-connected server-buffer))
+    (with-current-buffer server-buffer
+      (setq erc--querypoll-timer nil)
+      (if-let ((buffer (erc--querypoll-get-next erc--querypoll-ring)))
+          (letrec
+              ((target (erc--target-string
+                        (buffer-local-value 'erc--target buffer)))
+               (penalty 0)
+               (here-fn (erc-once-with-server-event
+                         "352" (lambda (_ parsed)
+                                 (erc--querypoll-on-352
+                                  target (erc-response.command-args parsed)))))
+               (done-fn (erc-once-with-server-event
+                         "315"
+                         (lambda (_ parsed)
+                           (if (memq here-fn erc-server-352-functions)
+                               (erc-remove-user
+                                (nth 1 (erc-response.command-args parsed)))
+                             (remove-hook 'erc-server-352-functions here-fn t))
+                           (remove-hook 'erc-server-263-functions fail-fn t)
+                           (remove-hook 'erc-server-315-functions done-fn t)
+                           (erc--querypoll-subscribe buffer penalty)
+                           t)))
+               (fail-fn (erc-once-with-server-event
+                         "263"
+                         (lambda (proc parsed)
+                           (setq penalty 60)
+                           (funcall done-fn proc parsed)
+                           t))))
+            (erc-server-send (concat "WHO " target)))
+        (unless (ring-empty-p erc--querypoll-ring)
+          (erc--querypoll-subscribe nil 30))))))
+
 (provide 'erc-goodies)
 
 ;;; erc-goodies.el ends here
diff --git a/lisp/erc/erc-sasl.el b/lisp/erc/erc-sasl.el
index f1cc68e2620..1998e4f129b 100644
--- a/lisp/erc/erc-sasl.el
+++ b/lisp/erc/erc-sasl.el
@@ -373,7 +373,8 @@ erc-sasl--destroy
   "Destroy process PROC and warn user that their settings are likely faulty."
   (delete-process proc)
   (erc--lwarn 'erc-sasl :error
-              "Disconnected from %s; please review SASL settings" proc))
+              "Disconnected from %s; please review SASL settings" proc)
+  nil)
 
 (define-erc-response-handler (902)
   "Handle an ERR_NICKLOCKED response." nil
diff --git a/lisp/erc/erc-speedbar.el b/lisp/erc/erc-speedbar.el
index b156f61d5d9..d4f91bb363a 100644
--- a/lisp/erc/erc-speedbar.el
+++ b/lisp/erc/erc-speedbar.el
@@ -133,7 +133,7 @@ erc-speedbar-browser
 (defun erc-speedbar-buttons (buffer)
   "Create buttons for speedbar in BUFFER."
   (erase-buffer)
-  (let (serverp chanp queryp)
+  (let (serverp chanp queryp queries-current-p)
     (with-current-buffer buffer
       ;; The function `dframe-help-echo' checks the default value of
       ;; `dframe-help-echo-function' when deciding whether to visit
@@ -145,10 +145,14 @@ erc-speedbar-buttons
       (setq-local dframe-help-echo-function #'ignore)
       (setq serverp (erc--server-buffer-p))
       (setq chanp (erc-channel-p (erc-default-target)))
-      (setq queryp (erc-query-buffer-p)))
-    (cond (serverp
+      (setq queryp (erc-query-buffer-p)
+            queries-current-p (erc--queries-current-p)))
+    (defvar erc-nickbar-mode)
+    (cond ((and erc-nickbar-mode (null (get-buffer-window speedbar-buffer)))
+           (run-at-time 0 nil #'erc-nickbar-mode -1))
+          (serverp
 	   (erc-speedbar-channel-buttons nil 0 buffer))
-	  (chanp
+          ((or chanp (and queryp queries-current-p))
 	   (erc-speedbar-insert-target buffer 0)
 	   (forward-line -1)
 	   (erc-speedbar-expand-channel "+" buffer 0))
@@ -202,7 +206,8 @@ erc-speedbar-channel-buttons
 	  t)))))
 
 (defun erc-speedbar-insert-target (buffer depth)
-  (if (erc--target-channel-p (buffer-local-value 'erc--target buffer))
+  (if (with-current-buffer buffer
+        (or (erc--target-channel-p erc--target) (erc--queries-current-p)))
       (progn
         (speedbar-make-tag-line
          'bracket ?+ 'erc-speedbar-expand-channel buffer
@@ -215,8 +220,9 @@ erc-speedbar-insert-target
             (speedbar-add-indicator (format "(%d)" (hash-table-count table)))
             (rx "(" (+ (any "0-9")) ")"))))
     ;; Query target
+    (cl-assert (erc-query-buffer-p buffer))
     (speedbar-make-tag-line
-     nil nil nil nil
+     'bracket ?? nil nil
      (buffer-name buffer) 'erc-speedbar-goto-buffer buffer nil
      depth)))
 
@@ -288,15 +294,9 @@ erc-speedbar--highlight-self-and-ops
                   'erc-current-nick-face
                 'erc-my-nick-face))
           (v v))
-      ;; FIXME overload `erc-channel-user-owner-p' and friends to
-      ;; accept an `erc-channel-user' object and replace this unrolled
-      ;; stuff with a single call to `erc-get-user-mode-prefix'.
-      (and cuser (or (erc-channel-user-owner cuser)
-                     (erc-channel-user-admin cuser)
-                     (erc-channel-user-op cuser)
-                     (erc-channel-user-halfop cuser)
-                     (erc-channel-user-voice cuser))
-           erc-button-nickname-face))))
+      (or (and cuser (not (zerop (erc-channel-user-status cuser)))
+               erc-button-nickname-face)
+          'erc-default-face))))
 
 (defun erc-speedbar--on-click (nick sbtoken _indent)
   ;; 0: finger, 1: name, 2: info, 3: buffer-name
@@ -447,7 +447,11 @@ erc-speedbar--buffer-options
     (speedbar-use-images . nil)
     (speedbar-hide-button-brackets-flag . t)))
 
-(defvar erc-speedbar--hidden-speedbar-frame nil)
+(defvar erc-speedbar--hidden-speedbar-frame nil
+  "The original `speedbar-frame', which `erc-nickbar-mode' deletes.
+It keeps a reference to it in order to run upstream teardown
+procedures without having to create a dummy frame for that
+purpose.")
 
 (defun erc-speedbar--emulate-sidebar-set-window-preserve-size ()
   (let ((erc-status-sidebar-buffer-name (buffer-name speedbar-buffer))
@@ -463,6 +467,7 @@ erc-speedbar--status-sidebar-mode--unhook
                #'erc-speedbar--emulate-sidebar-set-window-preserve-size))
 
 (defun erc-speedbar--emulate-sidebar ()
+  "Perform local setup for `erc-nickbar-mode' in a new `speedbar-buffer'."
   (require 'erc-status-sidebar)
   (cl-assert speedbar-frame)
   (cl-assert (eq speedbar-buffer (current-buffer)))
@@ -482,30 +487,32 @@ erc-speedbar--emulate-sidebar
       (add-function :around (local 'erc-speedbar--nick-face-function)
                     #'erc-speedbar--compose-nicks-face))))
 
-(defun erc-speedbar--toggle-nicknames-sidebar (arg)
-  (let ((force (numberp arg)))
-    (if speedbar-buffer
-        (progn
-          (cl-assert (buffer-live-p speedbar-buffer))
-          (if (or (and force (< arg 0))
-                  (and (not force) (get-buffer-window speedbar-buffer nil)))
-              ;; Close associated windows and stop updating but leave timer.
-              (progn
-                (dolist (window (get-buffer-window-list speedbar-buffer nil t))
-                  (unless (frame-root-window-p window)
-                    (when erc-speedbar--hidden-speedbar-frame
-                      (cl-assert
-                       (not (eq (window-frame window)
-                                erc-speedbar--hidden-speedbar-frame))))
-                    (delete-window window)))
-                (with-current-buffer speedbar-buffer
-                  (setq speedbar-update-flag nil)
-                  (speedbar-set-mode-line-format)))
-            (when (or (not force) (>= arg 0))
-              (with-selected-frame speedbar-frame
-                (erc-speedbar--emulate-sidebar-set-window-preserve-size)
-                (erc-speedbar-toggle-nicknames-window-lock -1)))))
-      (when-let (((or (not force) (>= arg 0)))
+(defun erc-speedbar--handle-delete-frame (event)
+  "Disable the nickbar if EVENT is deleting the proxy frame."
+  (when (and speedbar-frame
+             (cdr (frame-list))
+             (pcase event
+               (`(delete-frame (,frame)) (eq frame speedbar-frame))))
+    (erc-nickbar-mode -1)))
+
+(defun erc-speedbar--ensure (&optional forcep)
+  "Perform common setup for `erc-nickbar-mode'.
+Without FORCEP, return early when the calling context isn't
+associated with an ERC session."
+  (save-excursion
+    (when (or (erc-server-buffer) forcep)
+      (when erc-track-mode
+        (cl-pushnew '(derived-mode . speedbar-mode)
+                    erc-track--switch-fallback-blockers :test #'equal))
+      (unless speedbar-update-flag
+        (erc-button--display-error-notice-with-keys
+         (erc-server-buffer)
+         "Module `nickbar' needs `speedbar-update-flag' to be non-nil"
+         (and (not (display-graphic-p)) " in text terminals")
+         ". Setting to t for the current Emacs session."
+         " Customize it permanently to avoid this message.")
+        (setq speedbar-update-flag t))
+      (when-let (((null speedbar-buffer))
                  (speedbar-frame-parameters (backquote-list*
                                              '(visibility . nil)
                                              '(no-other-frame . t)
@@ -516,52 +523,45 @@ erc-speedbar--toggle-nicknames-sidebar
         ;; created twice.
         (speedbar-change-initial-expansion-list "ERC")
         (speedbar-frame-mode 1)
-        ;; If we put the remaining parts in the "create hook" along
-        ;; with everything else, the frame with `window-main-window'
-        ;; gets raised and steals focus if you've switched away from
-        ;; Emacs in the meantime.
-        (make-frame-invisible speedbar-frame)
-        (select-frame (setq speedbar-frame (previous-frame)))
+        ;; The setup steps below can't go in the "create hook" because
+        ;; the frame with `window-main-window' will be raised and
+        ;; steal focus if you switch away from Emacs in the meantime.
+        (let ((frame speedbar-frame))
+          (cl-assert (not (eq speedbar-frame (selected-frame))))
+          (select-frame (setq speedbar-frame (selected-frame)))
+          (delete-frame frame))
+        ;; Allow deleting (our) `speedbar-frame' with the mouse.
+        (with-current-buffer speedbar-buffer
+          (kill-local-variable 'dframe-delete-frame-function)
+          (setq dframe-delete-frame-function
+                #'erc-speedbar--handle-delete-frame)))
+      (with-selected-frame speedbar-frame
         (erc-speedbar--emulate-sidebar-set-window-preserve-size)
-        (erc-speedbar-toggle-nicknames-window-lock -1))))
-  (cl-assert (not (cdr (erc-speedbar--get-timers))) t))
-
-(defun erc-speedbar--ensure (&optional force)
-  (when (or (erc-server-buffer) force)
-    (when erc-track-mode
-      (cl-pushnew '(derived-mode . speedbar-mode)
-                  erc-track--switch-fallback-blockers :test #'equal))
-    (unless speedbar-update-flag
-      (erc-button--display-error-notice-with-keys
-       (erc-server-buffer)
-       "Module `nickbar' needs `speedbar-update-flag' to be non-nil"
-       (and (not (display-graphic-p)) " in text terminals")
-       ". Setting to t for the current Emacs session."
-       " Customize it permanently to avoid this message.")
-      (setq speedbar-update-flag t))
-    (erc-speedbar--toggle-nicknames-sidebar +1)
-    (with-current-buffer speedbar-buffer
-      (setq speedbar-update-flag t)
-      (speedbar-set-mode-line-format))))
+        (erc-speedbar-toggle-nicknames-window-lock -1))
+      (cl-assert (null (cdr (erc-speedbar--get-timers))))
+      (with-current-buffer speedbar-buffer
+        (setq speedbar-update-flag t)
+        (speedbar-set-mode-line-format)))))
 
-(defvar erc-speedbar--shutting-down-p nil)
-(defvar erc-speedbar--force-update-interval-secs 5 "Speedbar update period.")
+(defvar erc-speedbar--force-update-interval-secs 5
+  "Speedbar update period.")
 
 (defvar-local erc-speedbar--last-ran nil
   "When non-nil, a lisp timestamp updated when the speedbar timer runs.")
 
-(defun erc-speedbar--run-timer-on-post-insert ()
-  "Refresh speedbar if idle for `erc-speedbar--force-update-interval-secs'."
-  (when speedbar-buffer
+(defun erc-speedbar--prod-dframe-timer (&rest _)
+  "Refresh speedbar if dormant for `erc-speedbar--force-update-interval-secs'."
+  (when (buffer-live-p speedbar-buffer)
     (with-current-buffer speedbar-buffer
-      (when-let
-          ((dframe-timer)
-           ((erc--check-msg-prop 'erc--cmd 'PRIVMSG))
-           (interval erc-speedbar--force-update-interval-secs)
-           ((or (null erc-speedbar--last-ran)
-                (time-less-p erc-speedbar--last-ran
-                             (time-subtract (current-time) interval)))))
-        (run-at-time 0 nil #'dframe-timer-fn)))))
+      (when
+          (and dframe-timer
+               (or (null erc-speedbar--last-ran)
+                   (time-less-p erc-speedbar--last-ran
+                                (time-subtract
+                                 (current-time)
+                                 erc-speedbar--force-update-interval-secs))))
+        (run-at-time 0 nil #'dframe-timer-fn))))
+  nil)
 
 (defun erc-speedbar--reset-last-ran-on-timer ()
   "Reset `erc-speedbar--last-ran'."
@@ -574,42 +574,47 @@ nickbar
   "Show nicknames for current target buffer in a side window.
 When enabling, create a speedbar session if one doesn't exist and
 show its buffer in an `erc-status-sidebar' window instead of a
-separate frame.  When disabling, close the window or, with a
-negative prefix arg, destroy the session.
+separate frame.  If ERC doesn't yet have any live connections,
+defer activation until such time.  This means the variable
+`erc-nickbar-mode' may be t even though no actual speedbar yet
+exists.  When disabling, destroy the speedbar session.
 
 For controlling whether the speedbar window is selectable with
-`other-window', see `erc-nickbar-toggle-nicknames-window-lock'.
-Note that during initialization, this module may produce unwanted
-side effects, like the raising of frames or the stealing of input
-focus.  If you witness such a thing and can reproduce it, please
-file a bug report with \\[erc-bug]."
+`other-window', see `erc-nickbar-toggle-nicknames-window-lock'."
   ((add-hook 'erc--setup-buffer-hook #'erc-speedbar--ensure)
-   (add-hook 'erc-insert-post-hook #'erc-speedbar--run-timer-on-post-insert)
    (add-hook 'speedbar-timer-hook #'erc-speedbar--reset-last-ran-on-timer)
+   (add-hook 'erc-insert-post-hook #'erc-speedbar--prod-dframe-timer)
+   (add-hook 'erc-server-PONG-functions #'erc-speedbar--prod-dframe-timer)
    (erc-speedbar--ensure)
    (unless (or erc--updating-modules-p
-               (and-let* ((speedbar-buffer)
-                          (win (get-buffer-window speedbar-buffer 'all-frames))
-                          ((eq speedbar-frame (window-frame win))))))
+               (and speedbar-buffer
+                    (eq speedbar-frame
+                        (window-frame (get-buffer-window speedbar-buffer t)))))
      (when-let ((buf (or (and (derived-mode-p 'erc-mode) (current-buffer))
                          (car (erc-buffer-filter #'erc--server-buffer-p)))))
        (with-current-buffer buf
-         (erc-speedbar--ensure 'force)))))
+         (erc-speedbar--ensure 'forcep)))))
   ((remove-hook 'erc--setup-buffer-hook #'erc-speedbar--ensure)
-   (remove-hook 'erc-insert-post-hook #'erc-speedbar--run-timer-on-post-insert)
    (remove-hook 'speedbar-timer-hook #'erc-speedbar--reset-last-ran-on-timer)
+   (remove-hook 'erc-insert-post-hook #'erc-speedbar--prod-dframe-timer)
+   (remove-hook 'erc-server-PONG-functions #'erc-speedbar--prod-dframe-timer)
    (when erc-track-mode
      (setq erc-track--switch-fallback-blockers
            (remove '(derived-mode . speedbar-mode)
                    erc-track--switch-fallback-blockers)))
-   (erc-speedbar--toggle-nicknames-sidebar -1)
-   (when-let (((not erc-speedbar--shutting-down-p))
-              (arg erc--module-toggle-prefix-arg)
-              ((numberp arg))
-              ((< arg 0)))
-     (with-current-buffer speedbar-buffer
-       (dframe-close-frame)
-       (setq erc-speedbar--hidden-speedbar-frame nil)))))
+   (cl-assert speedbar-buffer)
+   ;; Close associated windows and stop updating but leave timer.
+   (dolist (window (get-buffer-window-list speedbar-buffer nil t))
+     (unless (frame-root-window-p window)
+       (when erc-speedbar--hidden-speedbar-frame
+         (cl-assert (not (eq (window-frame window)
+                             erc-speedbar--hidden-speedbar-frame))))
+       (delete-window window)))
+   (with-current-buffer speedbar-buffer
+     (setq speedbar-update-flag nil)
+     (speedbar-set-mode-line-format)
+     (unless (eq erc--module-toggle-prefix-arg most-negative-fixnum)
+       (dframe-close-frame)))))
 
 (defun erc-speedbar--get-timers ()
   (cl-remove #'dframe-timer-fn timer-idle-list
@@ -621,21 +626,18 @@ erc-speedbar--dframe-controlled
     (cl-assert (eq speedbar-buffer (current-buffer))))
   (when (and erc-speedbar--hidden-speedbar-frame (numberp arg) (< arg 0))
     (when erc-nickbar-mode
-      (let ((erc-speedbar--shutting-down-p t))
-        (erc-nickbar-mode -1)))
+      (erc-nickbar-mode most-negative-fixnum))
     (setq speedbar-frame erc-speedbar--hidden-speedbar-frame
           erc-speedbar--hidden-speedbar-frame nil)
-    ;; It's unknown whether leaving the frame invisible interferes
-    ;; with the upstream teardown sequence.
-    (when (display-graphic-p)
-      (make-frame-visible speedbar-frame))
     (speedbar-frame-mode arg) ; -1
     ;; As of Emacs 29, `dframe-set-timer' can't remove `dframe-timer'.
     (cl-assert (= 1 (length (erc-speedbar--get-timers))) t)
     (cancel-function-timers #'dframe-timer-fn)
     ;; `dframe-close-frame' kills the buffer but no function in
     ;; erc-speedbar.el resets this to nil.
-    (setq speedbar-buffer nil)))
+    (setq erc-speedbar--hidden-speedbar-frame nil
+          speedbar-buffer nil
+          speedbar-frame nil)))
 
 (defun erc-speedbar-toggle-nicknames-window-lock (arg)
   "Toggle whether nicknames window is selectable with \\[other-window].
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 1665b2cacf7..8ec5a54ab3b 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -509,7 +509,7 @@ erc-connect-pre-hook
 
 (defvaralias 'erc-channel-users 'erc-channel-members)
 (defvar-local erc-channel-members nil
-  "Hash table of members in the current channel.
+  "Hash table of members in the current channel or query buffer.
 It associates nicknames with cons cells of the form
 \(SERVER-USER . MEMBER-DATA), where SERVER-USER is a
 `erc-server-user' object and MEMBER-DATA is a `erc-channel-user'
@@ -549,14 +549,33 @@ erc-add-server-user
   (erc-with-server-buffer
     (puthash (erc-downcase nick) user erc-server-users)))
 
-(defun erc-remove-server-user (nick)
-  "This function is for internal use only.
+(cl-defmethod erc--queries-current-p ()
+  "Return non-nil if ERC actively updates query manifests."
+  (and (erc-query-buffer-p) (erc-get-channel-member (erc-target))))
+
+(defun erc--ensure-query-member (nick)
+  "Populate membership table in query buffer for online NICK."
+  (erc-with-buffer (nick)
+    (when-let (((zerop (hash-table-count erc-channel-users)))
+               (user (erc-get-server-user nick)))
+      (erc-update-current-channel-member nick nil t)
+      (erc--unhide-prompt)
+      t)))
 
-Removes the user with nickname NICK from the `erc-server-users'
-hash table.  This user is not removed from the
-`erc-channel-users' lists of other buffers.
+(defun erc--ensure-query-members ()
+  "Update membership tables in all query buffers.
+Ensure targets with an entry in `erc-server-users' are present in
+`erc-channel-members'."
+  (erc-with-all-buffers-of-server erc-server-process #'erc-query-buffer-p
+    (when-let (((zerop (hash-table-count erc-channel-users)))
+               (target (erc-target))
+               ((erc-get-server-user target)))
+      (erc-update-current-channel-member target nil t)
+      (erc--unhide-prompt))
+    erc-server-process))
 
-See also: `erc-remove-user'."
+(defun erc-remove-server-user (nick)
+  "Remove NICK from the session's `erc-server-users' table."
   (erc-with-server-buffer
     (remhash (erc-downcase nick) erc-server-users)))
 
@@ -579,15 +598,27 @@ erc-change-user-nickname
               (puthash (erc-downcase new-nick) cdata
                        erc-channel-users)))))))
 
-(defun erc-remove-channel-user (nick)
-  "This function is for internal use only.
-
-Removes the user with nickname NICK from the `erc-channel-users'
-list for this channel.  If this user is not in the
-`erc-channel-users' list of any other buffers, the user is also
-removed from the server's `erc-server-users' list.
+(defvar erc--forget-server-user-function
+  #'erc--forget-server-user-ignoring-queries
+  "Function to conditionally remove a user from `erc-server-users'.
+Called with a nick and its `erc-server-user' object.")
+
+(defun erc--forget-server-user (nick user)
+  "Remove NICK's USER from server table if they're not in any target buffers."
+  (unless (erc-server-user-buffers user)
+    (erc-remove-server-user nick)))
+
+(defun erc--forget-server-user-ignoring-queries (nick user)
+  "Remove NICK's USER from `erc-server-users' if they've parted all channels."
+  (let ((buffers (erc-server-user-buffers user)))
+    (when (or (null buffers) (cl-every #'erc-query-buffer-p buffers))
+      (when buffers
+        (erc--remove-user-from-targets (erc-downcase nick) buffers))
+      (erc-remove-server-user nick))))
 
-See also: `erc-remove-server-user' and `erc-remove-user'."
+(defun erc-remove-channel-user (nick)
+  "Remove NICK from the current target buffer's `erc-channel-members'.
+If this was their only target, also remove them from `erc-server-users'."
   (let ((channel-data (erc-get-channel-user nick)))
     (when channel-data
       (let ((user (car channel-data)))
@@ -595,32 +626,19 @@ erc-remove-channel-user
               (delq (current-buffer)
                     (erc-server-user-buffers user)))
         (remhash (erc-downcase nick) erc-channel-users)
-        (if (null (erc-server-user-buffers user))
-            (erc-remove-server-user nick))))))
+        (funcall erc--forget-server-user-function nick user)))))
 
 (defun erc-remove-user (nick)
-  "This function is for internal use only.
-
-Removes the user with nickname NICK from the `erc-server-users'
-list as well as from all `erc-channel-users' lists.
-
-See also: `erc-remove-server-user' and
-`erc-remove-channel-user'."
+  "Remove NICK from the server and all relevant channels tables."
   (let ((user (erc-get-server-user nick)))
     (when user
-      (let ((buffers (erc-server-user-buffers user)))
-        (dolist (buf buffers)
-          (if (buffer-live-p buf)
-              (with-current-buffer buf
-                (remhash (erc-downcase nick) erc-channel-users)
-                (run-hooks 'erc-channel-members-changed-hook)))))
+      (erc--remove-user-from-targets (erc-downcase nick)
+                                     (erc-server-user-buffers user))
       (erc-remove-server-user nick))))
 
 (defun erc-remove-channel-users ()
-  "This function is for internal use only.
-
-Removes all users in the current channel.  This is called by
-`erc-server-PART' and `erc-server-QUIT'."
+  "Drain current buffer's `erc-channel-members' table.
+Also remove members from the server table if this was their only buffer."
   (when (erc--target-channel-p erc--target)
     (setf (erc--target-channel-joined-p erc--target) nil))
   (when (and erc-server-connected
@@ -631,6 +649,17 @@ erc-remove-channel-users
              erc-channel-users)
     (clrhash erc-channel-users)))
 
+(defun erc--remove-channel-users-but (nick)
+  "Drain channel users and remove from server, sparing NICK."
+  (when-let ((users (erc-with-server-buffer erc-server-users))
+             (my-user (gethash (erc-downcase nick) users))
+             (original-function erc--forget-server-user-function)
+             (erc--forget-server-user-function
+              (lambda (nick user)
+                (unless (eq user my-user)
+                  (funcall original-function nick user)))))
+    (erc-remove-channel-users)))
+
 (defmacro erc--define-channel-user-status-compat-getter (name c d)
   "Define a gv getter for historical `erc-channel-user' status slot NAME.
 Expect NAME to be a string, C to be its traditionally associated
@@ -2151,6 +2180,10 @@ erc-buffer-list-with-nick
           (erc-server-user-buffers user)
         nil))))
 
+(defun erc--query-list ()
+  "Return all query buffers for the current connection."
+  (erc-buffer-list #'erc-query-buffer-p erc-server-process))
+
 ;; Some local variables
 
 ;; TODO eventually deprecate this variable
@@ -3987,7 +4020,9 @@ erc-display-message
 instead of lower-level ones, like `erc-insert-line', to insert
 arbitrary informative messages as if sent by the server.  That
 is, tell modules to treat a \"local\" message for which PARSED is
-nil like any other server-sent message."
+nil like any other server-sent message.  Finally, expect users to
+treat the return value of this function as undefined even though
+various default response handlers may appear to presume nil."
   (let* ((erc--msg-props
           (or erc--msg-props
               (let ((table (make-hash-table))
@@ -5149,8 +5184,7 @@ erc-cmd-QUOTE
 
 (defun erc-cmd-QUERY (&optional user)
   "Open a query with USER.
-How the query is displayed (in a new window, frame, etc.) depends
-on the value of `erc-interactive-display'."
+Display the query buffer in accordance with `erc-interactive-display'."
   ;; FIXME: The doc string used to say at the end:
   ;; "If USER is omitted, close the current query buffer if one exists
   ;; - except this is broken now ;-)"
@@ -5166,7 +5200,11 @@ erc-cmd-QUERY
         (erc--display-context `((erc-interactive-display . /QUERY)
                                 ,@erc--display-context)))
     (erc-with-server-buffer
-     (erc--open-target user))))
+      (if-let ((buffer (erc-get-buffer user erc-server-process)))
+          (prog1 buffer
+            (erc-setup-buffer buffer))
+        (prog1 (erc--open-target user) ; becomes current buffer
+          (erc--ensure-query-member user))))))
 
 (defalias 'erc-cmd-Q #'erc-cmd-QUERY)
 
@@ -5902,23 +5940,19 @@ erc-debug-missing-hooks
   nil)
 
 (defun erc--open-target (target)
-  "Open an ERC buffer on TARGET and return the buffer.
-Ensure own nick is present in the buffer's `erc-channel-members'."
-  (let ((buffer (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))))
-    (prog1 buffer
-      (when (erc-query-buffer-p buffer)
-        (erc-update-channel-member target (erc-current-nick) nil t)))))
+  "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.
@@ -9523,6 +9557,7 @@ english
    (s333   . "%c: topic set by %n, %t")
    (s341   . "Inviting %n to channel %c")
    (s352   . "%-11c %-10n %-4a %u@%h (%f)")
+   (s352-you . "%n %a %u@%h (%f)")
    (s353   . "Users on %c: %u")
    (s367   . "Ban for %b on %c")
    (s367-set-by . "Ban for %b on %c set by %s on %t")
@@ -9630,7 +9665,7 @@ erc-kill-channel-hook
     erc-networks-shrink-ids-and-buffer-names
     erc-networks-rename-surviving-target-buffer)
   "Invoked whenever a channel-buffer is killed via `kill-buffer'."
-  :package-version '(ERC . "5.5")
+  :package-version '(ERC . "5.6") ; FIXME sync on release
   :group 'erc-hooks
   :type 'hook)
 
@@ -9654,7 +9689,9 @@ erc-kill-buffer-function
 `erc-kill-channel-hook' if a channel buffer was killed,
 or `erc-kill-buffer-hook' if any other buffer."
   (when (eq major-mode 'erc-mode)
-    (erc-remove-channel-users)
+    (when-let ((erc--target)
+               (nick (erc-current-nick)))
+      (erc--remove-channel-users-but nick))
     (cond
      ((eq (erc-server-buffer) (current-buffer))
       (run-hooks 'erc-kill-server-hook))
diff --git a/test/lisp/erc/erc-goodies-tests.el b/test/lisp/erc/erc-goodies-tests.el
index 7cbaa39d3f7..ead0bf5a979 100644
--- a/test/lisp/erc/erc-goodies-tests.el
+++ b/test/lisp/erc/erc-goodies-tests.el
@@ -609,4 +609,61 @@ erc--get-inserted-msg-bounds/readonly
      (should (equal '(3 . 11) (erc--get-inserted-msg-bounds arg))))))
 
 
+;;;; querypoll
+
+(ert-deftest erc--querypoll-compute-period ()
+  (should (equal (mapcar (lambda (i)
+                           (/ (round (* 100 (erc--querypoll-compute-period i)))
+                              100.0))
+                         (number-sequence 0 10))
+                 '(11.0 10.05 9.19 8.41 7.7 7.07 6.49 5.97 5.49 5.07 4.68))))
+
+(declare-function ring-insert "ring" (ring item))
+
+(ert-deftest erc--querypoll-target-in-chan-p ()
+  (erc-tests-common-make-server-buf)
+  (with-current-buffer (erc--open-target "#chan")
+    (erc-update-current-channel-member "bob" "bob" 'addp))
+
+  (with-current-buffer (erc--open-target "bob")
+    (should (erc--querypoll-target-in-chan-p (current-buffer))))
+
+  (with-current-buffer (erc--open-target "alice")
+    (should-not (erc--querypoll-target-in-chan-p (current-buffer))))
+
+  (when noninteractive
+    (erc-tests-common-kill-buffers)))
+
+(ert-deftest erc--querypoll-get-length ()
+  (erc-tests-common-make-server-buf)
+  (with-current-buffer (erc--open-target "#chan")
+    (erc-update-current-channel-member "bob" "bob" 'addp))
+
+  (let ((ring (make-ring 5)))
+    (ring-insert ring (with-current-buffer (erc--open-target "bob")))
+    (should (= 0 (erc--querypoll-get-length ring)))
+    (ring-insert ring (with-current-buffer (erc--open-target "alice")))
+    (should (= 1 (erc--querypoll-get-length ring))))
+
+  (when noninteractive
+    (erc-tests-common-kill-buffers)))
+
+(ert-deftest erc--querypoll-get-next ()
+  (erc-tests-common-make-server-buf)
+  (with-current-buffer (erc--open-target "#chan")
+    (erc-update-current-channel-member "bob" "bob" 'addp)
+    (erc-update-current-channel-member "alice" "alice" 'addp))
+
+  (let ((ring (make-ring 5)))
+    (ring-insert ring (with-current-buffer (erc--open-target "bob")))
+    (ring-insert ring (with-current-buffer (erc--open-target "dummy")))
+    (ring-insert ring (with-current-buffer (erc--open-target "alice")))
+    (ring-insert ring (with-current-buffer (erc--open-target "tester")))
+    (kill-buffer (get-buffer "dummy"))
+
+    (should (eq (get-buffer "tester") (erc--querypoll-get-next ring))))
+
+  (when noninteractive
+    (erc-tests-common-kill-buffers)))
+
 ;;; erc-goodies-tests.el ends here
diff --git a/test/lisp/erc/erc-networks-tests.el b/test/lisp/erc/erc-networks-tests.el
index 90d6f13f2f6..f0a7c37ddf2 100644
--- a/test/lisp/erc/erc-networks-tests.el
+++ b/test/lisp/erc/erc-networks-tests.el
@@ -1199,7 +1199,7 @@ erc-networks--set-name
       (erc-mode)
 
       (cl-letf (((symbol-function 'erc--route-insertion)
-                 (lambda (&rest r) (push r calls))))
+                 (lambda (&rest r) (ignore (push r calls)))))
 
         (ert-info ("Signals when `erc-server-announced-name' unset")
           (should-error (erc-networks--set-name nil (make-erc-response)))
diff --git a/test/lisp/erc/erc-scenarios-base-renick.el b/test/lisp/erc/erc-scenarios-base-renick.el
index 290230259cb..866075e0b3b 100644
--- a/test/lisp/erc/erc-scenarios-base-renick.el
+++ b/test/lisp/erc/erc-scenarios-base-renick.el
@@ -185,21 +185,43 @@ erc-scenarios-base-renick-queries-solo
       (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "Lal"))
         (funcall expect 10 "<Lal> hello")
         (erc-scenarios-common-say "hi")
+        (should-not (erc-get-channel-member "tester"))
         (funcall expect 10 "is now known as Linguo")
         ;; No duplicate message.
         (funcall expect -0.1 "is now known as Linguo")
         ;; No duplicate buffer.
         (erc-d-t-wait-for 1 (equal (buffer-name) "Linguo"))
         (should-not (get-buffer "Lal"))
+        ;; Channel member has been updated
+        (should-not (erc-get-channel-member "Lal"))
+        (should-not (erc-get-server-user "Lal"))
+        (should (erc-get-channel-member "Linguo"))
         (erc-scenarios-common-say "howdy Linguo")))
 
     (with-current-buffer "#foo"
       (funcall expect 10 "is now known as Linguo")
       (funcall expect -0.1 "is now known as Linguo")
+      (funcall expect 10 "has left"))
+
+    ;; User parting a common channel removes them from queries.
+    (with-current-buffer "Linguo"
+      (should-not (erc-get-channel-member "tester"))
+      (erc-d-t-wait-for 10 (null (erc-get-channel-member "Linguo")))
+      (should-not (erc-get-server-user "Linguo")))
+
+    ;; Leaving the client's only channel doesn't remove its user data
+    ;; from the server table (see below, after "get along ...").
+    (with-current-buffer "#foo"
       (erc-scenarios-common-say "/part"))
 
+    ;; Server and "channel" user are *not* (re)created upon receiving
+    ;; a direct message for a user we already have an open query with
+    ;; but with whom we no longer share a channel.
     (with-current-buffer "Linguo"
-      (funcall expect 10 "get along"))))
+      (funcall expect 10 "get along")
+      (should-not (erc-get-channel-member "Linguo"))
+      (should-not (erc-get-channel-member "tester"))
+      (should (erc-get-server-user "tester")))))
 
 ;; Someone you have a query with disconnects and reconnects under a
 ;; new nick (perhaps due to their client appending a backtick or
@@ -332,7 +354,7 @@ erc-scenarios-base-renick-self/merge-query
       ;; Goto last message from previous session.
       (funcall expect 10 "has quit" (point-min))
       (funcall expect -0.01 "\n\n[") ; duplicate date stamp removed
-      (funcall expect 1 (concat "*** Grafted buffer `observer@foonet/dummy'"
+      (funcall expect 1 (concat "*** Grafting buffer `observer@foonet/dummy'"
                                 " onto `observer@foonet/tester'"))
       (funcall expect 1 "<dummy> hola")
       (funcall expect 1 "<observer> whodis?")
@@ -344,14 +366,14 @@ erc-scenarios-base-renick-self/merge-query
       ;; Goto last assertion.
       (funcall expect 10 "*** ERC finished ***" (point-min))
       (funcall expect -0.01 "\n\n[") ; duplicate date stamp removed
-      (funcall expect 10 "Grafted buffer `foonet/dummy' onto `foonet/tester'"))
+      (funcall expect 5 "Grafting buffer `foonet/dummy' onto `foonet/tester'"))
 
     (with-current-buffer "#chan"
       (should-not (get-buffer "#chan@foonet/dummy"))
       (should-not (get-buffer "#chan@foonet/tester"))
       (funcall expect 10 "has quit" (point-min))
       (funcall expect -0.01 "\n\n[") ; duplicate date stamp removed
-      (funcall expect 1 (concat "*** Grafted buffer `#chan@foonet/dummy'"
+      (funcall expect 1 (concat "*** Grafting buffer `#chan@foonet/dummy'"
                                 " onto `#chan@foonet/tester'"))
       (funcall expect 1 "You have joined channel #chan")
       (funcall expect 1 "<bob> alice: Have here bereft")
diff --git a/test/lisp/erc/erc-scenarios-status-sidebar.el b/test/lisp/erc/erc-scenarios-status-sidebar.el
index 2523ff9ee46..4cec00e2312 100644
--- a/test/lisp/erc/erc-scenarios-status-sidebar.el
+++ b/test/lisp/erc/erc-scenarios-status-sidebar.el
@@ -98,12 +98,14 @@ erc-scenarios-status-sidebar--bufbar
 (defvar erc-nickbar-mode)
 (defvar speedbar-buffer)
 
+;; FIXME move to own file because it takes 20+ seconds, uncompiled.
 (ert-deftest erc-scenarios-status-sidebar--nickbar ()
   :tags `(:expensive-test :unstable ,@(and (getenv "ERC_TESTS_GRAPHICAL")
                                            '(:erc--graphical)))
-  (when noninteractive (ert-skip "Interactive only"))
+  (when (and noninteractive (= emacs-major-version 27))
+    (ert-skip "Hangs on Emacs 27, asking for input"))
 
-  (erc-scenarios-common-with-cleanup
+  (erc-scenarios-common-with-noninteractive-in-term
       ((erc-scenarios-common-dialog "base/gapless-connect")
        (erc-server-flood-penalty 0.1)
        (erc-server-flood-penalty erc-server-flood-penalty)
@@ -156,14 +158,14 @@ erc-scenarios-status-sidebar--nickbar
         ;; etc. for testing commands that call those same functions.
         (call-interactively #'erc-nickbar-mode)
         (should-not erc-nickbar-mode)
-        (should-not (and speedbar-buffer
-                         (get-buffer-window speedbar-buffer)))
-        (should speedbar-buffer)
+        (should-not speedbar-buffer)
+        (should-not (get-buffer " SPEEDBAR"))
 
         (erc-nickbar-mode +1)
-        (should (and speedbar-buffer
-                     (get-buffer-window speedbar-buffer)))
+        (should (and speedbar-buffer (get-buffer-window speedbar-buffer)))
+        (should (eq speedbar-buffer (get-buffer " SPEEDBAR")))
         (should (get-buffer " SPEEDBAR"))
+
         (erc-nickbar-mode -1)
         (should-not (get-buffer " SPEEDBAR"))
         (should-not erc-nickbar-mode)
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 999d9f100c9..7bd5479f524 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -330,6 +330,7 @@ erc--refresh-prompt
                                (cl-incf counter))))
          erc-accidental-paste-threshold-seconds
          erc-insert-modify-hook
+         erc-send-modify-hook
          (erc-last-input-time 0)
          (erc-modules (remq 'stamp erc-modules))
          (erc-send-input-line-function #'ignore)
@@ -2533,7 +2534,7 @@ erc-message
         erc-kill-channel-hook erc-kill-server-hook erc-kill-buffer-hook)
     (cl-letf (((symbol-function 'erc-display-message)
                (lambda (_ _ _ msg &rest args)
-                 (push (apply #'erc-format-message msg args) calls)))
+                 (ignore (push (apply #'erc-format-message msg args) calls))))
               ((symbol-function 'erc-server-send)
                (lambda (line _) (push line calls)))
               ((symbol-function 'erc-server-buffer)
@@ -3665,9 +3666,9 @@ define-erc-module--global
 
                       (define-minor-mode erc-mname-mode
                         "Toggle ERC mname mode.
-With a prefix argument ARG, enable mname if ARG is positive, and
-disable it otherwise.  If called from Lisp, enable the mode if
-ARG is omitted or nil.
+If called interactively, enable `erc-mname-mode' if ARG is
+positive, and disable it otherwise.  If called from Lisp, enable
+the mode if ARG is omitted or nil.
 
 Some docstring."
                         :global t
@@ -3722,10 +3723,10 @@ define-erc-module--local
     (should (equal got
                    `(progn
                       (define-minor-mode erc-mname-mode
-                        "Toggle ERC mname mode.
-With a prefix argument ARG, enable mname if ARG is positive, and
-disable it otherwise.  If called from Lisp, enable the mode if
-ARG is omitted or nil.
+                        "Toggle ERC mname mode locally.
+If called interactively, enable `erc-mname-mode' if ARG is
+positive, and disable it otherwise.  If called from Lisp, enable
+the mode if ARG is omitted or nil.
 
 Some docstring."
                         :global nil
@@ -3736,7 +3737,7 @@ define-erc-module--local
                             (erc-mname-disable))))
 
                       (defun erc-mname-enable (&optional ,arg-en)
-                        "Enable ERC mname mode.
+                        "Enable ERC mname mode locally.
 When called interactively, do so in all buffers for the current
 connection."
                         (interactive "p")
@@ -3749,7 +3750,7 @@ define-erc-module--local
                             (ignore a) (ignore b))))
 
                       (defun erc-mname-disable (&optional ,arg-dis)
-                        "Disable ERC mname mode.
+                        "Disable ERC mname mode locally.
 When called interactively, do so in all buffers for the current
 connection."
                         (interactive "p")
diff --git a/test/lisp/erc/resources/base/reconnect/options-again.eld b/test/lisp/erc/resources/base/reconnect/options-again.eld
index 8a3264fda9c..a3a86fb7100 100644
--- a/test/lisp/erc/resources/base/reconnect/options-again.eld
+++ b/test/lisp/erc/resources/base/reconnect/options-again.eld
@@ -18,7 +18,7 @@
  (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")
+((mode-user 10 "MODE tester +i")
  (0 ":irc.foonet.org 221 tester +i")
  (0 ":irc.foonet.org NOTICE tester :This server is still in debug mode."))
 
-- 
2.45.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0001-5.6-Return-nil-from-more-ERC-response-handlers.patch --]
[-- Type: text/x-patch, Size: 7848 bytes --]

From 016fda6c9836a326d251c4343880da1a99861ded Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Sun, 19 May 2024 23:04:49 -0700
Subject: [PATCH 1/7] [5.6] Return nil from more ERC response handlers

* etc/ERC-NEWS: Mention that some non-nil returning default response
handlers now return nil.
* lisp/erc/erc-backend.el (define-erc-response-handler): Mention that
body should explicitly return nil.
(erc-server-PING): Return nil.
* lisp/erc/erc-sasl.el (erc-sasl--destroy): Return nil.
* lisp/erc/erc.el (erc-display-message): Mention in doc string that
the return value is undefined.
(erc-kill-channel-hook): Fix package-version.
* test/lisp/erc/erc-networks-tests.el (erc-networks--set-name): Ensure
`erc--route-insertion' returns nil because this influences whether
response-handler hooks continue running.
* test/lisp/erc/erc-tests.el (erc-message): Don't return non-nil in
mocked `erc-display-message'.
(erc-send-modify-hook): Shadow `erc-send-modify-hook' because
erc-stamp--date-mode' adds to it locally.
---
 etc/ERC-NEWS                        |  8 ++++++++
 lisp/erc/erc-backend.el             | 20 ++++++++++++++++----
 lisp/erc/erc-sasl.el                |  3 ++-
 lisp/erc/erc.el                     |  6 ++++--
 test/lisp/erc/erc-networks-tests.el |  2 +-
 test/lisp/erc/erc-tests.el          |  3 ++-
 6 files changed, 33 insertions(+), 9 deletions(-)

diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS
index 62970f52396..07e9608c836 100644
--- a/etc/ERC-NEWS
+++ b/etc/ERC-NEWS
@@ -685,6 +685,14 @@ The option 'erc-format-nick-function' has been renamed to
 actual role.  So too has the related function 'erc-format-nick', which
 is now 'erc-determine-speaker-from user'.
 
+*** All default response handlers return nil.
+Actually, this isn't yet true, but ERC is moving in this direction,
+with the aim of guaranteeing all response-handler hook members
+directly following a default handler always run.  In service of this
+goal, default handlers like 'erc-server-PONG' and 'erc-server-904'
+that may previously have returned non-nil have been updated to return
+nil in all cases.  User-defined default handlers should do the same.
+
 *** A template-based approach to formatting inserted chat messages.
 Predicting and influencing how ERC formats messages containing a
 leading "<speaker>" has never been straightforward.  The characters
diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index ef99d762a07..90c46eadaf4 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -1566,13 +1566,23 @@ define-erc-response-handler
    `erc-server-NAME'.
  - a function `erc-server-NAME' with body FN-BODY.
 
+\(Note that here, NAME merely refers to the parameter NAME rather than
+an actual IRC response or server-sent command.)
+
 If ALIASES is non-nil, each alias in ALIASES is `defalias'ed to
 `erc-server-NAME'.
 Alias hook variables are created as `erc-server-ALIAS-functions' and
 initialized to the same default value as `erc-server-NAME-functions'.
 
-FN-BODY is the body of `erc-server-NAME' it may refer to the two
-function arguments PROC and PARSED.
+ERC uses FN-BODY as the body of the default response handler
+`erc-server-NAME', which handles all incoming IRC \"NAME\" responses,
+unless overridden (see below).  ERC calls the function with two
+arguments, PROC and PARSED, whose symbols (lowercase) are bound to the
+current `erc-server-process' and `erc-response' instance within FN-BODY.
+Implementers should take care not to shadow them inadvertently.  In all
+cases, FN-BODY should return nil to allow third parties to run code
+after `erc-server-NAME' returns.  For historical reasons, ERC does not
+currently enforce this, however future versions very well may.
 
 If EXTRA-FN-DOC is non-nil, it is inserted at the beginning of the
 defined function's docstring.
@@ -1902,7 +1912,8 @@ erc--server-determine-join-display-context
         (when (and erc-kill-buffer-on-part buffer)
           (defvar erc-killing-buffer-on-part-p)
           (let ((erc-killing-buffer-on-part-p t))
-            (kill-buffer buffer)))))))
+            (kill-buffer buffer))))))
+  nil)
 
 (define-erc-response-handler (PING)
   "Handle ping messages." nil
@@ -1914,7 +1925,8 @@ erc--server-determine-join-display-context
       (erc-display-message
        parsed 'error proc
        'PING ?s (erc-time-diff erc-server-last-ping-time (erc-current-time))))
-    (setq erc-server-last-ping-time (erc-current-time))))
+    (setq erc-server-last-ping-time (erc-current-time)))
+  nil)
 
 (define-erc-response-handler (PONG)
   "Handle pong messages." nil
diff --git a/lisp/erc/erc-sasl.el b/lisp/erc/erc-sasl.el
index f1cc68e2620..1998e4f129b 100644
--- a/lisp/erc/erc-sasl.el
+++ b/lisp/erc/erc-sasl.el
@@ -373,7 +373,8 @@ erc-sasl--destroy
   "Destroy process PROC and warn user that their settings are likely faulty."
   (delete-process proc)
   (erc--lwarn 'erc-sasl :error
-              "Disconnected from %s; please review SASL settings" proc))
+              "Disconnected from %s; please review SASL settings" proc)
+  nil)
 
 (define-erc-response-handler (902)
   "Handle an ERR_NICKLOCKED response." nil
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 9100ab5577d..3d73c33312a 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -3987,7 +3987,9 @@ erc-display-message
 instead of lower-level ones, like `erc-insert-line', to insert
 arbitrary informative messages as if sent by the server.  That
 is, tell modules to treat a \"local\" message for which PARSED is
-nil like any other server-sent message."
+nil like any other server-sent message.  Finally, expect users to
+treat the return value of this function as undefined even though
+various default response handlers may appear to presume nil."
   (let* ((erc--msg-props
           (or erc--msg-props
               (let ((table (make-hash-table))
@@ -9626,7 +9628,7 @@ erc-kill-channel-hook
     erc-networks-shrink-ids-and-buffer-names
     erc-networks-rename-surviving-target-buffer)
   "Invoked whenever a channel-buffer is killed via `kill-buffer'."
-  :package-version '(ERC . "5.5")
+  :package-version '(ERC . "5.6") ; FIXME sync on release
   :group 'erc-hooks
   :type 'hook)
 
diff --git a/test/lisp/erc/erc-networks-tests.el b/test/lisp/erc/erc-networks-tests.el
index 90d6f13f2f6..f0a7c37ddf2 100644
--- a/test/lisp/erc/erc-networks-tests.el
+++ b/test/lisp/erc/erc-networks-tests.el
@@ -1199,7 +1199,7 @@ erc-networks--set-name
       (erc-mode)
 
       (cl-letf (((symbol-function 'erc--route-insertion)
-                 (lambda (&rest r) (push r calls))))
+                 (lambda (&rest r) (ignore (push r calls)))))
 
         (ert-info ("Signals when `erc-server-announced-name' unset")
           (should-error (erc-networks--set-name nil (make-erc-response)))
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 999d9f100c9..a6e6d58cf9d 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -330,6 +330,7 @@ erc--refresh-prompt
                                (cl-incf counter))))
          erc-accidental-paste-threshold-seconds
          erc-insert-modify-hook
+         erc-send-modify-hook
          (erc-last-input-time 0)
          (erc-modules (remq 'stamp erc-modules))
          (erc-send-input-line-function #'ignore)
@@ -2533,7 +2534,7 @@ erc-message
         erc-kill-channel-hook erc-kill-server-hook erc-kill-buffer-hook)
     (cl-letf (((symbol-function 'erc-display-message)
                (lambda (_ _ _ msg &rest args)
-                 (push (apply #'erc-format-message msg args) calls)))
+                 (ignore (push (apply #'erc-format-message msg args) calls))))
               ((symbol-function 'erc-server-send)
                (lambda (line _) (push line calls)))
               ((symbol-function 'erc-server-buffer)
-- 
2.45.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #4: 0002-5.6-Delete-original-speedbar-frame-in-erc-nickbar-mo.patch --]
[-- Type: text/x-patch, Size: 17801 bytes --]

From d94753dac3648daebf6e645c6f0e6df041770b81 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Tue, 21 May 2024 05:37:39 -0700
Subject: [PATCH 2/7] [5.6] Delete original speedbar frame in erc-nickbar-mode

* lisp/erc/erc-speedbar.el (erc-speedbar-buttons): Disable
`erc-nickbar-mode' when it's not displayed in a window.
(erc-speedbar--hidden-speedbar-frame)
(erc-speedbar--emulate-speedbar): Add doc string.
(erc-speedbar--handle-delete-frame): New function.
(erc-speedbar--toggle-nicknames-sidebar): Remove function because
these changes obviate the need for such overly complicated conditional
logic.
(erc-speedbar--ensure): Create `speedbar-buffer' when needed, and
delete the original frame.  Set `dframe-delete-frame-function' to own
handler.
(erc-speedbar--run-timer-on-post-insert)
(erc-speedbar--prod-dframe-timer): Rename former to latter.  Return
nil, and accept any number of args.
(erc-nickbar-mode, erc-nickbar-disable): Tear down completely when
disabling, regardless of universal argument.  Also run
`erc-speedbar--prod-dframe-timer' on `erc-server-PONG-functions'.
(erc-speedbar--shutting-down-p): Remove unused variable.
(erc-speedbar--dframe-controlled): Don't bother making frame visible
because it's been deleted.
* test/lisp/erc/erc-scenarios-status-sidebar.el
(erc-scenarios-status-sidebar--nickbar): Update assertions.
---
 lisp/erc/erc-speedbar.el                      | 203 +++++++++---------
 test/lisp/erc/erc-scenarios-status-sidebar.el |  16 +-
 2 files changed, 110 insertions(+), 109 deletions(-)

diff --git a/lisp/erc/erc-speedbar.el b/lisp/erc/erc-speedbar.el
index b156f61d5d9..9cde452be58 100644
--- a/lisp/erc/erc-speedbar.el
+++ b/lisp/erc/erc-speedbar.el
@@ -146,7 +146,10 @@ erc-speedbar-buttons
       (setq serverp (erc--server-buffer-p))
       (setq chanp (erc-channel-p (erc-default-target)))
       (setq queryp (erc-query-buffer-p)))
-    (cond (serverp
+    (defvar erc-nickbar-mode)
+    (cond ((and erc-nickbar-mode (null (get-buffer-window speedbar-buffer)))
+           (run-at-time 0 nil #'erc-nickbar-mode -1))
+          (serverp
 	   (erc-speedbar-channel-buttons nil 0 buffer))
 	  (chanp
 	   (erc-speedbar-insert-target buffer 0)
@@ -288,15 +291,9 @@ erc-speedbar--highlight-self-and-ops
                   'erc-current-nick-face
                 'erc-my-nick-face))
           (v v))
-      ;; FIXME overload `erc-channel-user-owner-p' and friends to
-      ;; accept an `erc-channel-user' object and replace this unrolled
-      ;; stuff with a single call to `erc-get-user-mode-prefix'.
-      (and cuser (or (erc-channel-user-owner cuser)
-                     (erc-channel-user-admin cuser)
-                     (erc-channel-user-op cuser)
-                     (erc-channel-user-halfop cuser)
-                     (erc-channel-user-voice cuser))
-           erc-button-nickname-face))))
+      (or (and cuser (not (zerop (erc-channel-user-status cuser)))
+               erc-button-nickname-face)
+          'erc-default-face))))
 
 (defun erc-speedbar--on-click (nick sbtoken _indent)
   ;; 0: finger, 1: name, 2: info, 3: buffer-name
@@ -447,7 +444,11 @@ erc-speedbar--buffer-options
     (speedbar-use-images . nil)
     (speedbar-hide-button-brackets-flag . t)))
 
-(defvar erc-speedbar--hidden-speedbar-frame nil)
+(defvar erc-speedbar--hidden-speedbar-frame nil
+  "The original `speedbar-frame', which `erc-nickbar-mode' deletes.
+It keeps a reference to it in order to run upstream teardown
+procedures without having to create a dummy frame for that
+purpose.")
 
 (defun erc-speedbar--emulate-sidebar-set-window-preserve-size ()
   (let ((erc-status-sidebar-buffer-name (buffer-name speedbar-buffer))
@@ -463,6 +464,7 @@ erc-speedbar--status-sidebar-mode--unhook
                #'erc-speedbar--emulate-sidebar-set-window-preserve-size))
 
 (defun erc-speedbar--emulate-sidebar ()
+  "Perform local setup for `erc-nickbar-mode' in a new `speedbar-buffer'."
   (require 'erc-status-sidebar)
   (cl-assert speedbar-frame)
   (cl-assert (eq speedbar-buffer (current-buffer)))
@@ -482,30 +484,32 @@ erc-speedbar--emulate-sidebar
       (add-function :around (local 'erc-speedbar--nick-face-function)
                     #'erc-speedbar--compose-nicks-face))))
 
-(defun erc-speedbar--toggle-nicknames-sidebar (arg)
-  (let ((force (numberp arg)))
-    (if speedbar-buffer
-        (progn
-          (cl-assert (buffer-live-p speedbar-buffer))
-          (if (or (and force (< arg 0))
-                  (and (not force) (get-buffer-window speedbar-buffer nil)))
-              ;; Close associated windows and stop updating but leave timer.
-              (progn
-                (dolist (window (get-buffer-window-list speedbar-buffer nil t))
-                  (unless (frame-root-window-p window)
-                    (when erc-speedbar--hidden-speedbar-frame
-                      (cl-assert
-                       (not (eq (window-frame window)
-                                erc-speedbar--hidden-speedbar-frame))))
-                    (delete-window window)))
-                (with-current-buffer speedbar-buffer
-                  (setq speedbar-update-flag nil)
-                  (speedbar-set-mode-line-format)))
-            (when (or (not force) (>= arg 0))
-              (with-selected-frame speedbar-frame
-                (erc-speedbar--emulate-sidebar-set-window-preserve-size)
-                (erc-speedbar-toggle-nicknames-window-lock -1)))))
-      (when-let (((or (not force) (>= arg 0)))
+(defun erc-speedbar--handle-delete-frame (event)
+  "Disable the nickbar if EVENT is deleting the proxy frame."
+  (when (and speedbar-frame
+             (cdr (frame-list))
+             (pcase event
+               (`(delete-frame (,frame)) (eq frame speedbar-frame))))
+    (erc-nickbar-mode -1)))
+
+(defun erc-speedbar--ensure (&optional forcep)
+  "Perform common setup for `erc-nickbar-mode'.
+Without FORCEP, return early when the calling context isn't
+associated with an ERC session."
+  (save-excursion
+    (when (or (erc-server-buffer) forcep)
+      (when erc-track-mode
+        (cl-pushnew '(derived-mode . speedbar-mode)
+                    erc-track--switch-fallback-blockers :test #'equal))
+      (unless speedbar-update-flag
+        (erc-button--display-error-notice-with-keys
+         (erc-server-buffer)
+         "Module `nickbar' needs `speedbar-update-flag' to be non-nil"
+         (and (not (display-graphic-p)) " in text terminals")
+         ". Setting to t for the current Emacs session."
+         " Customize it permanently to avoid this message.")
+        (setq speedbar-update-flag t))
+      (when-let (((null speedbar-buffer))
                  (speedbar-frame-parameters (backquote-list*
                                              '(visibility . nil)
                                              '(no-other-frame . t)
@@ -516,52 +520,45 @@ erc-speedbar--toggle-nicknames-sidebar
         ;; created twice.
         (speedbar-change-initial-expansion-list "ERC")
         (speedbar-frame-mode 1)
-        ;; If we put the remaining parts in the "create hook" along
-        ;; with everything else, the frame with `window-main-window'
-        ;; gets raised and steals focus if you've switched away from
-        ;; Emacs in the meantime.
-        (make-frame-invisible speedbar-frame)
-        (select-frame (setq speedbar-frame (previous-frame)))
+        ;; The setup steps below can't go in the "create hook" because
+        ;; the frame with `window-main-window' will be raised and
+        ;; steal focus if you switch away from Emacs in the meantime.
+        (let ((frame speedbar-frame))
+          (cl-assert (not (eq speedbar-frame (selected-frame))))
+          (select-frame (setq speedbar-frame (selected-frame)))
+          (delete-frame frame))
+        ;; Allow deleting (our) `speedbar-frame' with the mouse.
+        (with-current-buffer speedbar-buffer
+          (kill-local-variable 'dframe-delete-frame-function)
+          (setq dframe-delete-frame-function
+                #'erc-speedbar--handle-delete-frame)))
+      (with-selected-frame speedbar-frame
         (erc-speedbar--emulate-sidebar-set-window-preserve-size)
-        (erc-speedbar-toggle-nicknames-window-lock -1))))
-  (cl-assert (not (cdr (erc-speedbar--get-timers))) t))
-
-(defun erc-speedbar--ensure (&optional force)
-  (when (or (erc-server-buffer) force)
-    (when erc-track-mode
-      (cl-pushnew '(derived-mode . speedbar-mode)
-                  erc-track--switch-fallback-blockers :test #'equal))
-    (unless speedbar-update-flag
-      (erc-button--display-error-notice-with-keys
-       (erc-server-buffer)
-       "Module `nickbar' needs `speedbar-update-flag' to be non-nil"
-       (and (not (display-graphic-p)) " in text terminals")
-       ". Setting to t for the current Emacs session."
-       " Customize it permanently to avoid this message.")
-      (setq speedbar-update-flag t))
-    (erc-speedbar--toggle-nicknames-sidebar +1)
-    (with-current-buffer speedbar-buffer
-      (setq speedbar-update-flag t)
-      (speedbar-set-mode-line-format))))
+        (erc-speedbar-toggle-nicknames-window-lock -1))
+      (cl-assert (null (cdr (erc-speedbar--get-timers))))
+      (with-current-buffer speedbar-buffer
+        (setq speedbar-update-flag t)
+        (speedbar-set-mode-line-format)))))
 
-(defvar erc-speedbar--shutting-down-p nil)
-(defvar erc-speedbar--force-update-interval-secs 5 "Speedbar update period.")
+(defvar erc-speedbar--force-update-interval-secs 5
+  "Speedbar update period.")
 
 (defvar-local erc-speedbar--last-ran nil
   "When non-nil, a lisp timestamp updated when the speedbar timer runs.")
 
-(defun erc-speedbar--run-timer-on-post-insert ()
-  "Refresh speedbar if idle for `erc-speedbar--force-update-interval-secs'."
-  (when speedbar-buffer
+(defun erc-speedbar--prod-dframe-timer (&rest _)
+  "Refresh speedbar if dormant for `erc-speedbar--force-update-interval-secs'."
+  (when (buffer-live-p speedbar-buffer)
     (with-current-buffer speedbar-buffer
-      (when-let
-          ((dframe-timer)
-           ((erc--check-msg-prop 'erc--cmd 'PRIVMSG))
-           (interval erc-speedbar--force-update-interval-secs)
-           ((or (null erc-speedbar--last-ran)
-                (time-less-p erc-speedbar--last-ran
-                             (time-subtract (current-time) interval)))))
-        (run-at-time 0 nil #'dframe-timer-fn)))))
+      (when
+          (and dframe-timer
+               (or (null erc-speedbar--last-ran)
+                   (time-less-p erc-speedbar--last-ran
+                                (time-subtract
+                                 (current-time)
+                                 erc-speedbar--force-update-interval-secs))))
+        (run-at-time 0 nil #'dframe-timer-fn))))
+  nil)
 
 (defun erc-speedbar--reset-last-ran-on-timer ()
   "Reset `erc-speedbar--last-ran'."
@@ -574,42 +571,47 @@ nickbar
   "Show nicknames for current target buffer in a side window.
 When enabling, create a speedbar session if one doesn't exist and
 show its buffer in an `erc-status-sidebar' window instead of a
-separate frame.  When disabling, close the window or, with a
-negative prefix arg, destroy the session.
+separate frame.  If ERC doesn't yet have any live connections,
+defer activation until such time.  This means the variable
+`erc-nickbar-mode' may be t even though no actual speedbar yet
+exists.  When disabling, destroy the speedbar session.
 
 For controlling whether the speedbar window is selectable with
-`other-window', see `erc-nickbar-toggle-nicknames-window-lock'.
-Note that during initialization, this module may produce unwanted
-side effects, like the raising of frames or the stealing of input
-focus.  If you witness such a thing and can reproduce it, please
-file a bug report with \\[erc-bug]."
+`other-window', see `erc-nickbar-toggle-nicknames-window-lock'."
   ((add-hook 'erc--setup-buffer-hook #'erc-speedbar--ensure)
-   (add-hook 'erc-insert-post-hook #'erc-speedbar--run-timer-on-post-insert)
    (add-hook 'speedbar-timer-hook #'erc-speedbar--reset-last-ran-on-timer)
+   (add-hook 'erc-insert-post-hook #'erc-speedbar--prod-dframe-timer)
+   (add-hook 'erc-server-PONG-functions #'erc-speedbar--prod-dframe-timer)
    (erc-speedbar--ensure)
    (unless (or erc--updating-modules-p
-               (and-let* ((speedbar-buffer)
-                          (win (get-buffer-window speedbar-buffer 'all-frames))
-                          ((eq speedbar-frame (window-frame win))))))
+               (and speedbar-buffer
+                    (eq speedbar-frame
+                        (window-frame (get-buffer-window speedbar-buffer t)))))
      (when-let ((buf (or (and (derived-mode-p 'erc-mode) (current-buffer))
                          (car (erc-buffer-filter #'erc--server-buffer-p)))))
        (with-current-buffer buf
-         (erc-speedbar--ensure 'force)))))
+         (erc-speedbar--ensure 'forcep)))))
   ((remove-hook 'erc--setup-buffer-hook #'erc-speedbar--ensure)
-   (remove-hook 'erc-insert-post-hook #'erc-speedbar--run-timer-on-post-insert)
    (remove-hook 'speedbar-timer-hook #'erc-speedbar--reset-last-ran-on-timer)
+   (remove-hook 'erc-insert-post-hook #'erc-speedbar--prod-dframe-timer)
+   (remove-hook 'erc-server-PONG-functions #'erc-speedbar--prod-dframe-timer)
    (when erc-track-mode
      (setq erc-track--switch-fallback-blockers
            (remove '(derived-mode . speedbar-mode)
                    erc-track--switch-fallback-blockers)))
-   (erc-speedbar--toggle-nicknames-sidebar -1)
-   (when-let (((not erc-speedbar--shutting-down-p))
-              (arg erc--module-toggle-prefix-arg)
-              ((numberp arg))
-              ((< arg 0)))
-     (with-current-buffer speedbar-buffer
-       (dframe-close-frame)
-       (setq erc-speedbar--hidden-speedbar-frame nil)))))
+   (cl-assert speedbar-buffer)
+   ;; Close associated windows and stop updating but leave timer.
+   (dolist (window (get-buffer-window-list speedbar-buffer nil t))
+     (unless (frame-root-window-p window)
+       (when erc-speedbar--hidden-speedbar-frame
+         (cl-assert (not (eq (window-frame window)
+                             erc-speedbar--hidden-speedbar-frame))))
+       (delete-window window)))
+   (with-current-buffer speedbar-buffer
+     (setq speedbar-update-flag nil)
+     (speedbar-set-mode-line-format)
+     (unless (eq erc--module-toggle-prefix-arg most-negative-fixnum)
+       (dframe-close-frame)))))
 
 (defun erc-speedbar--get-timers ()
   (cl-remove #'dframe-timer-fn timer-idle-list
@@ -621,21 +623,18 @@ erc-speedbar--dframe-controlled
     (cl-assert (eq speedbar-buffer (current-buffer))))
   (when (and erc-speedbar--hidden-speedbar-frame (numberp arg) (< arg 0))
     (when erc-nickbar-mode
-      (let ((erc-speedbar--shutting-down-p t))
-        (erc-nickbar-mode -1)))
+      (erc-nickbar-mode most-negative-fixnum))
     (setq speedbar-frame erc-speedbar--hidden-speedbar-frame
           erc-speedbar--hidden-speedbar-frame nil)
-    ;; It's unknown whether leaving the frame invisible interferes
-    ;; with the upstream teardown sequence.
-    (when (display-graphic-p)
-      (make-frame-visible speedbar-frame))
     (speedbar-frame-mode arg) ; -1
     ;; As of Emacs 29, `dframe-set-timer' can't remove `dframe-timer'.
     (cl-assert (= 1 (length (erc-speedbar--get-timers))) t)
     (cancel-function-timers #'dframe-timer-fn)
     ;; `dframe-close-frame' kills the buffer but no function in
     ;; erc-speedbar.el resets this to nil.
-    (setq speedbar-buffer nil)))
+    (setq erc-speedbar--hidden-speedbar-frame nil
+          speedbar-buffer nil
+          speedbar-frame nil)))
 
 (defun erc-speedbar-toggle-nicknames-window-lock (arg)
   "Toggle whether nicknames window is selectable with \\[other-window].
diff --git a/test/lisp/erc/erc-scenarios-status-sidebar.el b/test/lisp/erc/erc-scenarios-status-sidebar.el
index 2523ff9ee46..4cec00e2312 100644
--- a/test/lisp/erc/erc-scenarios-status-sidebar.el
+++ b/test/lisp/erc/erc-scenarios-status-sidebar.el
@@ -98,12 +98,14 @@ erc-scenarios-status-sidebar--bufbar
 (defvar erc-nickbar-mode)
 (defvar speedbar-buffer)
 
+;; FIXME move to own file because it takes 20+ seconds, uncompiled.
 (ert-deftest erc-scenarios-status-sidebar--nickbar ()
   :tags `(:expensive-test :unstable ,@(and (getenv "ERC_TESTS_GRAPHICAL")
                                            '(:erc--graphical)))
-  (when noninteractive (ert-skip "Interactive only"))
+  (when (and noninteractive (= emacs-major-version 27))
+    (ert-skip "Hangs on Emacs 27, asking for input"))
 
-  (erc-scenarios-common-with-cleanup
+  (erc-scenarios-common-with-noninteractive-in-term
       ((erc-scenarios-common-dialog "base/gapless-connect")
        (erc-server-flood-penalty 0.1)
        (erc-server-flood-penalty erc-server-flood-penalty)
@@ -156,14 +158,14 @@ erc-scenarios-status-sidebar--nickbar
         ;; etc. for testing commands that call those same functions.
         (call-interactively #'erc-nickbar-mode)
         (should-not erc-nickbar-mode)
-        (should-not (and speedbar-buffer
-                         (get-buffer-window speedbar-buffer)))
-        (should speedbar-buffer)
+        (should-not speedbar-buffer)
+        (should-not (get-buffer " SPEEDBAR"))
 
         (erc-nickbar-mode +1)
-        (should (and speedbar-buffer
-                     (get-buffer-window speedbar-buffer)))
+        (should (and speedbar-buffer (get-buffer-window speedbar-buffer)))
+        (should (eq speedbar-buffer (get-buffer " SPEEDBAR")))
         (should (get-buffer " SPEEDBAR"))
+
         (erc-nickbar-mode -1)
         (should-not (get-buffer " SPEEDBAR"))
         (should-not erc-nickbar-mode)
-- 
2.45.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #5: 0003-5.6-Reuse-old-query-buffers-for-round-trip-renicks-i.patch --]
[-- Type: text/x-patch, Size: 31204 bytes --]

From 1e5d0ec61a632b975b1678ee2fb713de54bbbdee Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Wed, 8 May 2024 19:04:13 -0700
Subject: [PATCH 3/7] [5.6] Reuse old query buffers for round-trip renicks in
 ERC

* lisp/erc/erc-backend.el
(erc--wrangle-query-buffers-on-nick-change): New function for handling
buffer renaming and message routing triggered by a nick change.  Such
twiddling used to reside in `erc-server-NICK' but has been separated
out for use by built-in modules overriding `erc-server-NICK'.  The
behavior has also changed to favor always reusing an existing query
buffer whenever possible instead of creating a new, <N>-suffixed
buffer.  This addresses some arguably unfinished business from
bug#48598.
(erc-server-NICK): Fix erroneous call to `erc-update-user-nick' that
passed the sender's login as the function's INFO argument.  Move
buffer renaming logic to `erc--wrangle-query-buffers-on-nick-change'
for use by "NICK" handlers managed by modules.
* lisp/erc/erc.el (erc-generate-new-buffer-name): Fix typo in doc
string.
* test/lisp/erc/erc-scenarios-base-renick.el
(erc-scenarios-base-renick-queries-solo): Revise slightly to use
updated helpers.
(erc-scenarios-base-renick-queries/round-trip/default): New test.
(erc-scenarios-base-renick-queries/round-trip/merge-query): New test.
* test/lisp/erc/resources/base/reconnect/options-again.eld: Adjust
timeout.
* test/lisp/erc/resources/base/renick/queries/roundtrip.eld: New file.
* test/lisp/erc/resources/base/renick/self/manual.eld: Update timeouts.
* test/lisp/erc/resources/base/renick/self/merge-query-a.eld: New file.
* test/lisp/erc/resources/base/renick/self/merge-query-b.eld: New
file.  (Bug#70928)
---
 lisp/erc/erc-backend.el                       |  46 +++--
 lisp/erc/erc.el                               |   2 +-
 test/lisp/erc/erc-scenarios-base-renick.el    | 192 ++++++++++++++++--
 .../base/reconnect/options-again.eld          |   2 +-
 .../base/renick/queries/roundtrip.eld         |  64 ++++++
 .../erc/resources/base/renick/self/manual.eld |   8 +-
 .../base/renick/self/merge-query-a.eld        |  46 +++++
 .../base/renick/self/merge-query-b.eld        |  48 +++++
 8 files changed, 371 insertions(+), 37 deletions(-)
 create mode 100644 test/lisp/erc/resources/base/renick/queries/roundtrip.eld
 create mode 100644 test/lisp/erc/resources/base/renick/self/merge-query-a.eld
 create mode 100644 test/lisp/erc/resources/base/renick/self/merge-query-b.eld

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 90c46eadaf4..447139a2218 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -1839,6 +1839,37 @@ erc--server-determine-join-display-context
                                  ?h host ?t tgt ?m mode)))
       (erc-banlist-update proc parsed))))
 
+(defun erc--wrangle-query-buffers-on-nick-change (old new)
+  "Create or reuse a query buffer for NEW nick after considering OLD nick.
+Return a list of buffers in which to announce the change."
+  ;; Note that `new-buffer' may be older than `old-buffer', e.g., if
+  ;; the query target is switching to a previously used nick.
+  (let ((new-buffer (erc-get-buffer new erc-server-process))
+        (old-buffer (erc-get-buffer old erc-server-process))
+        (selfp (erc-current-nick-p old)) ; e.g., for note taking, etc.
+        buffers)
+    (when new-buffer
+      (push new-buffer buffers))
+    (when old-buffer
+      (push old-buffer buffers)
+      ;; Ensure the new nick is absent from the old query.
+      (unless selfp
+        (erc-remove-channel-member old-buffer old))
+      (when (or selfp (null new-buffer))
+        (let ((target (erc--target-from-string new))
+              (id (erc-networks--id-given erc-networks--id)))
+          (with-current-buffer old-buffer
+            (setq erc-default-recipients (cons new
+                                               (cdr erc-default-recipients))
+                  erc--target target))
+          (setq new-buffer (erc-get-buffer-create erc-session-server
+                                                  erc-session-port
+                                                  nil target id)))))
+    (when new-buffer
+      (with-current-buffer new-buffer
+        (erc-update-mode-line)))
+    buffers))
+
 (define-erc-response-handler (NICK)
   "Handle nick change messages." nil
   (let ((nn (erc-response.contents parsed))
@@ -1853,18 +1884,9 @@ erc--server-determine-join-display-context
       ;; erc-channel-users won't contain it
       ;;
       ;; Possibly still relevant: bug#12002
-      (when-let ((buf (erc-get-buffer nick erc-server-process))
-                 (tgt (erc--target-from-string nn)))
-        (with-current-buffer buf
-          (setq erc-default-recipients (cons nn (cdr erc-default-recipients))
-                erc--target tgt))
-        (with-current-buffer (erc-get-buffer-create erc-session-server
-                                                    erc-session-port nil tgt
-                                                    (erc-networks--id-given
-                                                     erc-networks--id))
-          ;; Current buffer is among bufs
-          (erc-update-mode-line)))
-      (erc-update-user-nick nick nn host nil nil login)
+      (dolist (buf (erc--wrangle-query-buffers-on-nick-change nick nn))
+        (cl-pushnew buf bufs))
+      (erc-update-user-nick nick nn host login)
       (cond
        ((string= nick (erc-current-nick))
         (cl-pushnew (erc-server-buffer) bufs)
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 3d73c33312a..38547ea94bf 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -1990,7 +1990,7 @@ erc-generate-new-buffer-name
 `erc-server-connect-function's.
 
 When TGT-INFO is non-nil, expect its string field to match the redundant
-param TARGET (retained for compatibility).  Whenever possibly, prefer
+param TARGET (retained for compatibility).  Whenever possible, prefer
 returning TGT-INFO's string unmodified.  But when a case-insensitive
 collision prevents that, return target@ID when ID is non-nil or
 target@network otherwise after renaming the conflicting buffer in the
diff --git a/test/lisp/erc/erc-scenarios-base-renick.el b/test/lisp/erc/erc-scenarios-base-renick.el
index 3001fde6da0..813cb758ee4 100644
--- a/test/lisp/erc/erc-scenarios-base-renick.el
+++ b/test/lisp/erc/erc-scenarios-base-renick.el
@@ -160,6 +160,7 @@ erc-scenarios-base-renick-queries-solo
        (erc-server-flood-penalty 0.1)
        (erc-server-flood-margin 20)
        (dumb-server (erc-d-run "localhost" t 'solo))
+       (expect (erc-d-t-make-expecter))
        (port (process-contact dumb-server :service))
        erc-autojoin-channels-alist
        erc-server-buffer-foo)
@@ -175,33 +176,186 @@ erc-scenarios-base-renick-queries-solo
 
     (erc-d-t-wait-for 10 (get-buffer "foonet"))
 
-    (ert-info ("Joined by bouncer to #foo, pal persent")
+    (ert-info ("Joined by bouncer to #foo, pal Lal is present")
       (with-current-buffer (erc-d-t-wait-for 1 (get-buffer "#foo"))
-        (erc-d-t-search-for 5 "On Thursday")
+        (funcall expect 10 "<bob> alice: On Thursday")
         (erc-scenarios-common-say "hi")))
 
-    (erc-d-t-wait-for 10 "Query buffer appears with message from pal"
-      (get-buffer "Lal"))
-
-    (ert-info ("Chat with pal, who changes name")
-      (with-current-buffer "Lal"
-        (erc-d-t-search-for 3 "hello")
+    (ert-info ("Query buffer appears from Lal, who renicks")
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "Lal"))
+        (funcall expect 10 "<Lal> hello")
         (erc-scenarios-common-say "hi")
-        (erc-d-t-search-for 10 "is now known as Linguo")
-        (should-not (search-forward "is now known as Linguo" nil t))))
-
-    (erc-d-t-wait-for 1 (get-buffer "Linguo"))
-    (should-not (get-buffer "Lal"))
-
-    (with-current-buffer "Linguo" (erc-scenarios-common-say "howdy Linguo"))
+        (funcall expect 10 "is now known as Linguo")
+        ;; No duplicate message.
+        (funcall expect -0.1 "is now known as Linguo")
+        ;; No duplicate buffer.
+        (erc-d-t-wait-for 1 (equal (buffer-name) "Linguo"))
+        (should-not (get-buffer "Lal"))
+        (erc-scenarios-common-say "howdy Linguo")))
 
     (with-current-buffer "#foo"
-      (erc-d-t-search-for 10 "is now known as Linguo")
-      (should-not (search-forward "is now known as Linguo" nil t))
-      (erc-cmd-PART ""))
+      (funcall expect 10 "is now known as Linguo")
+      (funcall expect -0.1 "is now known as Linguo")
+      (erc-scenarios-common-say "/part"))
 
     (with-current-buffer "Linguo"
-      (erc-d-t-search-for 10 "get along"))))
+      (funcall expect 10 "get along"))))
+
+;; Someone you have a query with disconnects and reconnects under a
+;; new nick (perhaps due to their client appending a backtick or
+;; underscore).  They then engage you in another query before
+;; renicking to their original nick.  Prior to 5.5, ERC would add a
+;; uniquifying suffix of the form bob<2> to the new, post-renick
+;; query.  ERC 5.6+ acts differently.  It mimics popular standalone
+;; clients in reusing existing query buffers.
+(ert-deftest erc-scenarios-base-renick-queries/round-trip/default ()
+  :tags '(:expensive-test)
+
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/renick/queries")
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (erc-d-run "localhost" t 'roundtrip))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       (erc-autojoin-channels-alist '((foonet "#chan"))))
+
+    (ert-info ("Connect to foonet")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester")
+        (funcall expect 10 "This server is in debug mode")))
+
+    (ert-info ("User dummy opens a query with you")
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "dummy"))
+        (funcall expect 10 "hi")))
+
+    (ert-info ("User dummy quits, reconnects as user warwick")
+      (with-current-buffer "#chan"
+        (funcall expect 10 "has quit")
+        (should-not (erc-get-channel-member "dummy"))
+        (with-current-buffer "dummy"
+          (should-not (erc-get-channel-member "dummy")))
+        (funcall expect 10 "<bob> Alas! sir")
+        (funcall expect 10 "<bob> warwick, welcome")
+        (funcall expect 10 "<warwick> hola")))
+
+    (ert-info ("User warwick queries you, creating a new buffer")
+      (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "warwick"))
+        (should (get-buffer "dummy")) ; not reused
+        (funcall expect 10 "<warwick> howdy")
+        (funcall expect 10 "is now known as dummy")
+        (should-not (erc-get-channel-member "warwick"))
+        (should-not (erc-get-channel-member "dummy"))))
+
+    (ert-info ("User warwick renicks as user dummy")
+      (with-current-buffer "#chan"
+        (funcall expect 10 "is now known as dummy")
+        (should-not (erc-get-channel-member "warwick"))))
+
+    (with-current-buffer "dummy"
+      (should-not (get-buffer "dummy<2>"))
+      (funcall expect 10 "has quit" (point-min))
+      (funcall expect -0.1 "merging buffer")
+      (funcall expect 10 "is now known as dummy")
+      (should (erc-get-channel-member "dummy"))
+      (funcall expect 10 "<dummy> hey"))
+
+    (with-current-buffer "#chan"
+      (funcall expect 10 "<alice> bob: Than those that"))))
+
+;; This test asserts behavior for the other side of the conversation
+;; described by `erc-scenarios-base-renick-queries/round-trip/default'
+;; above.  After speaking with someone in a query, you disconnect and
+;; reconnect under a new nick.  You then open a new query with the
+;; same person before changing your nick back to the previous one.
+;; The buffers for the two session should then be merged with the help
+;; of `erc-networks--transplant-target-buffer-function' and
+;; `erc-networks--copy-server-buffer-functions'.
+(ert-deftest erc-scenarios-base-renick-self/merge-query ()
+  :tags '(:expensive-test)
+
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "base/renick/self")
+       (erc-server-flood-penalty 0.1)
+       (dumb-server (erc-d-run "localhost" t 'merge-query-a 'merge-query-b))
+       (port (process-contact dumb-server :service))
+       (expect (erc-d-t-make-expecter))
+       (erc-autojoin-channels-alist '((foonet "#chan"))))
+
+    (ert-info ("Connect to foonet as tester")
+      (with-current-buffer (erc :server "127.0.0.1" :port port :nick "tester")
+        (funcall expect 10 "This server is in debug mode")))
+
+    (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#chan"))
+      (funcall expect 10 "<alice> bob: Speak to the people")
+      (erc-scenarios-common-say "/query observer"))
+
+    (with-current-buffer "observer"
+      (erc-scenarios-common-say "hi")
+      (funcall expect 10 "<observer> hi?"))
+
+    (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "#chan"))
+      (erc-scenarios-common-say "/quit"))
+
+    (with-current-buffer "foonet"
+      (funcall expect 10 "*** ERC finished ***"))
+
+    (ert-info ("Reconnect to foonet as dummy")
+      (with-current-buffer (erc :server "127.0.0.1" :port port :nick "dummy")
+        (funcall expect 10 "This server is in debug mode")))
+
+    (with-current-buffer
+        (erc-d-t-wait-for 10 (get-buffer "#chan@foonet/dummy"))
+      ;; Uniquification has been performed.
+      (should-not (get-buffer "#chan"))
+      (should (get-buffer "#chan@foonet/tester"))
+      (should-not (get-buffer "foonet"))
+      (should (get-buffer "foonet/tester"))
+      (should (get-buffer "foonet/dummy"))
+      (funcall expect 10 "<alice> bob: Pray you")
+      (erc-scenarios-common-say "/query observer"))
+
+    (with-current-buffer "observer@foonet/dummy"
+      (should-not (get-buffer "observer"))
+      (should (get-buffer "observer@foonet/tester"))
+      (erc-scenarios-common-say "hola")
+      (funcall expect 10 "<observer> whodis?"))
+
+    (with-current-buffer
+        (erc-d-t-wait-for 10 (get-buffer "#chan@foonet/dummy"))
+      (erc-scenarios-common-say "/nick tester"))
+
+    ;; All buffers have been merged.
+    (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "observer"))
+      (should-not (get-buffer "observer@foonet/dummy"))
+      (should-not (get-buffer "observer@foonet/tester"))
+      ;; Goto last message from previous session.
+      (funcall expect 10 "has quit" (point-min))
+      (funcall expect -0.01 "\n\n[") ; duplicate date stamp removed
+      (funcall expect 1 (concat "*** Grafting buffer `observer@foonet/dummy'"
+                                " onto `observer@foonet/tester'"))
+      (funcall expect 1 "<dummy> hola")
+      (funcall expect 1 "<observer> whodis?")
+      (funcall expect 1 "*** Your new nickname is tester"))
+
+    (with-current-buffer "foonet"
+      (should-not (get-buffer "foonet/dummy"))
+      (should-not (get-buffer "foonet/tester"))
+      ;; Goto last assertion.
+      (funcall expect 10 "*** ERC finished ***" (point-min))
+      (funcall expect -0.01 "\n\n[") ; duplicate date stamp removed
+      (funcall expect 5 "Grafting buffer `foonet/dummy' onto `foonet/tester'"))
+
+    (with-current-buffer "#chan"
+      (should-not (get-buffer "#chan@foonet/dummy"))
+      (should-not (get-buffer "#chan@foonet/tester"))
+      (funcall expect 10 "has quit" (point-min))
+      (funcall expect -0.01 "\n\n[") ; duplicate date stamp removed
+      (funcall expect 1 (concat "*** Grafting buffer `#chan@foonet/dummy'"
+                                " onto `#chan@foonet/tester'"))
+      (funcall expect 1 "You have joined channel #chan")
+      (funcall expect 1 "<bob> alice: Have here bereft")
+      (funcall expect 1 "*** Your new nickname is tester"))))
 
 ;; You share a channel and a query buffer with a user on two different
 ;; networks (through a proxy).  The user changes their nick on both
diff --git a/test/lisp/erc/resources/base/reconnect/options-again.eld b/test/lisp/erc/resources/base/reconnect/options-again.eld
index 8a3264fda9c..a3a86fb7100 100644
--- a/test/lisp/erc/resources/base/reconnect/options-again.eld
+++ b/test/lisp/erc/resources/base/reconnect/options-again.eld
@@ -18,7 +18,7 @@
  (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")
+((mode-user 10 "MODE tester +i")
  (0 ":irc.foonet.org 221 tester +i")
  (0 ":irc.foonet.org NOTICE tester :This server is still in debug mode."))
 
diff --git a/test/lisp/erc/resources/base/renick/queries/roundtrip.eld b/test/lisp/erc/resources/base/renick/queries/roundtrip.eld
new file mode 100644
index 00000000000..50764a143b6
--- /dev/null
+++ b/test/lisp/erc/resources/base/renick/queries/roundtrip.eld
@@ -0,0 +1,64 @@
+;; -*- mode: lisp-data; -*-
+((nick 10 "NICK tester"))
+((user 10 "USER user 0 * :unknown")
+ (0.00 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0.00 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version ergo-v2.11.1")
+ (0.00 ":irc.foonet.org 003 tester :This server was created Thu, 09 May 2024 05:19:24 UTC")
+ (0.00 ":irc.foonet.org 004 tester irc.foonet.org ergo-v2.11.1 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.00 ":irc.foonet.org 005 tester AWAYLEN=390 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# CHATHISTORY=25 ELIST=U EXCEPTS EXTBAN=,m FORWARD=f INVEX :are supported by this server")
+ (0.00 ":irc.foonet.org 005 tester KICKLEN=390 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 UTF8ONLY WHOX :are supported by this server")
+ (0.01 ":irc.foonet.org 005 tester draft/CHATHISTORY=25 :are supported by this server")
+ (0.00 ":irc.foonet.org 251 tester :There are 0 users and 6 invisible on 1 server(s)")
+ (0.00 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0.00 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0.00 ":irc.foonet.org 254 tester 2 :channels formed")
+ (0.00 ":irc.foonet.org 255 tester :I have 6 clients and 0 servers")
+ (0.00 ":irc.foonet.org 265 tester 6 6 :Current local users 6, max 6")
+ (0.00 ":irc.foonet.org 266 tester 6 6 :Current global users 6, max 6")
+ (0.00 ":irc.foonet.org 422 tester :MOTD File is missing"))
+
+((mode-user 10 "MODE tester +i")
+ (0.00 ":irc.foonet.org 221 tester +i")
+ (0.00 ":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 10 "JOIN #chan")
+ (0.03 ":irc.foonet.org 221 tester +i") ; dupe
+ (0.00 ":tester!~u@s8ceryiqkkcxk.irc JOIN #chan")
+ (0.04 ":irc.foonet.org 353 tester = #chan :@fsbot bob alice dummy tester")
+ (0.00 ":irc.foonet.org 366 tester #chan :End of NAMES list")
+ (0.00 ":alice!~u@68v4mpismdues.irc PRIVMSG #chan :tester, welcome!")
+ (0.00 ":bob!~u@68v4mpismdues.irc PRIVMSG #chan :tester, welcome!")
+ (0.03 ":bob!~u@68v4mpismdues.irc PRIVMSG #chan :That eye that told you so look'd but a-squint."))
+
+((mode-chan 10 "MODE #chan")
+ (0.00 ":irc.foonet.org 324 tester #chan +Cnt")
+ (0.01 ":irc.foonet.org 329 tester #chan 1715231970")
+
+ ;; existing query with dummy
+ (0.05 ":dummy!~u@s8ceryiqkkcxk.irc PRIVMSG tester :hi")
+ (0.02 ":bob!~u@68v4mpismdues.irc PRIVMSG #chan :alice: Villains, forbear! we are the empress' sons.")
+ (0.01 ":alice!~u@68v4mpismdues.irc PRIVMSG #chan :bob: This matter of marrying his king's daughter,wherein he must be weighed rather by her value than his own,words him, I doubt not, a great deal from the matter.")
+
+ ;; dummy quits
+ (0.07 ":dummy!~u@s8ceryiqkkcxk.irc QUIT :Quit: \2ERC\2 5.5.0.29.1 (IRC client for GNU Emacs 29.3.50)")
+ (0.03 ":bob!~u@68v4mpismdues.irc PRIVMSG #chan :We will afflict the emperor in his pride.")
+ (0.03 ":alice!~u@68v4mpismdues.irc PRIVMSG #chan :bob: Why, then, is my pump well flowered.")
+ (0.05 ":bob!~u@68v4mpismdues.irc PRIVMSG #chan :Alas! sir, I know not Jupiter; I never drank with him in all my life.")
+
+ ;; rejoins as warwick
+ (0.03 ":warwick!~u@s8ceryiqkkcxk.irc JOIN #chan")
+ (0.00 ":bob!~u@68v4mpismdues.irc PRIVMSG #chan :warwick, welcome!")
+ (0.00 ":alice!~u@68v4mpismdues.irc PRIVMSG #chan :warwick, welcome!")
+ (0.03 ":warwick!~u@s8ceryiqkkcxk.irc PRIVMSG #chan :hola")
+ (0.03 ":alice!~u@68v4mpismdues.irc PRIVMSG #chan :bob: And stint thou too, I pray thee, nurse, say I.")
+
+ ;; Makes contact in a query
+ (0.02 ":warwick!~u@s8ceryiqkkcxk.irc PRIVMSG tester :howdy")
+ (0.03 ":alice!~u@68v4mpismdues.irc PRIVMSG #chan :bob: Nor more willingly leaves winter; such summer-birds are men. Gentlemen, our dinner will not recompense this long stay: feast your ears with the music awhile, if they will fare so harshly o' the trumpet's sound; we shall to 't presently.")
+ (0.03 ":bob!~u@68v4mpismdues.irc PRIVMSG #chan :If it please your honour, I know not well what they are; but precise villains they are, that I am sure of, and void of all profanation in the world that good Christians ought to have.")
+
+ ;; warwick renicks back to dummy
+ (0.08 ":warwick!~u@s8ceryiqkkcxk.irc NICK dummy")
+ (0.04 ":bob!~u@68v4mpismdues.irc PRIVMSG #chan :Pleasure and action make the hours seem short.")
+ (0.01 ":dummy!~u@s8ceryiqkkcxk.irc PRIVMSG tester :hey")
+ (0.02 ":alice!~u@68v4mpismdues.irc PRIVMSG #chan :bob: Than those that have more cunning to be strange."))
diff --git a/test/lisp/erc/resources/base/renick/self/manual.eld b/test/lisp/erc/resources/base/renick/self/manual.eld
index dd107b806d5..a6220ffc2e6 100644
--- a/test/lisp/erc/resources/base/renick/self/manual.eld
+++ b/test/lisp/erc/resources/base/renick/self/manual.eld
@@ -1,5 +1,5 @@
 ;; -*- mode: lisp-data; -*-
-((pass 1 "PASS :foonet:changeme"))
+((pass 10 "PASS :foonet:changeme"))
 ((nick 1 "NICK tester"))
 ((user 1 "USER user 0 * :tester")
  (0 ":irc.foonet.org 001 tester :Welcome to the FooNet Internet Relay Chat Network tester")
@@ -24,7 +24,7 @@
  (0 ":irc.foonet.org 372 tester :- Please visit us in #libera for questions and support.")
  (0 ":irc.foonet.org 376 tester :End of /MOTD command."))
 
-((mode-user 1.2 "MODE tester +i")
+((mode-user 10 "MODE tester +i")
  (0 ":tester!~u@gq7yjr7gsu7nn.irc MODE tester :+RZi")
  (0 ":irc.znc.in 306 tester :You have been marked as being away")
  (0 ":tester!~u@gq7yjr7gsu7nn.irc JOIN #foo")
@@ -38,13 +38,13 @@
  (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 1 "MODE #foo")
+((mode-foo 10 "MODE #foo")
  (0 ":irc.foonet.org 324 tester #foo +nt")
  (0 ":irc.foonet.org 329 tester #foo 1622454985")
  (0.1 ":alice!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :bob: Farewell, pretty lady: you must hold the credit of your father.")
  (0.1 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :alice: On Thursday, sir ? the time is very short."))
 
-((nick 2 "NICK dummy")
+((nick 10 "NICK dummy")
  (0 ":tester!~u@gq7yjr7gsu7nn.irc NICK :dummy")
  (0.1 ":dummy!~u@gq7yjr7gsu7nn.irc MODE dummy :+RZi")
  (0.1 ":bob!~u@gq7yjr7gsu7nn.irc PRIVMSG #foo :dummy: Hi."))
diff --git a/test/lisp/erc/resources/base/renick/self/merge-query-a.eld b/test/lisp/erc/resources/base/renick/self/merge-query-a.eld
new file mode 100644
index 00000000000..27ef7ecd2ff
--- /dev/null
+++ b/test/lisp/erc/resources/base/renick/self/merge-query-a.eld
@@ -0,0 +1,46 @@
+;; -*- mode: lisp-data; -*-
+((nick 10 "NICK tester"))
+((user 10 "USER user 0 * :unknown")
+ (0.00 ":irc.foonet.org 001 tester :Welcome to the foonet IRC Network tester")
+ (0.01 ":irc.foonet.org 002 tester :Your host is irc.foonet.org, running version ergo-v2.11.1")
+ (0.00 ":irc.foonet.org 003 tester :This server was created Sun, 12 May 2024 00:41:10 UTC")
+ (0.00 ":irc.foonet.org 004 tester irc.foonet.org ergo-v2.11.1 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.00 ":irc.foonet.org 005 tester AWAYLEN=390 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# CHATHISTORY=25 ELIST=U EXCEPTS EXTBAN=,m FORWARD=f INVEX :are supported by this server")
+ (0.01 ":irc.foonet.org 005 tester KICKLEN=390 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 UTF8ONLY WHOX :are supported by this server")
+ (0.01 ":irc.foonet.org 005 tester draft/CHATHISTORY=25 :are supported by this server")
+ (0.00 ":irc.foonet.org 251 tester :There are 0 users and 6 invisible on 1 server(s)")
+ (0.00 ":irc.foonet.org 252 tester 0 :IRC Operators online")
+ (0.02 ":irc.foonet.org 253 tester 0 :unregistered connections")
+ (0.00 ":irc.foonet.org 254 tester 2 :channels formed")
+ (0.01 ":irc.foonet.org 255 tester :I have 6 clients and 0 servers")
+ (0.00 ":irc.foonet.org 265 tester 6 6 :Current local users 6, max 6")
+ (0.00 ":irc.foonet.org 266 tester 6 6 :Current global users 6, max 6")
+ (0.00 ":irc.foonet.org 422 tester :MOTD File is missing")
+ (0.00 ":irc.foonet.org 221 tester +i")
+ (0.00 ":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."))
+
+((mode-user 10 "MODE tester +i"))
+
+((join 10 "JOIN #chan")
+ (0.00 ":irc.foonet.org 221 tester +i")
+ (0.00 ":tester!~u@hyyensdmcrjxc.irc JOIN #chan")
+ (0.02 ":irc.foonet.org 353 tester = #chan :someone tester @fsbot alice bob observer")
+ (0.01 ":irc.foonet.org 366 tester #chan :End of NAMES list")
+ (0.00 ":bob!~u@zb3s8yrduykma.irc PRIVMSG #chan :tester, welcome!")
+ (0.01 ":alice!~u@zb3s8yrduykma.irc PRIVMSG #chan :tester, welcome!"))
+
+((mode-chan 10 "MODE #chan")
+ (0.00 ":irc.foonet.org 324 tester #chan +Cnt")
+ (0.02 ":irc.foonet.org 329 tester #chan 1715474476")
+ (0.09 ":bob!~u@zb3s8yrduykma.irc PRIVMSG #chan :alice: And, uncle, so will I, an if I live.")
+ (0.03 ":alice!~u@zb3s8yrduykma.irc PRIVMSG #chan :bob: Speak to the people, and they pity her."))
+
+((privmsg-observer 10 "PRIVMSG observer :hi")
+ (0.04 ":observer!~u@hyyensdmcrjxc.irc PRIVMSG tester :hi?")
+ (0.07 ":bob!~u@zb3s8yrduykma.irc PRIVMSG #chan :To ask of whence you are: report it."))
+
+((quit 10 "QUIT :\2ERC\2")
+ (0.03 ":tester!~u@hyyensdmcrjxc.irc QUIT :Quit: \2ERC\2 5.6-git (IRC client for GNU Emacs 30.0.50)")
+ (0.03 "ERROR :Quit: \2ERC\2 5.6-git (IRC client for GNU Emacs 30.0.50)"))
+
+((drop 0 DROP))
diff --git a/test/lisp/erc/resources/base/renick/self/merge-query-b.eld b/test/lisp/erc/resources/base/renick/self/merge-query-b.eld
new file mode 100644
index 00000000000..4d7581b3884
--- /dev/null
+++ b/test/lisp/erc/resources/base/renick/self/merge-query-b.eld
@@ -0,0 +1,48 @@
+;; -*- mode: lisp-data; -*-
+((nick 10 "NICK dummy"))
+((user 10 "USER user 0 * :unknown")
+ (0.01 ":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.11.1")
+ (0.01 ":irc.foonet.org 003 dummy :This server was created Sun, 12 May 2024 00:41:10 UTC")
+ (0.00 ":irc.foonet.org 004 dummy irc.foonet.org ergo-v2.11.1 BERTZios CEIMRUabefhiklmnoqstuv Iabefhkloqv")
+ (0.03 ":irc.foonet.org 005 dummy AWAYLEN=390 BOT=B CASEMAPPING=ascii CHANLIMIT=#:100 CHANMODES=Ibe,k,fl,CEMRUimnstu CHANNELLEN=64 CHANTYPES=# CHATHISTORY=25 ELIST=U EXCEPTS EXTBAN=,m FORWARD=f INVEX :are supported by this server")
+ (0.03 ":irc.foonet.org 005 dummy KICKLEN=390 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 UTF8ONLY WHOX :are supported by this server")
+ (0.01 ":irc.foonet.org 005 dummy draft/CHATHISTORY=25 :are supported by this server")
+ (0.00 ":irc.foonet.org 251 dummy :There are 0 users and 6 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 6 clients and 0 servers")
+ (0.00 ":irc.foonet.org 265 dummy 6 6 :Current local users 6, max 6")
+ (0.00 ":irc.foonet.org 266 dummy 6 6 :Current global users 6, max 6")
+ (0.03 ":irc.foonet.org 422 dummy :MOTD File is missing")
+ (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."))
+
+((mode-user 10 "MODE dummy +i"))
+
+((join-chan 10 "JOIN #chan")
+ (0.01 ":irc.foonet.org 221 dummy +i")
+ (0.00 ":dummy!~u@hyyensdmcrjxc.irc JOIN #chan")
+ (0.02 ":irc.foonet.org 353 dummy = #chan :@fsbot alice bob observer someone dummy")
+ (0.01 ":irc.foonet.org 366 dummy #chan :End of NAMES list")
+ (0.00 ":bob!~u@zb3s8yrduykma.irc PRIVMSG #chan :dummy, welcome!")
+ (0.01 ":alice!~u@zb3s8yrduykma.irc PRIVMSG #chan :dummy, welcome!"))
+
+((mode-chan 10 "MODE #chan")
+ (0.00 ":irc.foonet.org 324 dummy #chan +Cnt")
+ (0.02 ":irc.foonet.org 329 dummy #chan 1715474476")
+ (0.09 ":bob!~u@zb3s8yrduykma.irc PRIVMSG #chan :alice: Indeed, sir, he that sleeps feels not the toothache; but a man that were to sleep your sleep, and a hangman to help him to bed, I think he would change places with his officer; for look you, sir, you know not which way you shall go.")
+ (0.03 ":alice!~u@zb3s8yrduykma.irc PRIVMSG #chan :bob: Pray you, sir, deliver me this paper."))
+
+((privmsg-observer 10 "PRIVMSG observer :hola")
+ (0.01 ":bob!~u@zb3s8yrduykma.irc PRIVMSG #chan :alice: In manner and form following, sir; all those three: I was seen with her in the manor-house, sitting with her upon the form, and taken following her into the park; which, put together, is, in manner and form following. Now, sir, for the manner,it is the manner of a man to speak to a woman, for the form,in some form.")
+ (0.05 ":alice!~u@zb3s8yrduykma.irc PRIVMSG #chan :In Isbel's case and mine own. Service is no heritage; and I think I shall never have the blessing of God till I have issue o' my body, for they say barnes are blessings.")
+ (0.01 ":observer!~u@hyyensdmcrjxc.irc PRIVMSG dummy :whodis?")
+ (0.02 ":bob!~u@zb3s8yrduykma.irc PRIVMSG #chan :alice: Have here bereft my brother of his life."))
+
+((nick-tester 10 "NICK tester")
+ (0.02 ":dummy!~u@hyyensdmcrjxc.irc NICK tester")
+
+ (0.04 ":alice!~u@zb3s8yrduykma.irc PRIVMSG #chan :bob: You have too courtly a wit for me: I'll rest.")
+ (0.07 ":bob!~u@zb3s8yrduykma.irc PRIVMSG #chan :alice: And abstinence engenders maladies."))
-- 
2.45.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #6: 0004-5.6-Mention-if-an-ERC-module-is-local-in-its-doc-str.patch --]
[-- Type: text/x-patch, Size: 4126 bytes --]

From 2d7b6e1d9b64bb123bed898aae1a6f393d2b46ca Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 23 May 2024 20:50:20 -0700
Subject: [PATCH 4/7] [5.6] Mention if an ERC module is local in its doc string

* lisp/erc/erc-common.el (erc--assemble-toggle)
(define-erc-module): Update language of doc string to distinguish
between local and global ERC modules.
* test/lisp/erc/erc-tests.el (define-erc-module--global)
(define-erc-module--local)
(define-erc-module--local/permanent-locals): Update expected output.
---
 lisp/erc/erc-common.el     |  8 ++++----
 test/lisp/erc/erc-tests.el | 18 +++++++++---------
 2 files changed, 13 insertions(+), 13 deletions(-)

diff --git a/lisp/erc/erc-common.el b/lisp/erc/erc-common.el
index 4115e314b39..51a93bdaa50 100644
--- a/lisp/erc/erc-common.el
+++ b/lisp/erc/erc-common.el
@@ -216,7 +216,7 @@ erc--assemble-toggle
     `(defun ,ablsym ,(if localp `(&optional ,arg) '())
        ,(erc--fill-module-docstring
          (if val "Enable" "Disable")
-         " ERC " (symbol-name name) " mode."
+         " ERC " (symbol-name name) " mode" (and localp " locally") "."
          (when localp
            (concat "\nWhen called interactively,"
                    " do so in all buffers for the current connection.")))
@@ -413,11 +413,11 @@ define-erc-module
     `(progn
        (define-minor-mode
          ,mode
-         ,(erc--fill-module-docstring (format "Toggle ERC %s mode.
-With a prefix argument ARG, enable %s if ARG is positive,
+         ,(erc--fill-module-docstring (format "Toggle ERC %s mode%s.
+If called interactively, enable `%s' if ARG is positive,
 and disable it otherwise.  If called from Lisp, enable the mode
 if ARG is omitted or nil.
-\n%s" name name doc))
+\n%s" name (if local-p " locally" "") mode doc))
          :global ,(not local-p)
          :group (erc--find-group ',name ,(and alias (list 'quote alias)))
          ,@(unless local-p `(:require ',(erc--find-feature name alias)))
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index a6e6d58cf9d..7bd5479f524 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -3666,9 +3666,9 @@ define-erc-module--global
 
                       (define-minor-mode erc-mname-mode
                         "Toggle ERC mname mode.
-With a prefix argument ARG, enable mname if ARG is positive, and
-disable it otherwise.  If called from Lisp, enable the mode if
-ARG is omitted or nil.
+If called interactively, enable `erc-mname-mode' if ARG is
+positive, and disable it otherwise.  If called from Lisp, enable
+the mode if ARG is omitted or nil.
 
 Some docstring."
                         :global t
@@ -3723,10 +3723,10 @@ define-erc-module--local
     (should (equal got
                    `(progn
                       (define-minor-mode erc-mname-mode
-                        "Toggle ERC mname mode.
-With a prefix argument ARG, enable mname if ARG is positive, and
-disable it otherwise.  If called from Lisp, enable the mode if
-ARG is omitted or nil.
+                        "Toggle ERC mname mode locally.
+If called interactively, enable `erc-mname-mode' if ARG is
+positive, and disable it otherwise.  If called from Lisp, enable
+the mode if ARG is omitted or nil.
 
 Some docstring."
                         :global nil
@@ -3737,7 +3737,7 @@ define-erc-module--local
                             (erc-mname-disable))))
 
                       (defun erc-mname-enable (&optional ,arg-en)
-                        "Enable ERC mname mode.
+                        "Enable ERC mname mode locally.
 When called interactively, do so in all buffers for the current
 connection."
                         (interactive "p")
@@ -3750,7 +3750,7 @@ define-erc-module--local
                             (ignore a) (ignore b))))
 
                       (defun erc-mname-disable (&optional ,arg-dis)
-                        "Disable ERC mname mode.
+                        "Disable ERC mname mode locally.
 When called interactively, do so in all buffers for the current
 connection."
                         (interactive "p")
-- 
2.45.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #7: 0005-5.6-Update-ERC-query-participants-on-JOIN-and-after-.patch --]
[-- Type: text/x-patch, Size: 12236 bytes --]

From ce69478143756927d661b47d17e79249c96a47f8 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Mon, 20 May 2024 03:08:47 -0700
Subject: [PATCH 5/7] [5.6] Update ERC query participants on JOIN and after
 NAMES

* lisp/erc/erc-backend.el (erc-server-JOIN): Update query membership
when someone else joins a channel.
(erc-server-NICK): Update query membership after someone else changes
their nick.
(erc-server-PRIVMSG): After printing a query message from some other
person, remove their user data if they're unknown, likely on account
of them not being in any channels.
(erc-server-311): Fix call to `erc-update-user-nick' so the userhost
login component is no longer supplied as the `info' parameter but
rather, correctly, as `login'.
(erc-server-352): Handle asterisk as channel param.
(erc-server-366): Update membership in all query buffers after
all names have been received.
* lisp/erc/erc-common.el (erc--get-server-user): New function, a thin
wrapper around `erc-get-server-user' for cases were inlining would
require declaring symbols not defined in erc-common.
* lisp/erc/erc.el (erc-channel-members): Mention that this is used
for queries as well.
(erc--ensure-query-member, erc--ensure-query-members): New functions.
(erc-cmd-QUERY): Ensure parties are present in the query buffer's
membership table if they're known to be on the server.
(erc-message-english-s352-you): New variable.  (Bug#70928)
---
 lisp/erc/erc-backend.el | 68 ++++++++++++++++++++++++++++-------------
 lisp/erc/erc-common.el  |  3 ++
 lisp/erc/erc.el         | 33 +++++++++++++++++---
 3 files changed, 79 insertions(+), 25 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 447139a2218..d1e7ff610c7 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -118,6 +118,8 @@ erc-nick
 (defvar erc-nick-change-attempt-count)
 (defvar erc-verbose-server-ping)
 
+(declare-function erc--ensure-query-member "erc" (name))
+(declare-function erc--ensure-query-members "erc" ())
 (declare-function erc--init-channel-modes "erc" (channel raw-args))
 (declare-function erc--open-target "erc" (target))
 (declare-function erc--parse-nuh "erc" (string))
@@ -1780,6 +1782,8 @@ erc--server-determine-join-display-context
                       (list 'JOIN ?n nick ?u login ?h host ?c chnl)))))
           (when buffer (set-buffer buffer))
           (erc-update-channel-member chnl nick nick t nil nil nil nil nil host login)
+          (unless (erc-current-nick-p nick)
+            (erc--ensure-query-member nick))
           ;; on join, we want to stay in the new channel buffer
           ;;(set-buffer ob)
           (apply #'erc-display-message parsed 'notice buffer args))))))
@@ -1903,7 +1907,8 @@ erc--wrangle-query-buffers-on-nick-change
         (run-hook-with-args 'erc-nick-changed-functions nn nick))
        (t
         (when erc-server-connected
-          (erc-networks--id-reload erc-networks--id proc parsed))
+          (erc-networks--id-reload erc-networks--id proc parsed)
+          (erc--ensure-query-member nn))
         (erc-handle-user-status-change 'nick (list nick login host) (list nn))
         (erc-display-message parsed 'notice bufs 'NICK ?n nick
                              ?u login ?h host ?N nn))))))
@@ -2051,7 +2056,7 @@ erc--speaker-status-prefix-wanted-p
              (erc--speaker-status-prefix-wanted-p nil)
              (erc-current-message-catalog erc--message-speaker-catalog)
              ;;
-             buffer statusmsg cmem-prefix fnick)
+             finalize buffer statusmsg cmem-prefix fnick)
         (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))
@@ -2078,10 +2083,12 @@ erc--speaker-status-prefix-wanted-p
               (setq buffer (erc--open-target tgt))))))
         (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.
+            (when privp
+              (erc--unhide-prompt)
+              ;; Remove untracked query partners after display.
+              (unless (erc--get-server-user nick)
+                (setq finalize  (lambda ()
+                                  (erc-remove-channel-member buffer nick)))))
             (erc-update-channel-member (if privp nick tgt) nick nick
                                        privp nil nil nil nil nil host login nil nil t)
             (defvar erc--cmem-from-nick-function)
@@ -2120,7 +2127,9 @@ erc--speaker-status-prefix-wanted-p
                   (run-hook-with-args 'erc-echo-notice-always-hook
                                       fmtmsg parsed buffer nick)
                   (run-hook-with-args-until-success
-                   'erc-echo-notice-hook fmtmsg parsed buffer nick))))))))))
+                   'erc-echo-notice-hook fmtmsg parsed buffer nick)))))
+          (when finalize (funcall finalize)))
+        nil))))
 
 (define-erc-response-handler (QUIT)
   "Another user has quit IRC." nil
@@ -2327,6 +2336,9 @@ erc--with-isupport-data
 See `erc-display-server-message'." nil
   (erc-display-server-message proc parsed))
 
+(define-erc-response-handler (263) "RPL_TRYAGAIN." nil
+  (erc-handle-unknown-server-response proc parsed))
+
 (define-erc-response-handler (275)
   "Display secure connection message." nil
   (pcase-let ((`(,nick ,_user ,_message)
@@ -2379,7 +2391,7 @@ erc--with-isupport-data
         (catalog-entry (intern (format "s%s" (erc-response.command parsed)))))
     (pcase-let ((`(,nick ,user ,host)
                  (cdr (erc-response.command-args parsed))))
-      (erc-update-user-nick nick nick host nil fname user)
+      (erc-update-user-nick nick nick host user fname)
       (erc-display-message
        parsed 'notice 'active catalog-entry
        ?n nick ?f fname ?u user ?h host))))
@@ -2541,18 +2553,28 @@ erc-server-322-message
     (erc-display-message parsed 'notice (erc-get-buffer channel proc)
                          's341 ?n nick ?c channel)))
 
-;; FIXME update or add server user instead when channel is "*".
+(defun erc--extract-352-full-name (contents)
+  "Return full name from 352 trailing param, discarding hop count."
+  (pcase contents
+    ((rx (: bot (+ (any "0-9")) " ") (let full-name (group (* nonl))) eot)
+     full-name)
+    (_ contents)))
+
 (define-erc-response-handler (352)
-  "WHO notice." nil
-  (pcase-let ((`(,channel ,user ,host ,_server ,nick ,away-flag)
-               (cdr (erc-response.command-args parsed))))
-    (let ((full-name (erc-response.contents parsed)))
-      (when (string-match "\\(^[0-9]+ \\)\\(.*\\)$" full-name)
-        (setq full-name (match-string 2 full-name)))
-      (erc-update-channel-member channel nick nick nil nil nil nil nil nil host user full-name)
-      (erc-display-message parsed 'notice 'active 's352
-                           ?c channel ?n nick ?a away-flag
-                           ?u user ?h host ?f full-name))))
+  "RPL_WHOREPLY response." nil
+  (pcase-let*
+      ((`(,_ ,channel ,user ,host ,_server ,nick ,flags, hop-real)
+        (erc-response.command-args parsed))
+       (full-name (erc--extract-352-full-name hop-real))
+       (selfp (string= channel "*"))
+       (template (if selfp 's352-you 's352)))
+    (if selfp
+        (erc-update-user-nick nick nick host user full-name)
+      (erc-update-channel-member channel nick nick nil nil nil nil nil nil
+                                 host user full-name))
+    (erc-display-message parsed 'notice 'active template
+                         ?c channel ?n nick ?a flags
+                         ?u user ?h host ?f full-name)))
 
 (define-erc-response-handler (353)
   "NAMES notice." nil
@@ -2567,7 +2589,9 @@ erc-server-322-message
 (define-erc-response-handler (366)
   "End of NAMES." nil
   (erc-with-buffer ((cadr (erc-response.command-args parsed)) proc)
-    (erc-channel-end-receiving-names)))
+    (erc-channel-end-receiving-names))
+  (erc--ensure-query-members)
+  nil)
 
 (define-erc-response-handler (367)
   "Channel ban list entries." nil
@@ -2633,7 +2657,9 @@ erc-server-322-message
       (erc-log (format "cmd: WHOWAS: %s" nick/channel))
       (erc-server-send (format "WHOWAS %s 1" nick/channel)))
     (erc-display-message parsed '(notice error) 'active
-                         's401 ?n nick/channel)))
+                         's401 ?n nick/channel)
+    (unless (erc-channel-p nick/channel)
+      (erc-remove-user nick/channel))))
 
 (define-erc-response-handler (402)
   "No such server." nil
diff --git a/lisp/erc/erc-common.el b/lisp/erc/erc-common.el
index 51a93bdaa50..c01ee6546cb 100644
--- a/lisp/erc/erc-common.el
+++ b/lisp/erc/erc-common.el
@@ -557,6 +557,9 @@ erc-get-server-user
      (gethash (erc-downcase ,nick)
               (erc-with-server-buffer erc-server-users)))))
 
+(defun erc--get-server-user (nick)
+  (erc-get-server-user nick))
+
 (defmacro erc--with-dependent-type-match (type &rest features)
   "Massage Custom :type TYPE with :match function that pre-loads FEATURES."
   `(backquote-list* ',(car type)
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 38547ea94bf..f070fd526ac 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -509,7 +509,7 @@ erc-connect-pre-hook
 
 (defvaralias 'erc-channel-users 'erc-channel-members)
 (defvar-local erc-channel-members nil
-  "Hash table of members in the current channel.
+  "Hash table of members in the current channel or query buffer.
 It associates nicknames with cons cells of the form
 \(SERVER-USER . MEMBER-DATA), where SERVER-USER is a
 `erc-server-user' object and MEMBER-DATA is a `erc-channel-user'
@@ -549,6 +549,27 @@ erc-add-server-user
   (erc-with-server-buffer
     (puthash (erc-downcase nick) user erc-server-users)))
 
+(defun erc--ensure-query-member (nick)
+  "Populate membership table in query buffer for online NICK."
+  (erc-with-buffer (nick)
+    (when-let (((zerop (hash-table-count erc-channel-users)))
+               (user (erc-get-server-user nick)))
+      (erc-update-current-channel-member nick nil t)
+      (erc--unhide-prompt)
+      t)))
+
+(defun erc--ensure-query-members ()
+  "Update membership tables in all query buffers.
+Ensure targets with an entry in `erc-server-users' are present in
+`erc-channel-members'."
+  (erc-with-all-buffers-of-server erc-server-process #'erc-query-buffer-p
+    (when-let (((zerop (hash-table-count erc-channel-users)))
+               (target (erc-target))
+               ((erc-get-server-user target)))
+      (erc-update-current-channel-member target nil t)
+      (erc--unhide-prompt))
+    erc-server-process))
+
 (defun erc-remove-server-user (nick)
   "This function is for internal use only.
 
@@ -5151,8 +5172,7 @@ erc-cmd-QUOTE
 
 (defun erc-cmd-QUERY (&optional user)
   "Open a query with USER.
-How the query is displayed (in a new window, frame, etc.) depends
-on the value of `erc-interactive-display'."
+Display the query buffer in accordance with `erc-interactive-display'."
   ;; FIXME: The doc string used to say at the end:
   ;; "If USER is omitted, close the current query buffer if one exists
   ;; - except this is broken now ;-)"
@@ -5168,7 +5188,11 @@ erc-cmd-QUERY
         (erc--display-context `((erc-interactive-display . /QUERY)
                                 ,@erc--display-context)))
     (erc-with-server-buffer
-     (erc--open-target user))))
+      (if-let ((buffer (erc-get-buffer user erc-server-process)))
+          (prog1 buffer
+            (erc-setup-buffer buffer))
+        (prog1 (erc--open-target user) ; becomes current buffer
+          (erc--ensure-query-member user))))))
 
 (defalias 'erc-cmd-Q #'erc-cmd-QUERY)
 
@@ -9521,6 +9545,7 @@ english
    (s333   . "%c: topic set by %n, %t")
    (s341   . "Inviting %n to channel %c")
    (s352   . "%-11c %-10n %-4a %u@%h (%f)")
+   (s352-you . "%n %a %u@%h (%f)")
    (s353   . "Users on %c: %u")
    (s367   . "Ban for %b on %c")
    (s367-set-by . "Ban for %b on %c set by %s on %t")
-- 
2.45.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #8: 0006-5.6-Retain-client-s-own-user-in-erc-server-users.patch --]
[-- Type: text/x-patch, Size: 15561 bytes --]

From 6f7be0cfb3dd0aadd8fdb2f49d93b24b4fa48c58 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 25 Apr 2024 05:16:23 -0700
Subject: [PATCH 6/7] [5.6] Retain client's own user in erc-server-users

* lisp/erc/erc-backend.el (erc-server-KICK, erc-server-PART): Use
`erc--remove-channel-user-but'.  In `erc-server-KICK', remove sender's
channel membership data after displaying the message so that nicks are
buttonized.
(erc-server-PART): Don't run `erc-remove-channel-member' for own
parts.
(erc-server-NICK): Print notice in all query buffers when the client
changes its own nick.
(erc-server-QUIT): Show messages in all query buffers when the client
quits, but prevent `track' from updating the mode line.
* lisp/erc/erc-common.el (erc--remove-user-from-targets): New function.
* lisp/erc/erc.el (erc-remove-server-user): Redo doc string.
(erc--retained-server-user): New variable.
(erc-remove-channel-user): Don't remove server user if it's the value
of `erc--retained-server-user'.
(erc-remove-channel-users): Redo doc.
(erc-remove-user): Defer to `erc--remove-user-from-targets'.
(erc--remove-channel-users-but): New function.  The only use case thus
far is for protecting the client's own `erc-server-users' entry from
removal when draining `erc-channel-members' tables after the client
leaves a target buffer or quits.
(erc-kill-buffer-function): Don't remove own user from
`erc-server-users'.
* test/lisp/erc/erc-scenarios-base-renick.el
(erc-scenarios-base-renick-queries-solo): Assert own client parting
from a common channel doesn't remove own user from query buffers.
Also assert other client parting removes them from queries.  (Bug#70928)
---
 lisp/erc/erc-backend.el                    | 34 ++++++---
 lisp/erc/erc-common.el                     | 12 ++++
 lisp/erc/erc.el                            | 80 ++++++++++++----------
 test/lisp/erc/erc-scenarios-base-renick.el | 24 ++++++-
 4 files changed, 103 insertions(+), 47 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index d1e7ff610c7..89e40ac8374 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -123,6 +123,8 @@ erc-verbose-server-ping
 (declare-function erc--init-channel-modes "erc" (channel raw-args))
 (declare-function erc--open-target "erc" (target))
 (declare-function erc--parse-nuh "erc" (string))
+(declare-function erc--query-list "erc" ())
+(declare-function erc--remove-channel-users-but "erc" (nick))
 (declare-function erc--target-from-string "erc" (string))
 (declare-function erc--update-modes "erc" (raw-args))
 (declare-function erc-active-buffer "erc" nil)
@@ -1796,7 +1798,6 @@ erc--server-determine-join-display-context
          (buffer (erc-get-buffer ch proc)))
     (pcase-let ((`(,nick ,login ,host)
                  (erc-parse-user (erc-response.sender parsed))))
-      (erc-remove-channel-member buffer tgt)
       (cond
        ((string= tgt (erc-current-nick))
         (erc-display-message
@@ -1805,17 +1806,20 @@ erc--server-determine-join-display-context
         (run-hook-with-args 'erc-kick-hook buffer)
         (erc-with-buffer
             (buffer)
-          (erc-remove-channel-users))
+          (erc--remove-channel-users-but tgt))
         (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
          parsed 'notice buffer
-         'KICK-by-you ?k tgt ?c ch ?r reason))
+         'KICK-by-you ?k tgt ?c ch ?r reason)
+        (erc-remove-channel-member buffer tgt))
        (t (erc-display-message
-             parsed 'notice buffer
-             'KICK ?k tgt ?n nick ?u login ?h host ?c ch ?r reason))))))
+           parsed 'notice buffer
+           'KICK ?k tgt ?n nick ?u login ?h host ?c ch ?r reason)
+          (erc-remove-channel-member buffer tgt)))))
+  nil)
 
 (define-erc-response-handler (MODE)
   "Handle server mode changes." nil
@@ -1894,6 +1898,8 @@ erc--wrangle-query-buffers-on-nick-change
       (cond
        ((string= nick (erc-current-nick))
         (cl-pushnew (erc-server-buffer) bufs)
+        ;; Show message in all query buffers.
+        (setq bufs (append (erc--query-list) bufs))
         (erc-set-current-nick nn)
         ;; Rename session, possibly rename server buf and all targets
         (when erc-server-connected
@@ -1923,15 +1929,15 @@ erc--wrangle-query-buffers-on-nick-change
       ;; When `buffer' is nil, `erc-remove-channel-member' and
       ;; `erc-remove-channel-users' do almost nothing, and the message
       ;; is displayed in the server buffer.
-      (erc-remove-channel-member buffer nick)
       (erc-display-message parsed 'notice buffer
                            'PART ?n nick ?u login
                            ?h host ?c chnl ?r (or reason ""))
-      (when (string= nick (erc-current-nick))
+      (cond
+       ((string= nick (erc-current-nick))
         (run-hook-with-args 'erc-part-hook buffer)
         (erc-with-buffer
             (buffer)
-          (erc-remove-channel-users))
+          (erc--remove-channel-users-but nick))
         (with-suppressed-warnings ((obsolete erc-delete-default-channel))
           (erc-delete-default-channel chnl buffer))
         (erc-update-mode-line buffer)
@@ -1939,7 +1945,8 @@ erc--wrangle-query-buffers-on-nick-change
         (when (and erc-kill-buffer-on-part buffer)
           (defvar erc-killing-buffer-on-part-p)
           (let ((erc-killing-buffer-on-part-p t))
-            (kill-buffer buffer))))))
+            (kill-buffer buffer))))
+       (t (erc-remove-channel-member buffer nick)))))
   nil)
 
 (define-erc-response-handler (PING)
@@ -2134,15 +2141,20 @@ erc--speaker-status-prefix-wanted-p
 (define-erc-response-handler (QUIT)
   "Another user has quit IRC." nil
   (let ((reason (erc-response.contents parsed))
+        (erc--msg-prop-overrides erc--msg-prop-overrides)
         bufs)
     (pcase-let ((`(,nick ,login ,host)
                  (erc-parse-user (erc-response.sender parsed))))
       (setq bufs (erc-buffer-list-with-nick nick proc))
-      (erc-remove-user nick)
+      (when (erc-current-nick-p nick)
+        (setq bufs (append (erc--query-list) bufs))
+        (push '(erc--skip . (track)) erc--msg-prop-overrides))
       (setq reason (erc-wash-quit-reason reason nick login host))
       (erc-display-message parsed 'notice bufs
                            'QUIT ?n nick ?u login
-                           ?h host ?r reason))))
+                           ?h host ?r reason)
+      (erc-remove-user nick)))
+  nil)
 
 (define-erc-response-handler (TOPIC)
   "The channel topic has changed." nil
diff --git a/lisp/erc/erc-common.el b/lisp/erc/erc-common.el
index c01ee6546cb..4ba7990ab98 100644
--- a/lisp/erc/erc-common.el
+++ b/lisp/erc/erc-common.el
@@ -560,6 +560,18 @@ erc-get-server-user
 (defun erc--get-server-user (nick)
   (erc-get-server-user nick))
 
+(define-inline erc--remove-user-from-targets (downcased-nick buffers)
+  "Remove DOWNCASED-NICK from `erc-channel-members' in BUFFERS."
+  (inline-quote
+   (progn
+     (defvar erc-channel-members-changed-hook)
+     (dolist (buffer ,buffers)
+       (when (buffer-live-p buffer)
+         (with-current-buffer buffer
+           (remhash ,downcased-nick erc-channel-users)
+           (when erc-channel-members-changed-hook
+             (run-hooks 'erc-channel-members-changed-hook))))))))
+
 (defmacro erc--with-dependent-type-match (type &rest features)
   "Massage Custom :type TYPE with :match function that pre-loads FEATURES."
   `(backquote-list* ',(car type)
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index f070fd526ac..cd05b1b81a9 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -571,13 +571,7 @@ erc--ensure-query-members
     erc-server-process))
 
 (defun erc-remove-server-user (nick)
-  "This function is for internal use only.
-
-Removes the user with nickname NICK from the `erc-server-users'
-hash table.  This user is not removed from the
-`erc-channel-users' lists of other buffers.
-
-See also: `erc-remove-user'."
+  "Remove NICK from the session's `erc-server-users' table."
   (erc-with-server-buffer
     (remhash (erc-downcase nick) erc-server-users)))
 
@@ -600,15 +594,27 @@ erc-change-user-nickname
               (puthash (erc-downcase new-nick) cdata
                        erc-channel-users)))))))
 
-(defun erc-remove-channel-user (nick)
-  "This function is for internal use only.
-
-Removes the user with nickname NICK from the `erc-channel-users'
-list for this channel.  If this user is not in the
-`erc-channel-users' list of any other buffers, the user is also
-removed from the server's `erc-server-users' list.
+(defvar erc--forget-server-user-function
+  #'erc--forget-server-user-ignoring-queries
+  "Function to conditionally remove a user from `erc-server-users'.
+Called with a nick and its `erc-server-user' object.")
+
+(defun erc--forget-server-user (nick user)
+  "Remove NICK's USER from server table if they're not in any target buffers."
+  (unless (erc-server-user-buffers user)
+    (erc-remove-server-user nick)))
+
+(defun erc--forget-server-user-ignoring-queries (nick user)
+  "Remove NICK's USER from `erc-server-users' if they've parted all channels."
+  (let ((buffers (erc-server-user-buffers user)))
+    (when (or (null buffers) (cl-every #'erc-query-buffer-p buffers))
+      (when buffers
+        (erc--remove-user-from-targets (erc-downcase nick) buffers))
+      (erc-remove-server-user nick))))
 
-See also: `erc-remove-server-user' and `erc-remove-user'."
+(defun erc-remove-channel-user (nick)
+  "Remove NICK from the current target buffer's `erc-channel-members'.
+If this was their only target, also remove them from `erc-server-users'."
   (let ((channel-data (erc-get-channel-user nick)))
     (when channel-data
       (let ((user (car channel-data)))
@@ -616,32 +622,19 @@ erc-remove-channel-user
               (delq (current-buffer)
                     (erc-server-user-buffers user)))
         (remhash (erc-downcase nick) erc-channel-users)
-        (if (null (erc-server-user-buffers user))
-            (erc-remove-server-user nick))))))
+        (funcall erc--forget-server-user-function nick user)))))
 
 (defun erc-remove-user (nick)
-  "This function is for internal use only.
-
-Removes the user with nickname NICK from the `erc-server-users'
-list as well as from all `erc-channel-users' lists.
-
-See also: `erc-remove-server-user' and
-`erc-remove-channel-user'."
+  "Remove NICK from the server and all relevant channels tables."
   (let ((user (erc-get-server-user nick)))
     (when user
-      (let ((buffers (erc-server-user-buffers user)))
-        (dolist (buf buffers)
-          (if (buffer-live-p buf)
-              (with-current-buffer buf
-                (remhash (erc-downcase nick) erc-channel-users)
-                (run-hooks 'erc-channel-members-changed-hook)))))
+      (erc--remove-user-from-targets (erc-downcase nick)
+                                     (erc-server-user-buffers user))
       (erc-remove-server-user nick))))
 
 (defun erc-remove-channel-users ()
-  "This function is for internal use only.
-
-Removes all users in the current channel.  This is called by
-`erc-server-PART' and `erc-server-QUIT'."
+  "Drain current buffer's `erc-channel-members' table.
+Also remove members from the server table if this was their only buffer."
   (when (erc--target-channel-p erc--target)
     (setf (erc--target-channel-joined-p erc--target) nil))
   (when (and erc-server-connected
@@ -652,6 +645,17 @@ erc-remove-channel-users
              erc-channel-users)
     (clrhash erc-channel-users)))
 
+(defun erc--remove-channel-users-but (nick)
+  "Drain channel users and remove from server, sparing NICK."
+  (when-let ((users (erc-with-server-buffer erc-server-users))
+             (my-user (gethash (erc-downcase nick) users))
+             (original-function erc--forget-server-user-function)
+             (erc--forget-server-user-function
+              (lambda (nick user)
+                (unless (eq user my-user)
+                  (funcall original-function nick user)))))
+    (erc-remove-channel-users)))
+
 (defmacro erc--define-channel-user-status-compat-getter (name c d)
   "Define a gv getter for historical `erc-channel-user' status slot NAME.
 Expect NAME to be a string, C to be its traditionally associated
@@ -2172,6 +2176,10 @@ erc-buffer-list-with-nick
           (erc-server-user-buffers user)
         nil))))
 
+(defun erc--query-list ()
+  "Return all query buffers for the current connection."
+  (erc-buffer-list #'erc-query-buffer-p erc-server-process))
+
 ;; Some local variables
 
 ;; TODO eventually deprecate this variable
@@ -9677,7 +9685,9 @@ erc-kill-buffer-function
 `erc-kill-channel-hook' if a channel buffer was killed,
 or `erc-kill-buffer-hook' if any other buffer."
   (when (eq major-mode 'erc-mode)
-    (erc-remove-channel-users)
+    (when-let ((erc--target)
+               (nick (erc-current-nick)))
+      (erc--remove-channel-users-but nick))
     (cond
      ((eq (erc-server-buffer) (current-buffer))
       (run-hooks 'erc-kill-server-hook))
diff --git a/test/lisp/erc/erc-scenarios-base-renick.el b/test/lisp/erc/erc-scenarios-base-renick.el
index 813cb758ee4..866075e0b3b 100644
--- a/test/lisp/erc/erc-scenarios-base-renick.el
+++ b/test/lisp/erc/erc-scenarios-base-renick.el
@@ -185,21 +185,43 @@ erc-scenarios-base-renick-queries-solo
       (with-current-buffer (erc-d-t-wait-for 10 (get-buffer "Lal"))
         (funcall expect 10 "<Lal> hello")
         (erc-scenarios-common-say "hi")
+        (should-not (erc-get-channel-member "tester"))
         (funcall expect 10 "is now known as Linguo")
         ;; No duplicate message.
         (funcall expect -0.1 "is now known as Linguo")
         ;; No duplicate buffer.
         (erc-d-t-wait-for 1 (equal (buffer-name) "Linguo"))
         (should-not (get-buffer "Lal"))
+        ;; Channel member has been updated
+        (should-not (erc-get-channel-member "Lal"))
+        (should-not (erc-get-server-user "Lal"))
+        (should (erc-get-channel-member "Linguo"))
         (erc-scenarios-common-say "howdy Linguo")))
 
     (with-current-buffer "#foo"
       (funcall expect 10 "is now known as Linguo")
       (funcall expect -0.1 "is now known as Linguo")
+      (funcall expect 10 "has left"))
+
+    ;; User parting a common channel removes them from queries.
+    (with-current-buffer "Linguo"
+      (should-not (erc-get-channel-member "tester"))
+      (erc-d-t-wait-for 10 (null (erc-get-channel-member "Linguo")))
+      (should-not (erc-get-server-user "Linguo")))
+
+    ;; Leaving the client's only channel doesn't remove its user data
+    ;; from the server table (see below, after "get along ...").
+    (with-current-buffer "#foo"
       (erc-scenarios-common-say "/part"))
 
+    ;; Server and "channel" user are *not* (re)created upon receiving
+    ;; a direct message for a user we already have an open query with
+    ;; but with whom we no longer share a channel.
     (with-current-buffer "Linguo"
-      (funcall expect 10 "get along"))))
+      (funcall expect 10 "get along")
+      (should-not (erc-get-channel-member "Linguo"))
+      (should-not (erc-get-channel-member "tester"))
+      (should (erc-get-server-user "tester")))))
 
 ;; Someone you have a query with disconnects and reconnects under a
 ;; new nick (perhaps due to their client appending a backtick or
-- 
2.45.1


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #9: 0007-5.6-Add-ERC-module-querypoll-as-monitor-fallback.patch --]
[-- Type: text/x-patch, Size: 17121 bytes --]

From 6635456239fbb7ac0ac818992129114288c0f7f5 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Wed, 22 May 2024 22:59:54 -0700
Subject: [PATCH 7/7] [5.6] Add ERC module querypoll as monitor fallback

* lisp/erc/erc-goodies.el (erc--querypoll-period-secs)
(erc--querypoll-ring)
(erc--querypoll-timer): New variables.
(erc-querypoll-mode, erc-querypoll-enable, erc-querypoll-disable):
New module for polling with WHO requests for query targets.
(erc--querypoll-subscribe)
(erc--querypoll-on-352)
(erc--querypoll-send): New function.
* lisp/erc/erc-speedbar.el
(erc-speedbar-buttons): Dispatch queries as if they were channels when
`erc--queries-current-p' returns non-nil.
(erc-speedbar-insert-target): Honor option
`erc-speedbar-show-query-as-channel'.  This affects both the plain
speedbar integration as well as the `nickbar' module added for
bug#63595.
* lisp/erc/erc.el (erc--queries-current-p): New function.
* test/lisp/erc/erc-goodies-tests.el
(erc--querypoll-compute-period)
(erc--querypoll-target-in-chan-p)
(erc--querypoll-get-length)
(erc--querypoll-get-next): New tests.  (Bug#70928)
---
 etc/ERC-NEWS                       |  12 ++
 lisp/erc/erc-goodies.el            | 187 +++++++++++++++++++++++++++++
 lisp/erc/erc-speedbar.el           |  13 +-
 lisp/erc/erc.el                    |   4 +
 test/lisp/erc/erc-goodies-tests.el |  57 +++++++++
 5 files changed, 268 insertions(+), 5 deletions(-)

diff --git a/etc/ERC-NEWS b/etc/ERC-NEWS
index 07e9608c836..0341bcc6d04 100644
--- a/etc/ERC-NEWS
+++ b/etc/ERC-NEWS
@@ -100,6 +100,18 @@ one's optionally accessible from the keyboard, just like any other
 side window.  Hit '<RET>' over a nick to spawn a "/QUERY" or a
 "Lastlog" (Occur) session.  See 'erc-nickbar-mode' for more.
 
+** New module to keep tabs on query pals who aren't in your channels.
+ERC has gotten a bit pickier about managing participants in query
+buffers.  "Untracked" correspondents no longer appear automatically in
+membership tables, even if you respond or initiate contact.  Instead,
+ERC only adds and removes participant data when these same users join
+and leave channels.  Anyone uncomfortable with the apparent
+uncertainty this brings can look to the new 'querypoll' module, which
+periodically sends WHO requests to keep track of correspondents.
+Those familiar with the IRCv3 Monitor extension can think of this as
+"fallback code" and a temporary placeholder for the real thing.
+Add 'querypoll' (and 'nickbar') to 'erc-modules' to try it out.
+
 ** Option 'erc-timestamp-use-align-to' more versatile.
 While this option has always offered to right-align stamps via the
 'display' text property, it's now more effective at doing so when set
diff --git a/lisp/erc/erc-goodies.el b/lisp/erc/erc-goodies.el
index fe44c3bdfcb..180c5c3758e 100644
--- a/lisp/erc/erc-goodies.el
+++ b/lisp/erc/erc-goodies.el
@@ -1114,6 +1114,193 @@ erc-occur
              nil erc-server-process)))
   (multi-occur (erc-buffer-list nil proc) string))
 
+
+;;;; querypoll
+
+(declare-function ring-empty-p "ring" (ring))
+(declare-function ring-insert "ring" (ring item))
+(declare-function ring-insert+extend "ring" (ring item))
+(declare-function ring-length "ring" (ring))
+(declare-function ring-member "ring" (ring item))
+(declare-function ring-ref "ring" (ring index))
+(declare-function ring-remove "ring" (ring &optional index))
+
+(defvar-local erc--querypoll-ring nil)
+(defvar-local erc--querypoll-timer nil)
+
+(defcustom erc-querypoll-exclude-regexp
+  (rx bot (or (: "*" (+ nonl)) (: (+ (in "A-Za-z")) "Serv")) eot)
+  "Pattern to skip polling for bots and services you regularly query."
+  :group 'erc
+  :package-version '(ERC . "5.6")
+  :type 'regexp)
+
+;;;###autoload(autoload 'erc-querypoll-mode "erc-goodies" nil t)
+(define-erc-module querypoll nil
+  "Send periodic \"WHO\" requests for each query buffer.
+But omit query participants who share a channel with the client.
+
+Once ERC implements the `monitor' extension, this module will serve as
+an optional fallback for keeping query-participant rolls up to date on
+servers that lack support or are stingy with their allotments.  Until
+such time, this module should be considered experimental.
+
+This is a local ERC module, so selectively polling only a subset of
+query targets is possible but cumbersome.  To do so, ensure
+`erc-querypoll-mode' is enabled in the server buffer, and then toggle it
+as appropriate in desired query buffers.  To stop polling for the
+current connection, toggle off the command \\[erc-querypoll-mode] from a
+server buffer, or run \\`M-x C-u erc-querypoll-disable RET' from a
+target buffer."
+  ((if erc--target
+       (if (erc-query-buffer-p)
+           (progn ; accommodate those who eschew `erc-modules'
+             (erc-with-server-buffer
+               (unless erc-querypoll-mode
+                 (erc-querypoll-mode +1)))
+             (erc--querypoll-subscribe (current-buffer)))
+         (erc-querypoll-mode -1))
+     (setq-local erc--querypoll-ring (make-ring 5))
+     (erc-with-all-buffers-of-server erc-server-process nil
+       (unless erc-querypoll-mode
+         (erc-querypoll-mode +1)))))
+  ((when erc--querypoll-timer
+     (cancel-timer erc--querypoll-timer))
+   (if erc--target
+       (when-let (((erc-query-buffer-p))
+                  (ring (erc-with-server-buffer erc--querypoll-ring))
+                  (index (ring-member ring (current-buffer)))
+                  ((not (erc--querypoll-target-in-chan-p (current-buffer)))))
+         (ring-remove ring index)
+         (unless (erc-current-nick-p (erc-target))
+           (erc-remove-current-channel-member (erc-target))))
+     (erc-with-all-buffers-of-server erc-server-process #'erc-query-buffer-p
+       (erc-querypoll-mode -1)))
+   (kill-local-variable 'erc--querypoll-ring)
+   (kill-local-variable 'erc--querypoll-timer))
+  'local)
+
+(cl-defmethod erc--queries-current-p (&context (erc-querypoll-mode (eql t))) t)
+
+(defvar erc-querypoll-period-params '(10 10 1)
+  "Parameters affecting the delay with respect to the number of buffers.
+The elements represent some parameters of an exponential decay function,
+a(e)^{-x/b}+c.  The first number (a) affects the overall scaling.  A
+higher value means longer delays for all query buffers relative to queue
+length.  The second number (b) determines how quickly the delay
+decreases as the queue length increases.  Larger values make the delay
+taper off more gradually.  The last number (c) sets the minimum delay
+between updates regardless of queue length.")
+
+(defun erc--querypoll-compute-period (queue-size)
+  "Calculate delay based on QUEUE-SIZE."
+  (let ((scale (nth 0 erc-querypoll-period-params))
+        (rate (* 1.0 (nth 1 erc-querypoll-period-params)))
+        (min (nth 2 erc-querypoll-period-params)))
+    (+ (* scale (exp (/ (- queue-size) rate))) min)))
+
+(defun erc--querypoll-target-in-chan-p (buffer)
+  "Determine whether buffer's target, as a user, is joined to any channels."
+  (and-let*
+      ((target (erc--target-string (buffer-local-value 'erc--target buffer)))
+       (user (erc-get-server-user target))
+       (buffers (erc-server-user-buffers user))
+       ((seq-some #'erc-channel-p buffers)))))
+
+(defun erc--querypoll-get-length (ring)
+  "Return the effective length of RING, discounting chan members."
+  (let ((count 0))
+    (dotimes (i (ring-length ring))
+      (unless (erc--querypoll-target-in-chan-p (ring-ref ring i))
+        (cl-incf count 1)))
+    count))
+
+(defun erc--querypoll-get-next (ring)
+  (let ((n (ring-length ring)))
+    (catch 'found
+      (while (natnump (cl-decf n))
+        (when-let ((buffer (ring-remove ring))
+                   ((buffer-live-p buffer)))
+          ;; Push back buffers for users joined to some chan.
+          (if (erc--querypoll-target-in-chan-p buffer)
+              (ring-insert ring buffer)
+            (throw 'found buffer)))))))
+
+(defun erc--querypoll-subscribe (query-buffer &optional penalty)
+  "Add QUERY-BUFFER to FIFO and ensure timer is running."
+  (when query-buffer
+    (cl-assert (erc-query-buffer-p query-buffer)))
+  (erc-with-server-buffer
+    (when (and query-buffer
+               (not (with-current-buffer query-buffer
+                      (or (erc-current-nick-p (erc-target))
+                          (string-match erc-querypoll-exclude-regexp
+                                        (erc-target)))))
+               (not (ring-member erc--querypoll-ring query-buffer)))
+      (ring-insert+extend erc--querypoll-ring query-buffer))
+    (unless erc--querypoll-timer
+      (setq erc--querypoll-timer
+            (let* ((length (erc--querypoll-get-length erc--querypoll-ring))
+                   (period (erc--querypoll-compute-period length)))
+              (run-at-time (+ (or penalty 0) period)
+                           nil #'erc--querypoll-send (current-buffer)))))))
+
+(defun erc--querypoll-on-352 (target-nick args)
+  "Add or update `erc-server-users' data for TARGET-NICK from ARGS.
+Then add user to participant rolls in any existing query buffers."
+  (pcase-let
+      ((`(,_ ,channel ,login ,host ,_server ,nick ,_flags, hop-real) args))
+    (when (and (string= channel "*") (erc-nick-equal-p nick target-nick))
+      (if-let ((user (erc-get-server-user nick)))
+          (erc-update-user user nick host login
+                           (erc--extract-352-full-name hop-real))
+        ;; Don't add unless target is already known.
+        (when (erc-get-buffer nick erc-server-process)
+          (erc-add-server-user
+           nick (make-erc-server-user
+                 :nickname nick :login login :host host
+                 :full-name (erc--extract-352-full-name hop-real)))))
+      (erc--ensure-query-member nick)
+      t)))
+
+;; This uses heuristics to associate replies to the initial request
+;; because ERC does not yet support `labeled-response'.
+(defun erc--querypoll-send (server-buffer)
+  "Send a captive \"WHO\" in SERVER-BUFFER."
+  (when (and (buffer-live-p server-buffer)
+             (buffer-local-value 'erc-server-connected server-buffer))
+    (with-current-buffer server-buffer
+      (setq erc--querypoll-timer nil)
+      (if-let ((buffer (erc--querypoll-get-next erc--querypoll-ring)))
+          (letrec
+              ((target (erc--target-string
+                        (buffer-local-value 'erc--target buffer)))
+               (penalty 0)
+               (here-fn (erc-once-with-server-event
+                         "352" (lambda (_ parsed)
+                                 (erc--querypoll-on-352
+                                  target (erc-response.command-args parsed)))))
+               (done-fn (erc-once-with-server-event
+                         "315"
+                         (lambda (_ parsed)
+                           (if (memq here-fn erc-server-352-functions)
+                               (erc-remove-user
+                                (nth 1 (erc-response.command-args parsed)))
+                             (remove-hook 'erc-server-352-functions here-fn t))
+                           (remove-hook 'erc-server-263-functions fail-fn t)
+                           (remove-hook 'erc-server-315-functions done-fn t)
+                           (erc--querypoll-subscribe buffer penalty)
+                           t)))
+               (fail-fn (erc-once-with-server-event
+                         "263"
+                         (lambda (proc parsed)
+                           (setq penalty 60)
+                           (funcall done-fn proc parsed)
+                           t))))
+            (erc-server-send (concat "WHO " target)))
+        (unless (ring-empty-p erc--querypoll-ring)
+          (erc--querypoll-subscribe nil 30))))))
+
 (provide 'erc-goodies)
 
 ;;; erc-goodies.el ends here
diff --git a/lisp/erc/erc-speedbar.el b/lisp/erc/erc-speedbar.el
index 9cde452be58..d4f91bb363a 100644
--- a/lisp/erc/erc-speedbar.el
+++ b/lisp/erc/erc-speedbar.el
@@ -133,7 +133,7 @@ erc-speedbar-browser
 (defun erc-speedbar-buttons (buffer)
   "Create buttons for speedbar in BUFFER."
   (erase-buffer)
-  (let (serverp chanp queryp)
+  (let (serverp chanp queryp queries-current-p)
     (with-current-buffer buffer
       ;; The function `dframe-help-echo' checks the default value of
       ;; `dframe-help-echo-function' when deciding whether to visit
@@ -145,13 +145,14 @@ erc-speedbar-buttons
       (setq-local dframe-help-echo-function #'ignore)
       (setq serverp (erc--server-buffer-p))
       (setq chanp (erc-channel-p (erc-default-target)))
-      (setq queryp (erc-query-buffer-p)))
+      (setq queryp (erc-query-buffer-p)
+            queries-current-p (erc--queries-current-p)))
     (defvar erc-nickbar-mode)
     (cond ((and erc-nickbar-mode (null (get-buffer-window speedbar-buffer)))
            (run-at-time 0 nil #'erc-nickbar-mode -1))
           (serverp
 	   (erc-speedbar-channel-buttons nil 0 buffer))
-	  (chanp
+          ((or chanp (and queryp queries-current-p))
 	   (erc-speedbar-insert-target buffer 0)
 	   (forward-line -1)
 	   (erc-speedbar-expand-channel "+" buffer 0))
@@ -205,7 +206,8 @@ erc-speedbar-channel-buttons
 	  t)))))
 
 (defun erc-speedbar-insert-target (buffer depth)
-  (if (erc--target-channel-p (buffer-local-value 'erc--target buffer))
+  (if (with-current-buffer buffer
+        (or (erc--target-channel-p erc--target) (erc--queries-current-p)))
       (progn
         (speedbar-make-tag-line
          'bracket ?+ 'erc-speedbar-expand-channel buffer
@@ -218,8 +220,9 @@ erc-speedbar-insert-target
             (speedbar-add-indicator (format "(%d)" (hash-table-count table)))
             (rx "(" (+ (any "0-9")) ")"))))
     ;; Query target
+    (cl-assert (erc-query-buffer-p buffer))
     (speedbar-make-tag-line
-     nil nil nil nil
+     'bracket ?? nil nil
      (buffer-name buffer) 'erc-speedbar-goto-buffer buffer nil
      depth)))
 
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index cd05b1b81a9..8ec5a54ab3b 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -549,6 +549,10 @@ erc-add-server-user
   (erc-with-server-buffer
     (puthash (erc-downcase nick) user erc-server-users)))
 
+(cl-defmethod erc--queries-current-p ()
+  "Return non-nil if ERC actively updates query manifests."
+  (and (erc-query-buffer-p) (erc-get-channel-member (erc-target))))
+
 (defun erc--ensure-query-member (nick)
   "Populate membership table in query buffer for online NICK."
   (erc-with-buffer (nick)
diff --git a/test/lisp/erc/erc-goodies-tests.el b/test/lisp/erc/erc-goodies-tests.el
index 7cbaa39d3f7..ead0bf5a979 100644
--- a/test/lisp/erc/erc-goodies-tests.el
+++ b/test/lisp/erc/erc-goodies-tests.el
@@ -609,4 +609,61 @@ erc--get-inserted-msg-bounds/readonly
      (should (equal '(3 . 11) (erc--get-inserted-msg-bounds arg))))))
 
 
+;;;; querypoll
+
+(ert-deftest erc--querypoll-compute-period ()
+  (should (equal (mapcar (lambda (i)
+                           (/ (round (* 100 (erc--querypoll-compute-period i)))
+                              100.0))
+                         (number-sequence 0 10))
+                 '(11.0 10.05 9.19 8.41 7.7 7.07 6.49 5.97 5.49 5.07 4.68))))
+
+(declare-function ring-insert "ring" (ring item))
+
+(ert-deftest erc--querypoll-target-in-chan-p ()
+  (erc-tests-common-make-server-buf)
+  (with-current-buffer (erc--open-target "#chan")
+    (erc-update-current-channel-member "bob" "bob" 'addp))
+
+  (with-current-buffer (erc--open-target "bob")
+    (should (erc--querypoll-target-in-chan-p (current-buffer))))
+
+  (with-current-buffer (erc--open-target "alice")
+    (should-not (erc--querypoll-target-in-chan-p (current-buffer))))
+
+  (when noninteractive
+    (erc-tests-common-kill-buffers)))
+
+(ert-deftest erc--querypoll-get-length ()
+  (erc-tests-common-make-server-buf)
+  (with-current-buffer (erc--open-target "#chan")
+    (erc-update-current-channel-member "bob" "bob" 'addp))
+
+  (let ((ring (make-ring 5)))
+    (ring-insert ring (with-current-buffer (erc--open-target "bob")))
+    (should (= 0 (erc--querypoll-get-length ring)))
+    (ring-insert ring (with-current-buffer (erc--open-target "alice")))
+    (should (= 1 (erc--querypoll-get-length ring))))
+
+  (when noninteractive
+    (erc-tests-common-kill-buffers)))
+
+(ert-deftest erc--querypoll-get-next ()
+  (erc-tests-common-make-server-buf)
+  (with-current-buffer (erc--open-target "#chan")
+    (erc-update-current-channel-member "bob" "bob" 'addp)
+    (erc-update-current-channel-member "alice" "alice" 'addp))
+
+  (let ((ring (make-ring 5)))
+    (ring-insert ring (with-current-buffer (erc--open-target "bob")))
+    (ring-insert ring (with-current-buffer (erc--open-target "dummy")))
+    (ring-insert ring (with-current-buffer (erc--open-target "alice")))
+    (ring-insert ring (with-current-buffer (erc--open-target "tester")))
+    (kill-buffer (get-buffer "dummy"))
+
+    (should (eq (get-buffer "tester") (erc--querypoll-get-next ring))))
+
+  (when noninteractive
+    (erc-tests-common-kill-buffers)))
+
 ;;; erc-goodies-tests.el ends here
-- 
2.45.1


^ permalink raw reply related	[flat|nested] 3+ messages in thread

* bug#70928: 30.0.50; ERC 5.6: Reuse query buffers for round-trip nick changes in ERC
  2024-05-25  3:13 ` J.P.
@ 2024-05-28 13:37   ` J.P.
  0 siblings, 0 replies; 3+ messages in thread
From: J.P. @ 2024-05-28 13:37 UTC (permalink / raw)
  To: 70928-done; +Cc: emacs-erc

"J.P." <jp@neverwas.me> writes:

> Here's how I imagine things working in a saner ERC:
>
>   - A user's presence in a channel will dictate whether they exist in
>     the server buffer's `erc-server-users' table.
>
>   - Issuing a /query will create a user entry in the query buffer's
>     `erc-channel-members' table if they exist in the server-wide table
>     (meaning they're present in some channel).
>
>   - Users parting or being kicked from a channel will see their data
>     removed from all query tables (and the server table) if they're no
>     longer joined to any other channels.
>
>   - Insertion hooks running in query buffers can always expect to see a
>     speaker's user's in its `erc-channel-members' table. If they're
>     absent, a temporary user will be created for the duration of
>     response handling.
>
>   - A new, optional module will be added to mimic the effect of the
>     Monitor extension and to serve as a fallback after ERC adds support
>     (see bug#49860). When it's active, users in queries who aren't also
>     in a channel will be periodically polled for and kept up to date.
>
>   - A client's own user for its current nick will be absent in all query
>     tables but present, once discovered, in the server-wide table for
>     the remainder of the session.
>
> The attached patches attempt to implement the proposed changes. Comments
> welcome.

I've installed these changes as

  75aefe65148 * Reuse old query buffers for reassumed nicks in ERC
  04477cf97be * Tether query rolls to channel membership in ERC
  5f84213c980 * Retain client's own user in erc-server-users
  6888bbbe832 * Add ERC module querypoll as monitor placeholder

Thanks and closing (for now).





^ permalink raw reply	[flat|nested] 3+ messages in thread

end of thread, other threads:[~2024-05-28 13:37 UTC | newest]

Thread overview: 3+ messages (download: mbox.gz follow: Atom feed
-- links below jump to the message on this page --
2024-05-14  1:00 bug#70928: 30.0.50; ERC 5.6: Reuse query buffers for round-trip nick changes in ERC J.P.
2024-05-25  3:13 ` J.P.
2024-05-28 13:37   ` J.P.

Code repositories for project(s) associated with this public inbox

	https://git.savannah.gnu.org/cgit/emacs.git

This is a public inbox, see mirroring instructions
for how to clone and mirror all data and code used for this inbox;
as well as URLs for read-only IMAP folder(s) and NNTP newsgroup(s).