unofficial mirror of bug-gnu-emacs@gnu.org 
 help / color / mirror / code / Atom feed
* bug#68861: 30.0.50; ERC 5.x: Introduce a modern message-insertion API
@ 2024-02-01  3:07 J.P.
  0 siblings, 0 replies; 2+ messages in thread
From: J.P. @ 2024-02-01  3:07 UTC (permalink / raw)
  To: 68861; +Cc: emacs-erc

Severity: wishlist

Some recent ramblings on this topic (by me) were crudely tacked on to
the end of the now closed bug#67677. In summary, the solution originally
delivered by that bug of having all chat messages formatted by literal
`format-spec' templates was left wanting in terms of an easily exposable
and extendable public API. What it currently provides is merely a
declarative means of defining the layout of a message in an easily
verifiable manner, which is helpful for gaining an intuition into how a
message will look upon insertion. But it leaves unaddressed a
requirement commonly expressed by third parties, namely, a convenient
and straightforward way of influencing specific portions of a message
prior to formatting and without regard for overall layout.

The first attempt at providing such granular access was the awkwardly
termed "msgfspec" proposal offered in bug#67677 [1]. While supposedly
focused on convenience, it suffered from a fatal flaw in that modifying
a base template still involved scanning to splice in additions, even if
mostly hidden from the user. Instead, I think it'd be preferable to take
a more traditional tack and preserve the various constituent parts of a
template as separate nodes in a tree-like structure for deferred
assembly just prior to formatting, perhaps in conjunction with some
caching mechanism. The literal, propertized base template would still
feature prominently as part of a template class's definition, but its
role would be reduced to serving as input for a validation scheme.

In terms of proposals, I think a successful candidate for adoption
should provide at least two working demos, preferably among the
following:

  - Bidirectional input and display
  - Headline style messages w. speaker on a separate line
  - Text substitution (language translation, encryption, etc.)
  - Display names (e.g., for bridges, perhaps reversible)

My guess is this feature won't be ready for ERC 5.6, especially without
explicit interest from others. As such, proposals (from me) may be based
atop work that includes changes not currently on HEAD, such as those
from #49860.

Thanks.

[1] The current "msgfpec" demo linked to in #68265 will be moved to a
    location reflecting this bug number.


In GNU Emacs 30.0.50 (build 1, x86_64-pc-linux-gnu, GTK+ Version
 3.24.38, cairo version 1.17.6) of 2024-01-04 built on localhost
Repository revision: 1081e975c9370999df1a288b117bfd9053050d21
Repository branch: master
Windowing system distributor 'The X.Org Foundation', version 11.0.12014000
System Description: Fedora Linux 37 (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
JSON 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 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 erc derived auth-source eieio
eieio-core password-cache json map format-spec erc-backend erc-networks
easy-mmode byte-opt bytecomp byte-compile erc-common inline cl-extra
help-mode erc-compat cl-seq cl-macs gv pcase rx subr-x cl-loaddefs
cl-lib 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 149907 11404) (symbols 48 11361 0) (strings 32 27740 4779)
 (string-bytes 1 971593) (vectors 16 17647)
 (vector-slots 8 310679 13296) (floats 8 27 25) (intervals 56 330 0)
 (buffers 976 12))





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

* bug#68861: 30.0.50; ERC 5.x: Introduce a modern message-insertion API
       [not found] <87bk90swz5.fsf@neverwas.me>
@ 2024-03-05  3:03 ` J.P.
  0 siblings, 0 replies; 2+ messages in thread
From: J.P. @ 2024-03-05  3:03 UTC (permalink / raw)
  To: 68861; +Cc: emacs-erc

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

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

> In terms of proposals, I think a successful candidate for adoption
> should provide at least two working demos, preferably among the
> following:
>
>   - Bidirectional input and display
>   - Headline style messages w. speaker on a separate line
>   - Text substitution (language translation, encryption, etc.)
>   - Display names (e.g., for bridges, perhaps reversible)
>
> My guess is this feature won't be ready for ERC 5.6, especially without
> explicit interest from others. As such, proposals (from me) may be based
> atop work that includes changes not currently on HEAD, such as those
> from #49860.

In the absence of a coherent proposal for a revamped message preparation
and insertion API, I believe it may be wise to collect counterexamples
exhibiting some of the desperate and ugly measures taken to access the
various components of a message and influence its assembly for display.
The objective here is to emphasize the considerable shortcomings
currently on offer in this regard.

To that end, I've attached an adapted version of a POC from another bug
(#49860) as a working, practical example of the problem. It attempts to
introduce speaker display names for bridge bots by way of a new module.
To be clear, this is not being proposed for inclusion in ERC 5.6 (or any
subsequent version). Rather, its purpose is in keeping with the
aforementioned goal of underscoring the deficiencies of the current
situation. Of particular note is the fourth patch and the terrible
lengths it goes to in order to access and manipulate the content portion
of chat messages before they're fused to their leading "<speaker> "
prefixes.

Folks at home: even if you don't try this patch set, please look at the
module setup code and the interfaces it consumes. And please do post
your own scenarios here. They need not come in the form of patches or
include any working code.

Thanks.


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #2: 0001-5.x-Add-predicate-for-logical-ERC-module-activation.patch --]
[-- Type: text/x-patch, Size: 2874 bytes --]

From 6adc6c808367ba34ce72e9d80be67db9c8a62349 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Sat, 6 Jan 2024 12:57:27 -0800
Subject: [PATCH 1/5] [5.x] Add predicate for logical ERC module activation

* lisp/erc/erc.el (erc--module-active-p): New function, a utility for
simplifying the loading of module dependencies.
* test/lisp/erc/erc-tests.el (erc--module-active-p):
Add test.
---
 lisp/erc/erc.el            |  9 +++++++++
 test/lisp/erc/erc-tests.el | 33 +++++++++++++++++++++++++++++++++
 2 files changed, 42 insertions(+)

diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index cce3b2508fb..e0c0d1a4828 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -2340,6 +2340,15 @@ erc--find-mode
          (fboundp mode)
          mode)))
 
+(defun erc--module-active-p (symbol &optional ensurep)
+  "Return non-nil if module SYMBOL has been loaded or will be.
+With ENSUREP, enable the module's minor mode before returning."
+  (let ((mode-var (intern-soft (concat "erc-" (symbol-name symbol) "-mode"))))
+    (and (or (and mode-var (boundp mode-var) (symbol-value mode-var))
+             (and (memq symbol erc-modules)
+                  (setq mode-var (erc--find-mode symbol))))
+         (or (not ensurep) (always (funcall mode-var +1))))))
+
 (defun erc--update-modules (modules)
   (let (local-modes)
     (dolist (module modules local-modes)
diff --git a/test/lisp/erc/erc-tests.el b/test/lisp/erc/erc-tests.el
index 085b063bdb2..36e6fcd949e 100644
--- a/test/lisp/erc/erc-tests.el
+++ b/test/lisp/erc/erc-tests.el
@@ -3287,6 +3287,39 @@ erc--find-mode
                              (string< (symbol-name a) (symbol-name b))))))
    erc-tests--modules))
 
