From 027b6ef23178832ffd9e29233d0ac5161b7b953c Mon Sep 17 00:00:00 2001 From: "F. Jason Park" 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 . + +;;; 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 +;; . +;; +;; 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: +;; +;; telegram is bad +;; matrix is slwo +;; 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 \" msg\" becomes +\" 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 \"\", and we see the speaker as \"\", +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 " ". + (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 , 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 . + +;;; 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 + " ") + "bob")) + (should (string= (erc-masquerade--canonicalize-prefix-mattermost + "[t\u200belegram] ") + "bob:telegram")) + (should (string= (erc-masquerade--canonicalize-prefix-mattermost + "[telegram-] ") + "bob:telegram"))) + +(ert-deftest erc-masquerade--match-nick-default () + ;; Baseline. + (should (equal (erc-masquerade--match-nick-default + " 123" + (rx bot "<" (group (+ nonl)) "> ") + "") + '("ABC" "ABC" "" "123" nil))) + + ;; Replacement. + (should (equal (erc-masquerade--match-nick-default + " 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 : " + "Blah...")) + (erc-parse-server-response + erc-server-process + (concat "@time=2022-07-18T09:19:34.604Z " + ":frisbeeosbridge!~frisbeeo@matrix.frisbeeos.org " + "PRIVMSG #chan : " + "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 " " 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 " " 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 : " + "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 : " + "Consider formatting display names differently to nicknames"))) + + (with-current-buffer "#chan" + (goto-char (point-min)) + (should (search-forward " " 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 + " Receiving clients SHOULD" + (buffer-substring-no-properties (line-beginning-position) + (line-end-position)))) + (goto-char (line-end-position)) + + (should (search-forward " " 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 +;; . + +;;; 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 " " "")))) + ;; 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 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 "" nil t)) + (should (search-forward "" nil t)) + (goto-char (1+ (match-beginning 0))) + (should (string= (get-text-property (pos-bol) 'erc--masquerade) + "focused-ne:telegram")) + (should (search-forward "" 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 : 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 : 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 : 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 : 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 : 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 : Sell when you abcde.") + (0 "@batch=1;time=2021-01-02T15:00:49.511Z :botman!BillyBot@foo.org PRIVMSG #foo : 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 : 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 \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 \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 \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 \17it was unlocked since them?") + (0 "@batch=1;time=2021-01-02T18:23:02.472Z :botman!BillyBot@foo.org PRIVMSG #foo : 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 : 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 : 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 : 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 : 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 : 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 : 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 : And must advise the emperor for his good.") + (0 "@batch=1;time=2021-01-03T07:35:27.191Z :botman!BillyBot@foo.org PRIVMSG #foo : 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 : To have the touches dearest priz'd.") + (0 "@batch=1;time=2021-01-03T07:37:56.279Z :botman!BillyBot@foo.org PRIVMSG #foo : 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 : 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 : 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 : #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 : 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 : gallant: And hang himself. I pray you, do my greeting.") + (0.1 "@time=" now " :botman!BillyBot@foo.org PRIVMSG #foo : @focused-ne:telegram: And you sat smiling at his cruel prey.") + (0.1 "@time=" now " :botman!BillyBot@foo.org PRIVMSG #foo : Or never after look me in the face.")) + +((linger 30 LINGER)) -- 2.44.0