+(ert-deftest erc--module-active-p ()
+  (should-not (erc--module-active-p 'fake))
+  (should-not (erc--module-active-p 'fake :ensurep))
+
+  (should (featurep 'erc-networks))
+
+  (let (after-load-alist calls)
+
+    (cl-letf (((symbol-function 'erc-networks-mode)
+               (lambda (&rest r) (push r calls))))
+
+      (let (erc-networks-mode erc-modules)
+        (should-not (erc--module-active-p 'networks))
+        (should-not (erc--module-active-p 'networks :ensurep))
+        (should-not calls))
+
+      (let ((erc-networks-mode t)
+            erc-modules)
+        (should (erc--module-active-p 'networks))
+        (should-not calls)
+        (should (erc--module-active-p 'networks :ensurep))
+        (should (equal calls '((1))))
+        (setq calls nil))
+
+      (let ((erc-networks-mode nil)
+            (erc-modules '(networks)))
+        (should (erc--module-active-p 'networks))
+        (should-not calls)
+
+        (should (erc--module-active-p 'networks :ensurep))
+        (should (equal calls '((1))))
+        (setq calls nil)))))
+
 (ert-deftest erc--essential-hook-ordering ()
   (erc-tests--assert-printed-in-subprocess
    '(progn
-- 
2.44.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #3: 0002-5.x-Indirectly-call-erc-fill-wrap-continued-message-.patch --]
[-- Type: text/x-patch, Size: 1805 bytes --]

From 767f776587af39a7751d9528eaf28142fcc1bf42 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Thu, 8 Feb 2024 20:28:56 -0800
Subject: [PATCH 2/5] [5.x] Indirectly call erc-fill--wrap-continued-message-p

* lisp/erc/erc-fill.el (erc-fill--wrap-continued-predicate): Add
function-valued variable for other modules to influence the behavior
of `erc-fill--wrap-continued-message-p', which was originally
introduced as part of bug#60936.
(erc-fill-wrap): Use `erc-fill--wrap-continued-predicate'.
---
 lisp/erc/erc-fill.el | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/lisp/erc/erc-fill.el b/lisp/erc/erc-fill.el
index aa12b807fbc..e4e73846fc4 100644
--- a/lisp/erc/erc-fill.el
+++ b/lisp/erc/erc-fill.el
@@ -659,6 +659,9 @@ erc-fill--wrap-insert-merged-pre
       (cdr (setq erc-fill--wrap-merge-indicator-pre
                  (cons s (erc-fill--wrap-measure (point-min) (point))))))))
 
+(defvar erc-fill--wrap-continued-predicate #'erc-fill--wrap-continued-message-p
+  "Function called with no args to detect a continued speaker.")
+
 (defun erc-fill-wrap ()
   "Use text props to mimic the effect of `erc-fill-static'.
 See `erc-fill-wrap-mode' for details."
@@ -689,7 +692,7 @@ erc-fill-wrap
                                 (prog1 (erc-fill--wrap-measure beg (point))
                                   (delete-region (1- (point)) (point))))))
                            ((and erc-fill-wrap-merge
-                                 (erc-fill--wrap-continued-message-p))
+                                 (funcall erc-fill--wrap-continued-predicate))
                             (put-text-property (point-min) (point)
                                                'display "")
                             (if erc-fill-wrap-merge-indicator
-- 
2.44.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #4: 0003-5.x-Expose-erc-response-object-as-dynamic-variable.patch --]
[-- Type: text/x-patch, Size: 1954 bytes --]

From 7e8ce6ba936cbb7739ceac569404263073e23c83 Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Mon, 4 Mar 2024 17:36:25 -0800
Subject: [PATCH 3/5] [5.x] Expose erc-response object as dynamic variable

* lisp/erc/erc-backend.el (erc--parsed-response): New variable.
(erc-call-hooks): Bind `erc--parsed-response' to the parsed
`erc-response' object for the duration of handler hook execution.
* lisp/erc/erc.el (erc-message-parsed): Alias to `erc--parsed-message'
and deprecate.
---
 lisp/erc/erc-backend.el | 5 ++++-
 lisp/erc/erc.el         | 3 ++-
 2 files changed, 6 insertions(+), 2 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 9fc8a4d29f4..00ea4e13788 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -1529,11 +1529,14 @@ erc-get-hook
   (gethash (format (if (numberp command) "%03i" "%s") command)
            erc-server-responses))
 
+(defvar erc--parsed-response nil)
+
 (defun erc-call-hooks (process message)
   "Call hooks associated with MESSAGE in PROCESS.
 
 Finds hooks by looking in the `erc-server-responses' hash table."
-  (let ((hook (or (erc-get-hook (erc-response.command message))
+  (let ((erc--parsed-response message)
+        (hook (or (erc-get-hook (erc-response.command message))
                   'erc-default-server-functions)))
     (run-hook-with-args-until-success hook process message)
     (erc-with-server-buffer
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index e0c0d1a4828..8b8787f8a55 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -142,7 +142,8 @@ erc-scripts
 ;;;###autoload                   erc-imenu erc-nicks)) ; 30
 ;;;###autoload  (custom-add-load symbol symbol))
 
-(defvar erc-message-parsed) ; only known to this file
+(define-obsolete-variable-alias 'erc-message-parsed 'erc--parsed-response
+  "30.1")
 
 (defvar erc--msg-props nil
   "Hash table containing metadata properties for current message.
-- 
2.44.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #5: 0004-POC-Replace-erc-cmem-from-nick-function-args-with-st.patch --]
[-- Type: text/x-patch, Size: 6349 bytes --]

From 8f8ecbc2aec388f35d8c76a516f6383c2afb2d1e Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Wed, 7 Feb 2024 05:03:55 -0800
Subject: [PATCH 4/5] [POC] Replace erc--cmem-from-nick-function args with
 struct

* lisp/erc/erc-backend.el (erc-server-PRIVMSG): Create
`erc--msg-parts' object as input for `erc--cmem-from-nick-function'.
Assign contents of `msg' slot to static `msg' variable after the
latter runs.
* lisp/erc/erc-button.el (erc-button--add-phantom-speaker): Update to
expect `erc--msg-parts' object.
* lisp/erc/erc-common.el (erc--nonempty-str): New helper function.
(erc--msg-parts): New struct.
* lisp/erc/erc.el (erc--cmem-from-nick-function): Replace loose
parameters with single struct.
(erc--cmem-get-existing): Update function to expect `erc--msg-parts'
object.
---
 lisp/erc/erc-backend.el |  5 +++--
 lisp/erc/erc-button.el  |  8 ++++----
 lisp/erc/erc-common.el  | 20 ++++++++++++++++++++
 lisp/erc/erc.el         | 20 +++++++++++---------
 4 files changed, 38 insertions(+), 15 deletions(-)

diff --git a/lisp/erc/erc-backend.el b/lisp/erc/erc-backend.el
index 00ea4e13788..227c2ab2aac 100644
--- a/lisp/erc/erc-backend.el
+++ b/lisp/erc/erc-backend.el
@@ -2049,10 +2049,11 @@ erc--speaker-status-prefix-wanted-p
             (defvar erc-format-nick-function)
             (defvar erc-show-speaker-membership-status)
             (defvar erc-speaker-from-channel-member-function)
-            (let ((cdata (funcall erc--cmem-from-nick-function
-                                  (erc-downcase nick) sndr parsed)))
+            (let* ((parts (erc--msg-parts-from-nuh :nuh sndr :parsed parsed))
+                   (cdata (funcall erc--cmem-from-nick-function parts)))
               (setq fnick (funcall erc-speaker-from-channel-member-function
                                    (car cdata) (cdr cdata))
+                    msg (erc--msg-parts-msg parts)
                     cmem-prefix (and (or erc--speaker-status-prefix-wanted-p
                                          erc-show-speaker-membership-status
                                          inputp)
diff --git a/lisp/erc/erc-button.el b/lisp/erc/erc-button.el
index 6b78e451b54..b5a8a211448 100644
--- a/lisp/erc/erc-button.el
+++ b/lisp/erc/erc-button.el
@@ -415,9 +415,9 @@ erc-button--get-user-from-spkr-prop
 (cl-defstruct (erc--phantom-channel-user (:include erc-channel-user)))
 (cl-defstruct (erc--phantom-server-user (:include erc-server-user)))
 
-(defun erc-button--add-phantom-speaker (downcased nuh _parsed)
-  (pcase-let* ((`(,nick ,login ,host) nuh)
-               (cmem (gethash downcased erc-button--phantom-cmems))
+(defun erc-button--add-phantom-speaker (parts)
+  (pcase-let* (((cl-struct erc--msg-parts nick login host key) parts)
+               (cmem (gethash key erc-button--phantom-cmems))
                (user (or (car cmem)
                          (make-erc--phantom-server-user
                           :nickname nick
@@ -426,7 +426,7 @@ erc-button--add-phantom-speaker
                (cuser (or (cdr cmem)
                           (make-erc--phantom-channel-user
                            :last-message-time (current-time)))))
-    (puthash downcased (cons user cuser) erc-button--phantom-cmems)
+    (puthash key (cons user cuser) erc-button--phantom-cmems)
     (cons user cuser)))
 
 (defun erc-button--get-phantom-cmem (down _word _bounds _count)
diff --git a/lisp/erc/erc-common.el b/lisp/erc/erc-common.el
index 8388efe062c..dec79ff6acd 100644
--- a/lisp/erc/erc-common.el
+++ b/lisp/erc/erc-common.el
@@ -169,6 +169,26 @@ erc--isupport-data
   (table (make-char-table 'erc--channel-mode-types) :type char-table)
   (shortargs (make-hash-table :test #'equal)))
 
+(cl-defstruct (erc--msg-parts
+               (:constructor erc--msg-parts-from-nuh
+                             (&key nuh parsed &aux
+                                   (nick (nth 0 nuh))
+                                   (msg (erc-response.contents parsed))
+                                   (login (erc--nonempty-str (nth 1 nuh)))
+                                   (host (erc--nonempty-str (nth 2 nuh)))
+                                   (key (erc-downcase nick)))))
+  (key "" :type string :documentation "Downcased nick.")
+  (msg "" :type string :documentation "Message body.")
+  (nick "" :type string)
+  (login nil :type (or null string))
+  (host nil :type (or null string))
+  (parsed (make-erc-response) :type erc-response))
+
+(define-inline erc--nonempty-str (string)
+  "Evaluate and return nonempty STRING if it's well and truly thus."
+  (inline-letevals (string)
+    (inline-quote (and ,string (not (string-empty-p ,string)) ,string))))
+
 ;; After dropping 28, we can use prefixed "erc-autoload" cookies.
 (defun erc--normalize-module-symbol (symbol)
   "Return preferred SYMBOL for `erc--module'."
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 8b8787f8a55..5aa9343fa20 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -5954,15 +5954,17 @@ erc--get-speaker-bounds
     (cons beg (next-single-property-change beg 'erc--speaker))))
 
 (defvar erc--cmem-from-nick-function #'erc--cmem-get-existing
-  "Function maybe returning a \"channel member\" cons from a nick.
-Must return either nil or a cons of an `erc-server-user' and an
-`erc-channel-user' (see `erc-channel-users') for use in
-formatting a user's nick prior to insertion.  Called in the
-appropriate target buffer with the downcased nick, the parsed
-NUH, and the current `erc-response' object.")
-
-(defun erc--cmem-get-existing (downcased _nuh _parsed)
-  (and erc-channel-users (gethash downcased erc-channel-users)))
+  "Function returning a \"channel member\" cons cell.
+Called with an `erc--msg-parts' object derived from sender info.  Must
+return either nil or a \"cmem\" cons cell consisting of an
+`erc-server-user' and an `erc-channel-user' (see `erc-channel-members')
+for use in formatting a nick for insertion.  Runs after the sender's
+`erc-channel-members' entry has been created or updated.")
+
+(defun erc--cmem-get-existing (parts)
+  "Return `erc-channel-members' entry from `erc--msg-parts' PARTS."
+  (and erc-channel-members
+       (gethash (erc--msg-parts-key parts) erc-channel-members)))
 
 (defun erc-format-privmessage (nick msg privp msgp)
   "Format a PRIVMSG in an insertable fashion."
-- 
2.44.0


[-- Warning: decoded text below may be mangled, UTF-8 assumed --]
[-- Attachment #6: 0005-POC-Add-general-display-name-support-to-ERC.patch --]
[-- Type: text/x-patch, Size: 49716 bytes --]

From 027b6ef23178832ffd9e29233d0ac5161b7b953c Mon Sep 17 00:00:00 2001
From: "F. Jason Park" <jp@neverwas.me>
Date: Sun, 17 Jul 2022 02:21:42 -0700
Subject: [PATCH 5/5] [POC] Add general display-name support to ERC

* lisp/erc/erc-masquerade.el: New file.
* test/lisp/erc/erc-masquerade-tests.el: New file.
* test/lisp/erc/erc-scenarios-masquerade.el: New file.
* test/lisp/erc/resources/masquerade/batch.eld: New file.
---
 lisp/erc/erc-masquerade.el                   | 444 +++++++++++++++++++
 test/lisp/erc/erc-masquerade-tests.el        | 178 ++++++++
 test/lisp/erc/erc-scenarios-masquerade.el    | 164 +++++++
 test/lisp/erc/resources/masquerade/batch.eld |  86 ++++
 4 files changed, 872 insertions(+)
 create mode 100644 lisp/erc/erc-masquerade.el
 create mode 100644 test/lisp/erc/erc-masquerade-tests.el
 create mode 100644 test/lisp/erc/erc-scenarios-masquerade.el
 create mode 100644 test/lisp/erc/resources/masquerade/batch.eld

diff --git a/lisp/erc/erc-masquerade.el b/lisp/erc/erc-masquerade.el
new file mode 100644
index 00000000000..ec0d95f5922
--- /dev/null
+++ b/lisp/erc/erc-masquerade.el
@@ -0,0 +1,444 @@
+;;; erc-masquerade.el --- display-names, relaymsg, etc. -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2022-2024 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published
+;; by the Free Software Foundation, either version 3 of the License,
+;; or (at your option) any later version.
+
+;; GNU Emacs is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; *Update 2024*: This is currently a placeholder for a demo module
+;; whose purpose will be to extol the virtues of whatever next-gen API
+;; emerges from bug#68861.  For a more practical, minimal "bridge bot"
+;; module actually compatible with the current ERC release, see
+;; <https://git.sr.ht/~technomancy/erc-bridge-nicks>.
+;;
+;; Some channels are "bridged" to communities on other protocols, like
+;; Matrix and XMPP or commercial chat platforms.  Sometimes,
+;; individual remote presences aren't "puppeted" on the local IRC
+;; side.  Instead, a so-called "relay bot" preforms a degraded type of
+;; forwarding by sending messages prefixed with the names of speakers.
+;; This results in a frustrating user experience:
+;;
+;;   <relaybot> <alice@telegram> telegram is bad
+;;   <relaybot> <bob@matrix.org> matrix is slwo
+;;   <relaybot> <bob@matrix.org> matrix is slow
+;;
+;; In this example, only relaybot's nick is ever highlighted or
+;; offered as a completion suggestion, and bob's last message is isn't
+;; marked as being an edit.  This module addresses the first problem
+;; to varying degrees but does so at the expense of making it
+;; immediately obvious to users when they're dealing with a bot.
+;;
+;; To get started, set `erc-masquerade-bots' to something like
+;;
+;;   ((Libera.Chat ("#chan" "MyBot" "\\`<\\(.+\\)> " "")
+;;                 ("#spam" "UrBot" "\\`<\\(.+\\):\\(.+\\)> " "\\2")))
+;;
+;; And add `masquerade' to `erc-modules' before connecting.
+
+;;; Background:
+
+;; Right now (2022), there are two draft specs from IRCv3 that touch
+;; on various c2s issues involving relay bots and display-only names:
+;;
+;;   https://github.com/ircv3/ircv3-specifications/pull/417
+;;   https://github.com/ircv3/ircv3-specifications/pull/452
+;;
+;; Word is that neither is poised to take off.  If that remains true,
+;; something addressing similar problems will likely emerge.  In the
+;; meantime, we can still try to improve the experience by minimizing
+;; the distraction of untreated relay-bot messages while still clearly
+;; indicating when spoofing is taking place.  To that end, this file
+;; provides a module called `masquerade' that fake-implements
+;; `display-name' for bots declared a priori.  This is preferable to
+;; rewriting relayed messages as being native.
+
+;;; Code:
+(require 'erc-button)
+(require 'erc-goodies)
+
+(defgroup erc-masquerade nil
+  "Display-names for \"bridged\" users from other platforms."
+  :group 'erc
+  :package-version '(ERC . "5.7")) ; FIXME sync on release
+
+(define-obsolete-variable-alias 'erc-masquerade-relay-bots
+  'erc-masquerade-bots "30.1")
+(defcustom erc-masquerade-bots nil
+  "Network nicks to treat as relay bots.
+More precisely, an alist of (NETWORK CHANSPEC1 CHANSPEC2 ...) where
+NETWORK is a symbol and CHANSPEC is a list containing (CHAN BOTNICK
+SPEAKER REPLACER).  Here, the first three items in a CHANSPEC are regexp
+patterns.  When matching, ERC surrounds the first two with
+`string-start' and `string-end' zero-width assertions (\\\\=` and \\\\='
+respectively).  ERC uses the entire matched substring, sans surrounding
+whitespace and angle brackets as a unique identifier for the virtual
+user, which may be relevant when bridging communities from many chat
+platforms.  The first matched group becomes the display name.
+Additional groups may exists and be referenced by REPLACER, which
+specifies text to substitute for the entire matched portion.  It can
+include specifiers known to `replace-match'.
+
+REPLACER can also be a function that accepts two arguments: the
+message body and the above mentioned SPEAKER regexp, which they may
+choose to ignore.  It must either return nil, to signal that the message
+should instead be processed normally, or a list of these items:
+
+  1. a canonical key, ideally unique among virtual nicks in the channel
+  2. a display name to be shown as the speaker in place of the robot's
+     nick; typically identical to 2 or a truncated version
+  3. a replacement for the matched portion of the message
+  4. a replacement for the remainder of the message body
+  5. an optional face for colorizing the speaker when `nicks' is enabled
+
+This latter function form mainly exists for ERC itself to provide ready
+made handlers for well known bots of a specific make, model, and
+configuration.
+
+If you tinker with this option during a live IRC session, cycle the
+module's minor mode for the changes to take effect."
+  :type (let ((pats `(choice :tag "Speaker display name pattern"
+                             (regexp :tag "Full: <{Name}> {Msg}"
+                                     ,(rx "<" (group (+? nonl)) "> "))
+                             (regexp :tag "Partitioned: <{Name:Host}> {Msg}"
+                                     ,(rx "<" (group (+? nonl)) ":"
+                                          (group (+? nonl)) "> "))
+                             (regexp :tag "Speaker pattern")))
+              (reps '(choice :tag "Replacer"
+                             (string :tag "Partitioned" "\\2 ")
+                             (string :tag "User-provided string")
+                             (function-item
+                              :tag "Mattermost bot with platform prefix"
+                              erc-masquerade-parse-fallback-mattermost)
+                             (function :tag "User-provided function"))))
+          `(alist :key-type (symbol :tag "Network")
+                  :value-type (repeat (list (regexp :tag "Channel pattern")
+                                            (regexp :tag "Bot nick pattern")
+                                            ,pats ,reps)))))
+
+(defcustom erc-masquerade-prefer-mirc-colors-for-nicks t
+  "Whether to prefer faces derived from MIRC control-sequences.
+This only matters when the `nicks' module is enabled.  Some bots enclose
+the entire fallback prefix in control chars, such as
+\"\\C-c02<{DisplayName}> \\C-o{MessageContent}\".  When enabled, this
+options tells ERC to retain the interpreted face rather than let `nicks'
+choose one.  See also `erc-controls-interpret'."
+  :type 'boolean)
+
+(defface erc-masquerade-alt-prefix-face
+  '((t :inherit (erc-notice-face italic)))
+  "Face for text that replaces the \"fallback\" prefix.
+For example, if an original message \"<bot> <nick@host> msg\" becomes
+\"<nick> host msg\", then use this face for the \"host \" portion."
+  :group 'erc-faces)
+
+(defvar-local erc-masquerade--match-replacer nil)
+(defvar-local erc-masquerade--botnick-regexp nil)
+(defvar-local erc-masquerade--speaker-regexp nil)
+;; FIXME rename to `erc-masquerade--frag-to-nonirc'
+(defvar-local erc-masquerade--partial-to-nonstandard nil)
+
+(defvar-local erc-masquerade--display-names nil
+  "Hash table mapping visible displayed name to full, given display name.
+For example, if the original display name from the bot's message
+was \"<joe:example.com>\", and we see the speaker as \"<joe>\",
+then this has an item mapping \"joe\" to \"joe:example.com\".")
+
+(defvar erc-masquerade--canonicalize-prefix #'erc-masquerade--strip-chars)
+(defvar erc-masquerade--detect-edit-function nil)
+(defvar erc-masquerade--expanded-syntax-table nil)
+(defvar erc-masquerade--extra-word-chars ".")
+
+(define-erc-module masquerade nil
+  "Replace speakers with \"display names\".
+
+This is a local module whose minor mode is non-nil in target buffers.
+In-session changes to `erc-masquerade-bots' will not be detected.  If
+reconnecting is impossible, try cycling the minor mode off and then on
+again in the appropriate buffer."
+  ((cond
+    ((and erc--target (erc-masquerade--determine-channel-info))
+     (add-hook 'erc-insert-pre-hook 'erc-masquerade--override-spkr-prop -50 t)
+     (add-function :around (local 'erc--cmem-from-nick-function)
+                   #'erc-masquerade--adorn '((depth . 30)))
+     (when (erc--module-active-p 'fill)
+       (add-function :after-while (local 'erc-fill--wrap-continued-predicate)
+                     #'erc-masquerade--fill-wrap-on-continued-message-p
+                     '((depth . -50))))
+     (when (erc--module-active-p 'nicks)
+       (add-function :filter-args (local 'erc-button--modify-nick-function)
+                     #'erc-masquerade--massage-nick '((depth . -80))))
+     (setq
+      erc-masquerade--display-names (make-hash-table :test #'equal)
+      erc-masquerade--partial-to-nonstandard (make-hash-table :test #'equal)))
+    (t (erc-masquerade-mode -1))))
+  ((remove-hook 'erc-insert-pre-hook 'erc-masquerade--override-spkr-prop t)
+   (remove-function (local 'erc-fill--wrap-continued-predicate)
+                    #'erc-masquerade--fill-wrap-on-continued-message-p)
+   (remove-function (local 'erc--cmem-from-nick-function)
+                    #'erc-masquerade--adorn)
+   (remove-function (local 'erc-button--modify-nick-function)
+                    #'erc-masquerade--massage-nick)
+   (kill-local-variable 'erc-masquerade--botnick-regexp)
+   (kill-local-variable 'erc-masquerade--speaker-regexp)
+   (kill-local-variable 'erc-masquerade--match-replacer)
+   (kill-local-variable 'erc-masquerade--partial-to-nonstandard)
+   (kill-local-variable 'erc-masquerade--display-names))
+  'local)
+
+(defun erc-masquerade--strip-chars (text)
+  "Helper for relay-bot functions to clean puppet names."
+  (string-replace "\u200b" ""
+                  (if erc-masquerade-prefer-mirc-colors-for-nicks
+                      (let ((erc-interpret-mirc-color t))
+                        (erc-controls-interpret text))
+                    (erc-controls-strip text))))
+
+(defun erc-masquerade--override-spkr-prop (string)
+  "Replace real `erc--spkr' with bridged user.
+Also swap out placeholder face property in prefix portion of STRING."
+  (when-let ((beg (text-property-not-all
+                   0 (length string) 'erc-masquerade--pfx-face nil string))
+             (end (next-single-property-change
+                   beg 'erc-masquerade--pfx-face string))
+             (val (get-text-property beg 'erc-masquerade--pfx-face string)))
+    (remove-list-of-text-properties beg end
+                                    '(erc-masquerade--pfx-face string) string)
+    (put-text-property beg end 'font-lock-face val string))
+  (when-let ((bridged (erc--check-msg-prop 'erc--masquerade-key)))
+    (puthash 'erc--spkr bridged erc--msg-props)
+    (remhash 'erc--masquerade-key erc--msg-props)))
+
+(define-obsolete-function-alias 'erc-masquerade-parse-fallback-host
+  'erc-masquerade--preform-legacy-fallback "30.1")
+(defun erc-masquerade--preform-legacy-fallback (msg &rest _)
+  (erc-masquerade--match-nick-default
+   msg (rx "<" (group (+? nonl)) ":" (group (+? nonl)) "> ") "\\2 "))
+
+(defun erc-masquerade--canonicalize-prefix-mattermost (fallback)
+  (let (user platform)
+    (if-let ((stripped (erc-masquerade--strip-chars fallback))
+             (proto (string-search "] " stripped)))
+        (setq user (substring stripped (+ proto 3) -2)
+              platform (substring stripped 1 proto))
+      (setq user (substring stripped 1 -2)))
+    (when (and platform (string-suffix-p "-" platform))
+      (setq platform (substring platform 0 -1)))
+    (concat user (and platform ":") platform)))
+
+(defun erc-masquerade-parse-fallback-mattermost (msg _)
+  "Example prefix parser for Mattermost bot (as it behaved in 2022).
+Process MSG, ignoring the `erc-masquerade-bot' option's REGEXP.  Only
+handle bridges configured with `RemoteNickFormat' set to either
+\"[{PROTOCOL}] <{NICK}> \" or \"<{NICK}> \".  Otherwise behave like
+`erc-masquerade--match-nick-default' with the replacer parameter set to
+sub-expression 2."
+  (let ((erc-masquerade--canonicalize-prefix
+         #'erc-masquerade--canonicalize-prefix-mattermost))
+    (erc-masquerade--preform-legacy-fallback msg)))
+
+(defun erc-masquerade--match-nick-default (msg regexp subst)
+  "Return MSG parts partitioned via REGEXP after replacing with SUBST."
+  (when-let (((string-match regexp msg))
+             (msg (funcall erc-masquerade--canonicalize-prefix msg))
+             ((string-match regexp msg))
+             (display (substring-no-properties (match-string 1 msg)))
+             (trimmed (substring-no-properties msg (match-end 0)))
+             (prefix (substring msg 0 (match-end 0)))
+             (replaced (substring-no-properties
+                        (replace-match subst nil nil prefix)))
+             (canon (substring-no-properties
+                     (string-trim prefix "[ <]+" "[ >]+"))))
+    (let ((face (and-let* ((erc-masquerade-prefer-mirc-colors-for-nicks)
+                           (val (get-text-property 0 'font-lock-face prefix))
+                           (face (car-safe val))
+                           ((symbolp face))
+                           ((string-prefix-p "fg:erc-" (symbol-name face))))
+                  face)))
+      (list canon display replaced trimmed face))))
+
+(defun erc-masquerade--determine-channel-info ()
+  "Set local variables for function and bot-nick, returning the latter.
+If not found, return nil.  For compatibility, allow the function
+to be nil."
+  (when-let ((entries (alist-get (erc-network) erc-masquerade-bots))
+             (entry (alist-get (erc-default-target) entries nil nil
+                               (lambda (pattern target)
+                                 (string-match (concat "\\`" pattern "\\'")
+                                               target)))))
+    (when (functionp (nth 1 entry))
+      (setq entry (list (car entry) (rx unmatchable) (nth 1 entry)))
+      (erc-button--display-error-notice-with-keys
+       (erc-server-buffer)
+       "The shape of `erc-masquerade-bots's expected value has changed."
+       " Please take corrective action."))
+    (pcase-exhaustive entry
+      (`(,botpat ,spkrpat ,replacement)
+       (when (and erc-masquerade-prefer-mirc-colors-for-nicks
+                  (string-suffix-p " " spkrpat))
+         (setq spkrpat (concat spkrpat (rx (? ?\C-o)))))
+       (setq erc-masquerade--match-replacer replacement
+             erc-masquerade--botnick-regexp (concat "\\`" botpat "\\'")
+             erc-masquerade--speaker-regexp spkrpat)))))
+
+(defun erc-masquerade--add-speaker-to-phantom-users (downcased name)
+  "Add speaker to `erc-button--phantom-users' and return bot user.
+Also map speaker to bot data in `erc-button--nicks-to-data'."
+  (cl-assert erc-button-mode)
+  (cl-assert erc-button--phantom-users-mode)
+  (or (gethash downcased erc-button--phantom-cmems)
+      (and-let* ((bot-nick (erc-extract-nick
+                            (erc-response.sender erc--parsed-response)))
+                 (bot-down (erc-downcase bot-nick))
+                 (cmem (cons
+                        (make-erc--phantom-server-user :nickname name)
+                        (make-erc--phantom-channel-user :last-message-time
+                                                        (current-time)))))
+        ;; Handle names containing nonstandard chars.
+        (when-let ((found (string-match
+                           (concat "[" erc-masquerade--extra-word-chars "]")
+                           name))
+                   (partial (substring name 0 found)))
+          (puthash (erc-downcase partial) cmem erc-button--phantom-cmems)
+          (puthash partial name erc-masquerade--partial-to-nonstandard))
+        (puthash downcased cmem erc-button--phantom-cmems))))
+
+(defun erc-masquerade--adorn (orig parts)
+  "Stylize message to impersonate real channel members.
+Defer to `erc-masquerade--detect-edit-function' to handle edit
+detection and styling."
+  (if-let ((botnick-downcased (erc--msg-parts-key parts))
+           ((string-match erc-masquerade--botnick-regexp botnick-downcased))
+           (msg (erc--msg-parts-msg parts))
+           (pat erc-masquerade--speaker-regexp)
+           (rep erc-masquerade--match-replacer)
+           (result (if (functionp rep)
+                       (funcall rep msg pat)
+                     (erc-masquerade--match-nick-default msg pat rep))))
+      (pcase-let* ((`(,canon ,display ,prefix ,emsg ,face) result)
+                   ((cl-struct erc--msg-parts login host) parts)
+                   ;; File phony cmem under key "<downcased> <display-name>".
+                   (key (concat botnick-downcased " " canon))
+                   (cmem (or (gethash key erc-channel-members)
+                             (cons (make-erc--phantom-server-user
+                                    :nickname display :host host :login login)
+                                   (make-erc--phantom-channel-user)))))
+        (unless emsg (setq emsg (erc--msg-parts-msg parts)))
+        (setf (erc--msg-parts-key parts) key
+              ;;
+              (erc--msg-parts-msg parts)
+              (concat (and prefix (not (string-empty-p prefix))
+                           (propertize prefix
+                                       'erc-masquerade--pfx-face
+                                       'erc-masquerade-alt-prefix-face))
+                      (or (and erc-masquerade--detect-edit-function
+                               (funcall erc-masquerade--detect-edit-function
+                                        canon emsg))
+                          emsg)))
+        (when erc-button--phantom-users-mode
+          (erc-masquerade--add-speaker-to-phantom-users (erc-downcase display)
+                                                        display))
+        (puthash display canon erc-masquerade--display-names)
+        (puthash key cmem erc-channel-members)
+        (when face
+          (push (cons 'erc--masquerade-face face) erc--msg-prop-overrides))
+        (push (cons 'erc--masquerade-key key) erc--msg-prop-overrides)
+        (push (cons 'erc--masquerade canon) erc--msg-prop-overrides)
+        cmem)
+    (funcall orig parts)))
+
+
+;;;; `fill-wrap' integration
+
+(defun erc-masquerade--fill-wrap-on-continued-message-p ()
+  (when-let ((beg (text-property-any (point-min) (point-max) 'font-lock-face
+                                     'erc-masquerade-alt-prefix-face))
+             (end (next-single-property-change beg 'face)))
+    (goto-char end))
+  t)
+
+;;;; `nicks' integration
+
+(defun erc-masquerade--bounds-of-word-at-point ()
+  "Return more liberal bounds than allowed by `erc-button-syntax-table'."
+  ;; FIXME move this into a public setter to allow user-defined bot
+  ;; functions to alter the table.
+  (unless erc-masquerade--expanded-syntax-table
+    (let ((table (setq erc-masquerade--expanded-syntax-table
+                       (copy-syntax-table erc-button-syntax-table))))
+      (erc--doarray (c erc-masquerade--extra-word-chars)
+        (modify-syntax-entry c "w" table))))
+  (with-syntax-table erc-masquerade--expanded-syntax-table
+    (erc-bounds-of-word-at-point)))
+
+(defun erc-masquerade--resolve-partial (name nick-object)
+  "Rewrite bounds of NICK-OBJECT if NAME resides in expanded word at point."
+  (when-let ((expanded (erc-masquerade--bounds-of-word-at-point))
+             ((save-excursion (goto-char (car expanded))
+                              (search-forward name (cdr expanded) 'noerror))))
+    (setf (erc-button--nick-bounds nick-object)
+          (cons (match-beginning 0) (match-end 0)))
+    (cl-assert (equal name (match-string 0)))
+    (let ((found (gethash name erc-masquerade--display-names)))
+      (cl-assert found t)
+      found)))
+
+;; FIXME don't highlight over `erc-masquerade-alt-prefix-face'.
+(defun erc-masquerade--massage-nick (args)
+  (cl-assert (erc-button--nick-user (car args)))
+  (when-let ((nick-object (car args))
+             (erc--target)
+             (server-user (erc-button--nick-user nick-object))
+             (bounds (erc-button--nick-bounds nick-object)))
+    ;; Set the button-nick object's `downcased' slot to the full
+    ;; display name, so `erc-nicks--highlight' uses it for its "key".
+    ;; Known speakers, like <bob>, have this in a `erc--masquerade'
+    ;; msg prop.  Otherwise, look in `erc-masquerade--display-names'.
+    (let ((masq (erc--check-msg-prop 'erc--masquerade))
+          (alt-face (erc--check-msg-prop 'erc--masquerade-face)))
+      (when-let
+          (((not (and masq (get-text-property (car bounds) 'erc--speaker))))
+           (str (buffer-substring-no-properties (car bounds) (cdr bounds)))
+           (tab erc-masquerade--partial-to-nonstandard)
+           (seen (or (gethash str erc-masquerade--display-names)
+                     (and-let* ((name (gethash str tab))
+                                (alt (erc-masquerade--resolve-partial
+                                      name nick-object)))
+                       (setq str name)
+                       alt))))
+        (setq masq seen))
+      (when masq
+        (setf
+         (erc-button--nick-downcased nick-object) (erc-downcase masq)
+         (erc-button--nick-user nick-object) (copy-erc-server-user server-user)
+         (erc-server-user-nickname (erc-button--nick-user nick-object)) masq)
+        ;; Replace natural color with assigned `irccontrols' face.
+        ;; FIXME provide internal API to accomplish this painlessly.
+        (when alt-face
+          (defvar erc-nicks-colors)
+          (defvar erc-nicks--colors-pool)
+          (defvar erc-nicks--colors-len)
+          (declare-function erc-nicks--get-face "erc-nicks" (nick key))
+          (erc-with-server-buffer
+            (let ((erc-nicks-colors)
+                  (erc-nicks--colors-pool (list (face-foreground alt-face)))
+                  (erc-nicks--colors-len 1))
+              (erc-nicks--get-face (erc-downcase masq) "key")))))))
+  args)
+
+(provide 'erc-masquerade)
+
+;;; erc-masquerade.el ends here
diff --git a/test/lisp/erc/erc-masquerade-tests.el b/test/lisp/erc/erc-masquerade-tests.el
new file mode 100644
index 00000000000..a2bf2b79728
--- /dev/null
+++ b/test/lisp/erc/erc-masquerade-tests.el
@@ -0,0 +1,178 @@
+;;; erc-masquerade-tests.el --- Tests for erc-masquerade.  -*- lexical-binding:t -*-
+
+;; Copyright (C) 2020-2022 Free Software Foundation, Inc.
+
+;; This file is part of GNU Emacs.
+;;
+;; GNU Emacs is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published
+;; by the Free Software Foundation, either version 3 of the License,
+;; or (at your option) any later version.
+;;
+;; GNU Emacs is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with GNU Emacs.  If not, see <https://www.gnu.org/licenses/>.
+
+;;; Code:
+(require 'ert-x)
+(require 'erc-masquerade)
+
+
+(ert-deftest erc-masquerade--canonicalize-prefix-mattermost ()
+  (should (string= (erc-masquerade--canonicalize-prefix-mattermost
+                    "<\C-]b\u200bob\C-]> ")
+                   "bob"))
+  (should (string= (erc-masquerade--canonicalize-prefix-mattermost
+                    "<b\u200bob> ")
+                   "bob"))
+  (should (string= (erc-masquerade--canonicalize-prefix-mattermost
+                    "[t\u200belegram] <b\u200bob> ")
+                   "bob:telegram"))
+  (should (string= (erc-masquerade--canonicalize-prefix-mattermost
+                    "[telegram-] <bob> ")
+                   "bob:telegram")))
+
+(ert-deftest erc-masquerade--match-nick-default ()
+  ;; Baseline.
+  (should (equal (erc-masquerade--match-nick-default
+                  "<ABC> 123"
+                  (rx bot "<" (group (+ nonl)) "> ")
+                  "")
+                 '("ABC" "ABC" "" "123" nil)))
+
+  ;; Replacement.
+  (should (equal (erc-masquerade--match-nick-default
+                  "<ABC:gnu.org> 123"
+                  (rx bot "<" (group (+ nonl)) ":" (group (+ nonl)) "> ")
+                  "\\2 ")
+                 '("ABC:gnu.org" "ABC" "gnu.org " "123" nil))))
+
+(defun ert-masquerade-tests--perform-in-chan (setup test)
+  (let* ((pat (rx bot "<" (group (+ nonl)) "> "))
+         (erc-masquerade-bots `((foonet ("#tes." "spammer_?" ,pat "")
+                                        ("#chan" "frisbeeosbridge" ,pat ""))))
+         erc-kill-channel-hook erc-kill-server-hook erc-kill-buffer-hook)
+
+    (with-current-buffer (get-buffer-create "foonet")
+      (erc-mode)
+      (setq erc-server-process (start-process "true"
+                                              (current-buffer) "sleep" "1")
+            erc-server-current-nick "tester"
+            erc--isupport-params (make-hash-table)
+            erc-server-announced-name "foo.gnu.chat"
+            erc-network 'foonet
+            erc-networks--id (erc-networks--id-create nil)
+            erc-server-users (make-hash-table :test #'equal))
+      (puthash 'CHANTYPES '("&#") erc--isupport-params)
+      (set-process-query-on-exit-flag erc-server-process nil))
+
+    (dolist (name '("#test" "#chan"))
+      (with-current-buffer (get-buffer-create name)
+        (erc-mode)
+        (erc--initialize-markers (point) nil)
+        (setq erc-server-process (buffer-local-value 'erc-server-process
+                                                     (get-buffer "foonet"))
+              erc-default-recipients (list name)
+              erc--target (erc--target-from-string name)
+              erc-network 'foonet
+              erc-networks--id (buffer-local-value 'erc-networks--id
+                                                   (get-buffer "foonet"))
+              erc-channel-users (make-hash-table :test #'equal))
+        (erc-fill-mode +1)
+        (funcall setup)
+        (erc-masquerade-mode +1)))
+
+    (funcall test)
+
+    (when noninteractive
+      (kill-buffer "foonet")
+      (kill-buffer "#test")
+      (kill-buffer "#chan"))))
+
+(ert-deftest erc-masquerade--adorn ()
+  (ert-masquerade-tests--perform-in-chan
+   #'ignore
+   (lambda ()
+
+     (with-current-buffer "foonet"
+       (erc-parse-server-response
+        erc-server-process
+        (concat "@time=2022-07-18T09:19:34.604Z "
+                ":spammer_!~spammer_@matrix.spammer.org "
+                "PRIVMSG #test :<Bob:discord> "
+                "Blah..."))
+       (erc-parse-server-response
+        erc-server-process
+        (concat "@time=2022-07-18T09:19:34.604Z "
+                ":frisbeeosbridge!~frisbeeo@matrix.frisbeeos.org "
+                "PRIVMSG #chan :<Alice:telegram> "
+                "Receiving clients SHOULD check for the presence of...")))
+
+     (with-current-buffer "#test"
+       (goto-char (point-min))
+       (should (string= erc-masquerade--botnick-regexp "\\`spammer_?\\'"))
+       (should (search-forward "<Bob:discord> " nil t))
+       (should (string= (get-text-property (pos-bol) 'erc--masquerade)
+                        "Bob:discord"))
+       (should (looking-at-p "Blah")))
+
+     (with-current-buffer "#chan"
+       (goto-char (point-min))
+       (should (search-forward "<Alice:telegram> " nil t))
+       (should (string= (get-text-property (pos-bol) 'erc--masquerade)
+                        "Alice:telegram"))
+       (should (looking-at-p "Receiving"))))))
+
+(defvar erc-fill-function)
+(defvar erc-fill-static-center)
+(declare-function erc-hl-nicks-mode "ext:erc-hl-nicks" (&optional arg))
+(declare-function erc-fill-static "erc-fill" nil)
+
+(ert-deftest erc-masquerade--adorn/fill-static ()
+  (ert-masquerade-tests--perform-in-chan
+
+   (lambda ()
+     (when (string= (buffer-name) "#chan")
+       (setq-local erc-fill-function #'erc-fill-static)
+       (setq-local erc-fill-static-center 22)))
+
+   (lambda ()
+     (with-current-buffer "foonet"
+       (erc-parse-server-response
+        erc-server-process
+        (concat "@time=2022-07-18T09:19:34.604Z "
+                ":frisbeeosbridge!~frisbeeo@matrix.frisbeeos.org "
+                "PRIVMSG #chan :<Alice:telegram> "
+                "Receiving clients SHOULD check for the presence of..."))
+       (erc-parse-server-response
+        erc-server-process
+        (concat "@time=2022-07-18T09:19:35.604Z "
+                ":frisbeeosbridge!~frisbeeo@matrix.frisbeeos.org "
+                "PRIVMSG #chan :<BillyBob:matrix.org> "
+                "Consider formatting display names differently to nicknames")))
+
+     (with-current-buffer "#chan"
+       (goto-char (point-min))
+       (should (search-forward "<Alice:telegram> " nil t))
+       (goto-char (match-beginning 0))
+       (should (string= (get-text-property (pos-bol) 'erc--masquerade)
+                        "Alice:telegram"))
+       (should (= (- (point) (line-beginning-position)) 5))
+       (should
+        (string-prefix-p
+         "     <Alice:telegram> Receiving clients SHOULD"
+         (buffer-substring-no-properties (line-beginning-position)
+                                         (line-end-position))))
+       (goto-char (line-end-position))
+
+       (should (search-forward "<BillyBob:matrix.org> " nil t))
+       (goto-char (match-beginning 0))
+       (should (string= (get-text-property (pos-bol) 'erc--masquerade)
+                        "BillyBob:matrix.org"))
+       (should (= (point) (line-beginning-position)))))))
+
+;;; erc-masquerade-tests.el ends here
diff --git a/test/lisp/erc/erc-scenarios-masquerade.el b/test/lisp/erc/erc-scenarios-masquerade.el
new file mode 100644
index 00000000000..238e983b944
--- /dev/null
+++ b/test/lisp/erc/erc-scenarios-masquerade.el
@@ -0,0 +1,164 @@
+;;; erc-scenarios-masquerade.el --- masquerade scenarios -*- lexical-binding: t -*-
+
+;; Copyright (C) 2022 Free Software Foundation, Inc.
+;;
+;; This file is part of GNU Emacs.
+;;
+;; This program is free software: you can redistribute it and/or
+;; modify it under the terms of the GNU General Public License as
+;; published by the Free Software Foundation, either version 3 of the
+;; License, or (at your option) any later version.
+;;
+;; This program is distributed in the hope that it will be useful, but
+;; WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
+;; General Public License for more details.
+;;
+;; You should have received a copy of the GNU General Public License
+;; along with this program.  If not, see
+;; <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;;; Code:
+(require 'ert-x)
+(eval-and-compile
+  (let ((load-path (cons (ert-resource-directory) load-path)))
+    (require 'erc-scenarios-common)))
+
+(require 'erc-scenarios-common)
+(require 'erc-masquerade)
+
+(defun erc-scenarios-common--tps (timeout text)
+  (save-restriction
+    (widen)
+    (let ((from (point)))
+      (erc-d-t-wait-for timeout (format "string: %s" text)
+        (goto-char from)
+        (text-property-search-forward 'display text
+                                      (lambda (v p)
+                                        (string-prefix-p v p)))))))
+
+(defun erc-scenarios-masqerade--assert-fill-button-ordering ()
+  ;; Ensure that our custom fill function can see all face
+  ;; modifications; otherwise its calculations will be off.
+  (ert-info ("Button follows fill in modification hooks")
+    (should (thread-last 'erc-insert-modify-hook
+                         (default-value)
+                         (memq 'erc-button-add-buttons)
+                         (memq 'erc-fill)))))
+
+(defun erc-scenarios-masquerade--batch (after-foo)
+  (erc-scenarios-common-with-cleanup
+      ((erc-scenarios-common-dialog "masquerade")
+       (erc-d-linger-secs 0.5)
+       (erc-server-flood-penalty 0.1)
+       (erc-d-tmpl-vars
+        `((now . ,(lambda () (format-time-string "%FT%T.%3NZ" nil t)))))
+       (dumb-server (erc-d-run "localhost" t 'batch))
+       (port (process-contact dumb-server :service))
+       (erc-modules `(nicks masquerade ,@erc-modules))
+       (expect (erc-d-t-make-expecter)))
+
+    (ert-info ("Connect")
+      (with-current-buffer (erc :server "127.0.0.1"
+                                :port port
+                                :nick "tester"
+                                :password "password123"
+                                :full-name "tester")
+        (should (string= (buffer-name) (format "127.0.0.1:%d" port)))))
+    (erc-d-t-wait-for 10 "server buffer ready" (get-buffer "ExampleOrg"))
+
+    (erc-scenarios-masqerade--assert-fill-button-ordering)
+
+    (ert-info ("Notices received")
+      (with-current-buffer "ExampleOrg"
+        (funcall expect 10 "Setting your virtual host")))
+
+    (erc-d-t-wait-for 10 "buffer #foo ready" (get-buffer "#foo"))
+
+    (ert-info ("Chan buffer #foo populated")
+      (with-current-buffer "#foo"
+        (funcall expect 10 "You have joined channel #foo")
+        (funcall expect 5 "<focused-ne")
+        (funcall expect 20 "#frllklw-klwjlsrs")
+        (funcall expect 5 "#foo modes:")
+        (funcall expect 5 "#foo was")
+        (funcall expect 5 "<gallant")
+        (funcall expect 5 "According")
+        (funcall expect 5 "<focused-ne")
+        (funcall expect 5 "Or never")
+        (funcall expect 5 erc-prompt)
+        (funcall after-foo)))))
+
+(defvar erc-fill-function)
+(declare-function erc-fill-static "erc-fill" nil)
+(declare-function erc-fill-wrap "erc-fill" nil)
+
+(defun erc-scenarios-masquerade--assert-completion ()
+  (ert-info ("Completion at point works")
+    (save-excursion
+      (goto-char erc-input-marker)
+      (insert "gall")
+      (call-interactively #'erc-tab)
+      (should (looking-back "gallant: "))
+      (goto-char erc-input-marker)
+      (delete-region erc-input-marker (point-max)))))
+
+;; This asserts baseline behavior without fallback substitutions.
+(ert-deftest erc-scenarios-masquerade-batch--normal ()
+  :tags '(:expensive-test)
+  (let ((erc-masquerade-bots '((ExampleOrg
+                                ("#test" "spammer" "" "")
+                                ("#foo" "botman" "<\\(.+?\\)> " ""))))
+        ;; This style includes a trailing ":platform" portion on
+        ;; display names, so make ERC recognize ?: as a word char.
+        (erc-masquerade--extra-word-chars ".:")
+        (erc-timestamp-use-align-to nil))
+    (erc-scenarios-masquerade--batch
+     (lambda ()
+       (ert-info ("Completion at point works for local names")
+         (save-excursion
+           (goto-char erc-input-marker)
+           (insert "some")
+           (call-interactively #'erc-tab)
+           (should (looking-back "someone: "))
+           (goto-char erc-input-marker)
+           (delete-region erc-input-marker (point-max))
+           ;; SHA<tab> won't work because they were only present
+           ;; during playback.
+           (insert "TR")
+           (call-interactively #'erc-tab)
+           (should (looking-back "TRUST:dvdovt.org: "))
+           (delete-region erc-input-marker (point-max))))))))
+
+;; This demos a user-provided custom parsing function.
+(ert-deftest erc-scenarios-masquerade-batch--custom-parser ()
+  :tags '(:expensive-test)
+  (require 'erc-fill)
+  (let ((erc-masquerade-bots
+         '((ExampleOrg
+            ("fake" . ignore) ; legacy :type of (nick . func)
+            ("#foo" "botman"
+             erc-masquerade--preform-legacy-fallback))))
+        (erc-interpret-mirc-color t)
+        (erc-fill-wrap-merge-indicator
+         '(pre #xb7 erc-fill-wrap-merge-indicator-face))
+        (erc-fill-function #'erc-fill-wrap))
+    (erc-scenarios-masquerade--batch
+     (lambda ()
+       (erc-scenarios-masquerade--assert-completion)
+       (goto-char (point-min))
+       (should-not (search-forward "<botman>" nil t))
+       (should (search-forward "<focused-ne>" nil t))
+       (goto-char (1+ (match-beginning 0)))
+       (should (string= (get-text-property (pos-bol) 'erc--masquerade)
+                        "focused-ne:telegram"))
+       (should (search-forward "<gallant>" nil t))
+       (goto-char (1+ (match-beginning 0)))
+       (should (string= (get-text-property (pos-bol) 'erc--masquerade)
+                        "gallant:xsxpvtson.net"))
+       (should (string= (get-text-property (pos-bol) 'erc--spkr)
+                        "botman gallant:xsxpvtson.net"))))))
+
+;;; erc-scenarios-masquerade.el ends here
diff --git a/test/lisp/erc/resources/masquerade/batch.eld b/test/lisp/erc/resources/masquerade/batch.eld
new file mode 100644
index 00000000000..58d915e95ea
--- /dev/null
+++ b/test/lisp/erc/resources/masquerade/batch.eld
@@ -0,0 +1,86 @@
+;;; -*- mode: lisp-data -*-
+
+((pass 0.2 "PASS :password123"))
+((nick 0.2 "NICK tester"))
+((user 0.2 "USER user 0 * :tester")
+ (0.0 "@time=" now " :irc.example.org NOTICE tester :*** Found your ident, 'tester'")
+ (0.0 "@time=" now " :irc.example.org 001 tester :Welcome to the example.org IRC Network tester!~tester@127.0.0.1")
+ (0.0 "@time=" now " :irc.example.org 002 tester :Your host is irc.example.org, running version InspIRCd-3")
+ (0.0 "@time=" now " :irc.example.org 003 tester :This server was created 23:41:47 Nov 17 2020")
+ (0.0 "@time=" now " :irc.example.org 004 tester irc.example.org InspIRCd-3 BILRSWcghiorswz ABEFHIJLMNOQRSTXYabcefghijklmnopqrstuvz :BEFHIJLXYabefghjkloqv")
+ (0.0 "@time=" now " :irc.example.org 005 tester ACCEPT=30 AWAYLEN=200 BOT=B CALLERID=g CASEMAPPING=rfc1459 CHANLIMIT=#:120 CHANMODES=IXbeg,k,BEFHJLfjl,AMNOQRSTcimnprstuz CHANNELLEN=64 CHANTYPES=# ELIST=CMNTU ESILENCE=CcdiNnPpTtx EXCEPTS=e :are supported by this server")
+ (0.0 "@time=" now " :irc.example.org 005 tester EXTBAN=,ANOQRSTUacmnprz HOSTLEN=64 INVEX=I KEYLEN=32 KICKLEN=255 LINELEN=512 MAXLIST=I:100,X:100,b:100,e:100,g:100 MAXTARGETS=20 MODES=20 MONITOR=30 NAMELEN=128 NAMESX NETWORK=ExampleOrg :are supported by this server")
+ (0.0 "@time=" now " :irc.example.org 005 tester NICKLEN=31 PREFIX=(Yqaohv)!~&@%!R(MISSING)EMOVE SAFELIST SECURELIST=60 SILENCE=32 STATUSMSG=!~&@%!T(MISSING)OPICLEN=307 UHNAMES USERIP USERLEN=11 USERMODES=,,s,BILRSWcghiorwz WATCH=30 :are supported by this server")
+ (0.0 "@time=" now " :irc.example.org 005 tester :are supported by this server")
+ (0.0 "@time=" now " :irc.example.org 251 tester :There are 740 users and 105 invisible on 10 servers")
+ (0.0 "@time=" now " :irc.example.org 252 tester 9 :operator(s) online")
+ (0.0 "@time=" now " :irc.example.org 253 tester 1 :unknown connections")
+ (0.0 "@time=" now " :irc.example.org 254 tester 378 :channels formed")
+ (0.0 "@time=" now " :irc.example.org 255 tester :I have 223 clients and 1 servers")
+ (0.0 "@time=" now " :irc.example.org 265 tester :Current local users: 223  Max: 226")
+ (0.0 "@time=" now " :irc.example.org 266 tester :Current global users: 845  Max: 903")
+ (0.0 "@time=" now " :irc.example.org 375 tester :irc.example.org message of the day")
+ (0.0 "@time=" now " :irc.example.org 372 tester :   figlet 1")
+ (0.0 "@time=" now " :irc.example.org 372 tester :   figlet 2")
+ (0.0 "@time=" now " :irc.example.org 376 tester :End of message of the day."))
+
+((mode-user 20 "MODE tester +i")
+ (0.0 "@time=" now " :irc.example.org NOTICE tester :Setting your virtual host: irc.example.org")
+ (0.0 "@time=" now " :irc.example.org 396 tester irc.example.org :is now your displayed host")
+ ;; It doesn't make sense for a server to send 396 before 900 because the latter will clobber user/host.
+ ;; XXX verify that this is even possible.
+ (0.0 "@time=" now ";msgid=632~1605656507~232192;inspircd.org/service;inspircd.org/bot :NickServ!services@services.example.org NOTICE tester :Password accepted - you are now recognized.")
+ (0.0 "@time=" now " :irc.example.org 900 tester tester!~tester@127.0.0.1 tester :You are now logged in as tester")
+ (0.0 "@time=" now ";inspircd.org/service;inspircd.org/bot :NickServ!services@services.example.org MODE tester :+r")
+
+ (0 "@time=" now " :tester!~tester@127.0.0.1 JOIN #foo")
+ (0 "@time=" now " :irc.example.org 332 tester #foo :Topic for tester: ERC: the powerful, modular, and extensible Emacs IRC client | https://gnu.org/s/emacs/erc | Questions welcome! | See https://gnu.org/server/irc-rules and https://gnu.org/philosophy/kind-communication | Asynchronous (non blocking) erc connection patches - http://platypus.pepperfish.net/~vivek/erc/ | https://nitter.net/UniOulu/status/1032214470263812096")
+ (0 "@time=" now " :irc.example.org 333 tester #foo frostyE!~fros@inr/apmsns 1587065826")
+ (0 "@time=" now " :irc.example.org 353 tester = #foo :@bob botman!BillyBot@foo.org ecstatic!~ecstatic@196-99-4-98 RELAX!~relax@unmxxzqzmhvj/jvvno/ooh/xsooh")
+ (0 "@time=" now " :irc.example.org 353 tester = #foo :tester!~tester@127.0.0.1 iym_ irde dkm4 someone")
+ (0 "@time=" now " :irc.example.org 366 tester #foo :End of /NAMES list.")
+
+ ;; XXX yes, obviously these BATCH messages and tags don't make any
+ ;; sense without prior negotiation.  But this is just for demo
+ ;; purposes.  ERC on master (5.6) just ignores them.  See Bug#49860.
+ (0 "@time=" now " :irc.example.org BATCH +1 chathistory :#foo")
+ (0 "@batch=1;time=2021-01-01T04:35:31.906Z :botman!BillyBot@foo.org PRIVMSG #foo :<focused-ne:telegram> He cannot be heard of. Out of doubt he is transported.")
+ (0 "@batch=1;time=2021-01-01T04:36:25.763Z :botman!BillyBot@foo.org PRIVMSG #foo :<focused-ne:telegram> I can't you had send to someone. ~ i've not thsthr it bghbfghr a message in file ")
+ (0 "@batch=1;time=2021-01-01T05:33:17.150Z :botman!BillyBot@foo.org PRIVMSG #foo :<gallant:xsxpvtson.net> More evident than this; for this was stol'n.")
+ (0 "@batch=1;time=2021-01-01T07:42:35.816Z :botman!BillyBot@foo.org PRIVMSG #foo :<focused-ne:telegram> Worse if not wutjbpet dzuubnp yeztevej is up when I try to dwnnnedt to a")
+ (0 "@batch=1;time=2021-01-01T14:37:58.877Z :botman!BillyBot@foo.org PRIVMSG #foo :<gallant:xsxpvtson.net> Tphsjiomjiheey funny! I hope that it would not send the iontvnts of the file.")
+ (0 "@batch=1;time=2021-01-02T15:00:49.413Z :botman!BillyBot@foo.org PRIVMSG #foo :<wonderkinaolive.now:westlandia.org> Sell when you abcde.")
+ (0 "@batch=1;time=2021-01-02T15:00:49.511Z :botman!BillyBot@foo.org PRIVMSG #foo :<TRUST:dvdovt.org> focused-ne: There. it's mostly vsixx spvm :P ah :-p lel, just axsierzrza that when I fire up a second network and join a second z.o. #aunozen-meaz")
+ (0 "@batch=1;time=2021-01-02T17:29:53.606Z :botman!BillyBot@foo.org PRIVMSG #foo :<focused-ne:telegram> Their ahqwan of nwak. n_n hope i dwdnv just send them that sarhffc srhnahnr :P I get")
+ (0 "@batch=1;time=2021-01-02T17:36:25.763Z :botman!BillyBot@foo.org PRIVMSG #foo :\00302<hell-cz:matrix.org> \17> <@mircat:plan9.org> you didn't complete the install but tried to lock or boot")
+ (0 "@batch=1;time=2021-01-02T17:37:25.763Z :botman!BillyBot@foo.org PRIVMSG #foo :\00302<hell-cz:matrix.org> \17I completed the install over a year ago...")
+ (0 "@batch=1;time=2021-01-02T17:38:25.763Z :botman!BillyBot@foo.org PRIVMSG #foo :\00307<mircat:plan9.org> \17> <@hell-cz:matrix.org> I completed the install over a year ago...")
+ (0 "@batch=1;time=2021-01-02T17:39:25.763Z :botman!BillyBot@foo.org PRIVMSG #foo :\00307<mircat:plan9.org> \17it was unlocked since them?")
+ (0 "@batch=1;time=2021-01-02T18:23:02.472Z :botman!BillyBot@foo.org PRIVMSG #foo :<gallant:xsxpvtson.net> that have sent that text to that owck? if i had hit uozuy? ~ someone pwckuc the same name as i'd given the bot i was writing, was about to cneprueuoz")
+ (0 "@batch=1;time=2021-01-02T20:14:00.040Z :botman!BillyBot@foo.org PRIVMSG #foo :<SHA:kpvxmd.org> There's the fool hangs on your back already, wonderkinaolive.now.")
+ (0 "@batch=1;time=2021-01-02T20:14:04.597Z :someone!~someone@127.0.0.1 PRIVMSG #foo :Something. Sounds hard. Or, maybe i'm nntl and this has been there since at start with a # such as #foo and tbtrtedrt it is nnliktla to be a name.")
+ (0 "@batch=1;time=2021-01-02T20:14:10.513Z :botman!BillyBot@foo.org PRIVMSG #foo :<SHA:kpvxmd.org> The ape is dead, and I must conjure him.")
+ (0 "@batch=1;time=2021-01-03T05:09:40.611Z :botman!BillyBot@foo.org PRIVMSG #foo :<TRUST:dvdovt.org> Prempc at the qeccem. i was too safrjw, i just closed it, but... would using really ahesj which one to send to i don't think, so much as maybe send to the")
+ (0 "@batch=1;time=2021-01-03T05:46:05.791Z :botman!BillyBot@foo.org PRIVMSG #foo :<focused-ne:telegram> SHA: One of those ksys that because of the gwpx of gallant pnrtqrts to be hrqelnss pnreqrswtlnr and only new stuff wftqrdwru.")
+ (0 "@batch=1;time=2021-01-03T05:49:13.305Z :botman!BillyBot@foo.org PRIVMSG #foo :<focused-ne:telegram> gallant: my liege; the youngest son of Sir Rowland de Boys.")
+ (0 "@batch=1;time=2021-01-03T06:04:06.661Z :botman!BillyBot@foo.org PRIVMSG #foo :<focused-ne:telegram> Mean... zttist mode is right there. (That should probably be the horsvp's name for the buffer. That 4670-74/hsg67666.fthv seems like a different problem.")
+ (0 "@batch=1;time=2021-01-03T07:35:07.624Z :botman!BillyBot@foo.org PRIVMSG #foo :<SHA:kpvxmd.org> And must advise the emperor for his good.")
+ (0 "@batch=1;time=2021-01-03T07:35:27.191Z :botman!BillyBot@foo.org PRIVMSG #foo :<SHA:kpvxmd.org> Have open a buffer aksde gxwiaspfsb and then /qvdxh you it will use the same https://fists.yav.gxy/kxbcivd/ctsf/dskbs-edvdf/5614-15/ssy61666.ctsf This is probably cannot be easily fixed.")
+ (0 "@batch=1;time=2021-01-03T07:35:57.130Z :tester!~tester@127.0.0.1 PRIVMSG #foo :Flrahcr yet. a bug that I did a not great job of hltabtz so far seems related: have any particular name space and ahcrcforc are possible to eollbwc.")
+ (0 "@batch=1;time=2021-01-03T07:36:20.310Z :botman!BillyBot@foo.org PRIVMSG #foo :<SHA:kpvxmd.org> To have the touches dearest priz'd.")
+ (0 "@batch=1;time=2021-01-03T07:37:56.279Z :botman!BillyBot@foo.org PRIVMSG #foo :<SHA:kpvxmd.org> So if I code. wow. that one def qoaorvoa k-x roporc-okgpa-duj if you can make a -Q pollhahon. But direct messages use the name of the nhpzngko which does not")
+ (0 "@batch=1;time=2021-01-03T08:54:04.650Z :botman!BillyBot@foo.org PRIVMSG #foo :<focused-ne:telegram> SHA: Sppleppodk to private message someone who is using the same dofb as an open because I ruzpodljy have #debian both on Wrllduil and UWPF open at the same")
+ (0 "@batch=1;time=2021-01-03T09:03:42.584Z :botman!BillyBot@foo.org PRIVMSG #foo :<focused-ne:telegram> For he hath still been tried a holy man.")
+ (0 "@batch=1;time=2021-01-03T09:03:48.985Z :unknown!~unknown@192-91-9-18 PRIVMSG #foo :help")
+ (0 "@batch=1;time=2021-01-03T09:03:48.985Z :botman!BillyBot@foo.org PRIVMSG #foo :<focused-ne:telegram> #frllklw-klwjlsrs this year so look out for lrq-ssqbb-sfsg by 0200 :P I ZKJ Lrqlrs :) PS, I fllt completely xwtqlsssln by zlxqzstlrs using python in.")
+ (0 "@time=" now " :irc.example.org BATCH :-1"))
+
+((~mode-foo 10 "MODE #foo")
+ (0.0 "@time=" now " :irc.example.org 324 tester #foo +HMfnrt 50:5h :10:5")
+ (0.0 "@time=" now " :irc.example.org 329 tester #foo :1602642829")
+ (0.1 "@time=" now " :botman!BillyBot@foo.org PRIVMSG #foo :<gallant:xsxpvtson.net> According to the fool's bolt, sir, and such dulcet diseases, focused-ne <- may not highlighted because this can be processed before playback.")
+ (0.1 "@time=" now " :botman!BillyBot@foo.org PRIVMSG #foo :<TRUST:dvdovt.org> gallant: And hang himself. I pray you, do my greeting.")
+ (0.1 "@time=" now " :botman!BillyBot@foo.org PRIVMSG #foo :<TRUST:dvdovt.org> @focused-ne:telegram: And you sat smiling at his cruel prey.")
+ (0.1 "@time=" now " :botman!BillyBot@foo.org PRIVMSG #foo :<focused-ne:telegram> Or never after look me in the face."))
+
+((linger 30 LINGER))
-- 
2.44.0


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

end of thread, other threads:[~2024-03-05  3:03 UTC | newest]

Thread overview: 2+ messages (download: mbox.gz / follow: Atom feed)
-- links below jump to the message on this page --
2024-02-01  3:07 bug#68861: 30.0.50; ERC 5.x: Introduce a modern message-insertion API J.P.
     [not found] <87bk90swz5.fsf@neverwas.me>
2024-03-05  3:03 ` 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).