summaryrefslogtreecommitdiff
path: root/lisp/erc/erc.el
diff options
context:
space:
mode:
Diffstat (limited to 'lisp/erc/erc.el')
-rw-r--r--lisp/erc/erc.el1113
1 files changed, 760 insertions, 353 deletions
diff --git a/lisp/erc/erc.el b/lisp/erc/erc.el
index 635228e7f55..151d75e7ce1 100644
--- a/lisp/erc/erc.el
+++ b/lisp/erc/erc.el
@@ -2,8 +2,8 @@
;; Copyright (C) 1997-2022 Free Software Foundation, Inc.
-;; Author: Alexander L. Belikoff (alexander@belikoff.net)
-;; Maintainer: Amin Bandali <bandali@gnu.org>
+;; Author: Alexander L. Belikoff <alexander@belikoff.net>
+;; Maintainer: Amin Bandali <bandali@gnu.org>, F. Jason Park <jp@neverwas.me>
;; Contributors: Sergey Berezin (sergey.berezin@cs.cmu.edu),
;; Mario Lang (mlang@delysid.org),
;; Alex Schroeder (alex@gnu.org)
@@ -12,8 +12,8 @@
;; David Edmondson (dme@dme.org)
;; Michael Olson (mwolson@gnu.org)
;; Kelvin White (kwhite@gnu.org)
-;; Version: 5.4
-;; Package-Requires: ((emacs "27.1"))
+;; Version: 5.4.1
+;; Package-Requires: ((emacs "27.1") (compat "28.1.2.0"))
;; Keywords: IRC, chat, client, Internet
;; URL: https://www.gnu.org/software/emacs/erc.html
@@ -69,7 +69,9 @@
(require 'iso8601)
(eval-when-compile (require 'subr-x))
-(defconst erc-version "5.4"
+(require 'erc-compat)
+
+(defconst erc-version "5.4.1"
"This version of ERC.")
(defvar erc-official-location
@@ -83,7 +85,8 @@
'customize-package-emacs-version-alist
'(ERC ("5.2" . "22.1")
("5.3" . "23.1")
- ("5.4" . "28.1")))
+ ("5.4" . "28.1")
+ ("5.4.1" . "29.1")))
(defgroup erc nil
"Emacs Internet Relay Chat client."
@@ -129,7 +132,29 @@
"Running scripts at startup and with /LOAD."
:group 'erc)
-(require 'erc-backend)
+;; Defined in erc-backend
+(defvar erc--server-last-reconnect-count)
+(defvar erc--server-reconnecting)
+(defvar erc-channel-members-changed-hook)
+(defvar erc-network)
+(defvar erc-networks--id)
+(defvar erc-server-367-functions)
+(defvar erc-server-announced-name)
+(defvar erc-server-connect-function)
+(defvar erc-server-connected)
+(defvar erc-server-current-nick)
+(defvar erc-server-lag)
+(defvar erc-server-last-sent-time)
+(defvar erc-server-process)
+(defvar erc-server-quitting)
+(defvar erc-server-reconnect-count)
+(defvar erc-server-reconnecting)
+(defvar erc-session-client-certificate)
+(defvar erc-session-connector)
+(defvar erc-session-port)
+(defvar erc-session-server)
+(defvar erc-session-user-full-name)
+(defvar erc-session-username)
;; tunable connection and authentication parameters
@@ -189,16 +214,30 @@ parameters and authentication."
:set (lambda (sym val)
(set sym (if (functionp val) (funcall val) val))))
-(defcustom erc-rename-buffers nil
+(defcustom erc-rename-buffers t
"Non-nil means rename buffers with network name, if available."
:version "24.5"
:group 'erc
:type 'boolean)
+;; For the sake of compatibility, an ID will be created on the user's
+;; behalf when `erc-rename-buffers' is nil and one wasn't provided.
+;; The name will simply be that of the buffer, usually SERVER:PORT.
+;; This violates the policy of treating provided IDs as gospel, but
+;; it'll have to do for now.
+
+(make-obsolete-variable 'erc-rename-buffers
+ "old behavior when t now permanent" "29.1")
+
(defvar erc-password nil
- "Password to use when authenticating to an IRC server.
-It is not strictly necessary to provide this, since ERC will
-prompt you for it.")
+ "Password to use when authenticating to an IRC server interactively.
+
+This variable only exists for legacy reasons. It's not customizable and
+is limited to a single server password. Users looking for similar
+functionality should consider auth-source instead. See info
+node `(auth) Top' and info node `(erc) Connecting'.")
+
+(make-obsolete-variable 'erc-password "use auth-source instead" "29.1")
(defcustom erc-user-mode "+i"
;; +i "Invisible". Hides user from global /who and /names.
@@ -209,7 +248,7 @@ prompt you for it.")
(defcustom erc-prompt-for-password t
- "Asks before using the default password, or whether to enter a new one."
+ "Ask for a server password when invoking `erc-tls' interactively."
:group 'erc
:type 'boolean)
@@ -223,13 +262,49 @@ prompt you for it.")
:group 'erc
:type 'boolean)
-(defcustom erc-hide-prompt nil
- "If non-nil, do not display the prompt for commands.
+(defcustom erc-inhibit-multiline-input nil
+ "When non-nil, conditionally disallow input consisting of multiple lines.
+Issue an error when the number of input lines submitted for
+sending exceeds this value. The value t means disallow more
+than 1 line of input."
+ :package-version '(ERC . "5.4.1") ; FIXME match to next release
+ :group 'erc
+ :type '(choice integer boolean))
+
+(defcustom erc-ask-about-multiline-input nil
+ "Whether to ask to ignore `erc-inhibit-multiline-input' when tripped."
+ :package-version '(ERC . "5.4.1") ; FIXME match to next release
+ :group 'erc
+ :type 'boolean)
-\(A command is any input starting with a `/').
+(defcustom erc-prompt-hidden ">"
+ "Text to show in lieu of the prompt when hidden."
+ :package-version '(ERC . "5.4.1") ; FIXME increment on next ELPA release
+ :group 'erc-display
+ :type 'string)
-See also the variables `erc-prompt' and `erc-command-indicator'."
+(defcustom erc-hide-prompt t
+ "If non-nil, hide input prompt upon disconnecting.
+To unhide, type something in the input area. Once revealed, a
+prompt remains unhidden until the next disconnection. Channel
+prompts are unhidden upon rejoining. See
+`erc-unhide-query-prompt' for behavior concerning query prompts."
+ :package-version '(ERC . "5.4.1") ; FIXME increment on next ELPA release
+ :group 'erc-display
+ :type '(choice (const :tag "Always hide prompt" t)
+ (set (const server)
+ (const query)
+ (const channel))))
+
+(defcustom erc-unhide-query-prompt nil
+ "When non-nil, always reveal query prompts upon reconnecting.
+Otherwise, prompts in a connection's query buffers remain hidden
+until the user types in the input area or a new message arrives
+from the target."
+ :package-version '(ERC . "5.4.1") ; FIXME increment on next ELPA release
:group 'erc-display
+ ;; Extensions may one day offer a way to discover whether a target
+ ;; is online. When that happens, this can be expanded accordingly.
:type 'boolean)
;; tunable GUI stuff
@@ -351,18 +426,30 @@ erc-channel-user struct.")
"Hash table of users on the current server.
It associates nicknames with `erc-server-user' struct instances.")
+(defconst erc--casemapping-rfc1459
+ (make-translation-table
+ '((?\[ . ?\{) (?\] . ?\}) (?\\ . ?\|) (?~ . ?^))
+ (mapcar (lambda (c) (cons c (+ c 32))) "ABCDEFGHIJKLMNOPQRSTUVWXYZ")))
+
+(defconst erc--casemapping-rfc1459-strict
+ (make-translation-table
+ '((?\[ . ?\{) (?\] . ?\}) (?\\ . ?\|))
+ (mapcar (lambda (c) (cons c (+ c 32))) "ABCDEFGHIJKLMNOPQRSTUVWXYZ")))
+
(defun erc-downcase (string)
- "Convert STRING to IRC standard conforming downcase."
- (let ((s (downcase string))
- (c '((?\[ . ?\{)
- (?\] . ?\})
- (?\\ . ?\|)
- (?~ . ?^))))
- (save-match-data
- (while (string-match "[]\\[~]" s)
- (aset s (match-beginning 0)
- (cdr (assq (aref s (match-beginning 0)) c)))))
- s))
+ "Return a downcased copy of STRING with properties.
+Use the CASEMAPPING ISUPPORT parameter to determine the style."
+ (let* ((mapping (erc--get-isupport-entry 'CASEMAPPING 'single))
+ (inhibit-read-only t))
+ (if (equal mapping "ascii")
+ (downcase string)
+ (with-temp-buffer
+ (insert string)
+ (translate-region (point-min) (point-max)
+ (if (equal mapping "rfc1459-strict")
+ erc--casemapping-rfc1459-strict
+ erc--casemapping-rfc1459))
+ (buffer-string)))))
(defmacro erc-with-server-buffer (&rest body)
"Execute BODY in the current ERC server buffer.
@@ -871,8 +958,8 @@ See `erc-server-flood-margin' for other flood-related parameters.")
;; Script parameters
(defcustom erc-startup-file-list
- (list (concat user-emacs-directory ".ercrc.el")
- (concat user-emacs-directory ".ercrc")
+ (list (locate-user-emacs-file ".ercrc.el")
+ (locate-user-emacs-file ".ercrc")
"~/.ercrc.el" "~/.ercrc" ".ercrc.el" ".ercrc")
"List of files to try for a startup script.
The first existent and readable one will get executed.
@@ -1053,6 +1140,29 @@ The struct has three slots:
:type 'hook
:version "27.1")
+;; This is being auditioned for possible exporting (as a custom hook
+;; option). Likewise for (public versions of) `erc--input-split' and
+;; `erc--discard-trailing-multiline-nulls'. If unneeded, we'll just
+;; run the latter on the input after `erc-pre-send-functions', and
+;; remove this hook and the struct completely. IOW, if you need this,
+;; please say so.
+
+(defvar erc--pre-send-split-functions '(erc--discard-trailing-multiline-nulls)
+ "Special hook for modifying individual lines in multiline prompt input.
+The functions are called with one argument, an `erc--input-split'
+struct, which they can optionally modify.
+
+The struct has five slots:
+
+ `string': the input string delivered by `erc-pre-send-functions'
+ `insertp': whether to insert the lines into the buffer
+ `sendp': whether the lines should be sent to the IRC server
+ `lines': a list of lines to be sent, each one a `string'
+ `cmdp': whether to interpret input as a command, like /ignore
+
+The `string' field is effectively read-only. When `cmdp' is
+non-nil, all but the first line will be discarded.")
+
(defvar erc-insert-this t
"Insert the text into the target buffer or not.
Functions on `erc-insert-pre-hook' can set this variable to nil
@@ -1291,7 +1401,7 @@ Example:
#\\='erc-replace-insert))
((remove-hook \\='erc-insert-modify-hook
#\\='erc-replace-insert)))"
- (declare (doc-string 3))
+ (declare (doc-string 3) (indent defun))
(let* ((sn (symbol-name name))
(mode (intern (format "erc-%s-mode" (downcase sn))))
(group (intern (format "erc-%s" (downcase sn))))
@@ -1337,6 +1447,45 @@ if ARG is omitted or nil.
(put ',enable 'definition-name ',name)
(put ',disable 'definition-name ',name))))
+;; The rationale for favoring inheritance here (nicer dispatch) is
+;; kinda flimsy since there aren't yet any actual methods.
+
+(cl-defstruct erc--target
+ (string "" :type string :documentation "Received name of target.")
+ (symbol nil :type symbol :documentation "Case-mapped name as symbol."))
+
+;; These should probably take on a `joined' field to track joinedness,
+;; which should be toggled by `erc-server-JOIN', `erc-server-PART',
+;; etc. Functions like `erc--current-buffer-joined-p' (bug#48598) may
+;; find it useful.
+
+(cl-defstruct (erc--target-channel (:include erc--target)))
+
+(cl-defstruct (erc--target-channel-local (:include erc--target-channel)))
+
+;; At some point, it may make sense to add a query type with an
+;; account field, which may help support reassociation across
+;; reconnects and nick changes (likely requires v3 extensions).
+
+(defun erc--target-from-string (string)
+ "Construct an `erc--target' variant from STRING."
+ (funcall (if (erc-channel-p string)
+ (if (erc--valid-local-channel-p string)
+ #'make-erc--target-channel-local
+ #'make-erc--target-channel)
+ #'make-erc--target)
+ :string string :symbol (intern (erc-downcase string))))
+
+(defvar-local erc--target nil
+ "Info about a buffer's target, if any.")
+
+;; Temporary internal getter to ease transition to `erc--target'
+;; everywhere. Will be replaced by updated `erc-default-target'.
+(defun erc--default-target ()
+ "Return target string or nil."
+ (when erc--target
+ (erc--target-string erc--target)))
+
(defun erc-once-with-server-event (event f)
"Run function F the next time EVENT occurs in the `current-buffer'.
@@ -1478,6 +1627,7 @@ Defaults to the server buffer."
(define-derived-mode erc-mode fundamental-mode "ERC"
"Major mode for Emacs IRC."
+ :interactive nil
(setq local-abbrev-table erc-mode-abbrev-table)
(setq-local next-line-add-newlines nil)
(setq line-move-ignore-invisible t)
@@ -1486,6 +1636,7 @@ Defaults to the server buffer."
(setq-local paragraph-start
(concat "\\(" (regexp-quote (erc-prompt)) "\\)"))
(setq-local completion-ignore-case t)
+ (add-hook 'kill-buffer-hook #'erc-kill-buffer-function nil t)
(add-hook 'completion-at-point-functions #'erc-complete-word-at-point nil t))
;; activation
@@ -1519,6 +1670,22 @@ The available choices are:
(const :tag "Use current buffer" buffer)
(const :tag "Use current buffer" t)))
+(defcustom erc-reconnect-display nil
+ "How (and whether) to display a channel buffer upon reconnecting.
+
+This only affects automatic reconnections and is ignored when
+issuing a /reconnect command or reinvoking `erc-tls' with the
+same args (assuming success, of course). See `erc-join-buffer'
+for a description of possible values."
+ :package-version '(ERC . "5.4.1") ; FIXME increment upon publishing to ELPA
+ :group 'erc-buffers
+ :type '(choice (const :tag "Use value of `erc-join-buffer'" nil)
+ (const :tag "Split window and select" window)
+ (const :tag "Split window, don't select" window-noselect)
+ (const :tag "New frame" frame)
+ (const :tag "Bury in new buffer" bury)
+ (const :tag "Use current buffer" buffer)))
+
(defcustom erc-frame-alist nil
"Alist of frame parameters for creating erc frames.
A value of nil means to use `default-frame-alist'."
@@ -1550,6 +1717,14 @@ effect when `erc-join-buffer' is set to `frame'."
(erc-channel-p (erc-default-target))))
(t nil)))
+;; For the sake of compatibility, a historical quirk concerning this
+;; option, when nil, has been preserved: all buffers are suffixed with
+;; the original dialed host name, which is usually something like
+;; irc.libera.chat. Collisions are handled by adding a uniquifying
+;; numeric suffix of the form <N>. Note that channel reassociation
+;; behavior involving this option (when nil) was inverted in 28.1 (ERC
+;; 5.4 and 5.4.1). This was regrettable and has since been undone.
+
(defcustom erc-reuse-buffers t
"If nil, create new buffers on joining a channel/query.
If non-nil, a new buffer will only be created when you join
@@ -1559,6 +1734,9 @@ the existing buffers will be reused."
:group 'erc-buffers
:type 'boolean)
+(make-obsolete-variable 'erc-reuse-buffers
+ "old behavior when t now permanent" "29.1")
+
(defun erc-normalize-port (port)
"Normalize the port specification PORT to integer form.
PORT may be an integer, a string or a symbol. If it is a string or a
@@ -1594,55 +1772,61 @@ symbol, it may have these values:
"Check whether ports A and B are equal."
(= (erc-normalize-port a) (erc-normalize-port b)))
-(defun erc-generate-new-buffer-name (server port target)
- "Create a new buffer name based on the arguments."
- (when (numberp port) (setq port (number-to-string port)))
- (let* ((buf-name (or target
- (let ((name (concat server ":" port)))
- (when (> (length name) 1)
- name))
- ;; This fallback should in fact never happen.
- "*erc-server-buffer*"))
- (full-buf-name (concat buf-name "/" server))
- (dup-buf-name (buffer-name (car (erc-channel-list nil))))
- buffer-name)
- ;; Reuse existing buffers, but not if the buffer is a connected server
- ;; buffer and not if its associated with a different server than the
- ;; current ERC buffer.
- ;; If buf-name is taken by a different connection (or by something !erc)
- ;; then see if "buf-name/server" meets the same criteria.
- (if (and dup-buf-name (string-match-p (concat buf-name "/") dup-buf-name))
- (setq buffer-name full-buf-name) ; ERC buffer with full name already exists.
- (dolist (candidate (list buf-name full-buf-name))
- (if (and (not buffer-name)
- erc-reuse-buffers
- (or (not (get-buffer candidate))
- ;; Looking for a server buffer, so there's no target.
- (and (not target)
- (with-current-buffer (get-buffer candidate)
- (and (erc-server-buffer-p)
- (not (erc-server-process-alive)))))
- ;; Channel buffer; check that it's from the right server.
- (and target
- (with-current-buffer (get-buffer candidate)
- (and (string= erc-session-server server)
- (erc-port-equal erc-session-port port))))))
- (setq buffer-name candidate)
- (when (and (not buffer-name) (get-buffer buf-name) erc-reuse-buffers)
- ;; A new buffer will be created with the name buf-name/server, rename
- ;; the existing name-duplicated buffer with the same format as well.
- (with-current-buffer (get-buffer buf-name)
- (when (derived-mode-p 'erc-mode) ; ensure it's an erc buffer
- (rename-buffer
- (concat buf-name "/" (or erc-session-server erc-server-announced-name)))))))))
- ;; If buffer-name is unset, neither candidate worked out for us,
- ;; fallback to the old <N> uniquification method:
- (or buffer-name (generate-new-buffer-name full-buf-name))))
-
-(defun erc-get-buffer-create (server port target)
+(defun erc-generate-new-buffer-name (server port target &optional tgt-info id)
+ "Determine the name of an ERC buffer.
+When TGT-INFO is nil, assume this is a server buffer. If ID is non-nil,
+return ID as a string unless a buffer already exists with a live server
+process, in which case signal an error. When ID is nil, return a
+temporary name based on SERVER and PORT to be replaced with the network
+name when discovered (see `erc-networks--rename-server-buffer'). Allow
+either SERVER or PORT (but not both) to be nil to accommodate oddball
+`erc-server-connect-function's.
+
+When TGT-INFO is non-nil, expect its string field to match the redundant
+param TARGET (retained for compatibility). Whenever possibly, prefer
+returning TGT-INFO's string unmodified. But when a case-insensitive
+collision prevents that, return target@ID when ID is non-nil or
+target@network otherwise after renaming the conflicting buffer in the
+same manner."
+ (when target ; compat
+ (setq tgt-info (erc--target-from-string target)))
+ (if tgt-info
+ (let* ((esid (erc-networks--id-symbol erc-networks--id))
+ (name (if esid
+ (erc-networks--reconcile-buffer-names tgt-info
+ erc-networks--id)
+ (erc--target-string tgt-info))))
+ (if (and esid (with-suppressed-warnings ((obsolete erc-reuse-buffers))
+ erc-reuse-buffers))
+ name
+ (generate-new-buffer-name name)))
+ (if (and (with-suppressed-warnings ((obsolete erc-reuse-buffers))
+ erc-reuse-buffers)
+ id)
+ (progn
+ (when-let* ((buf (get-buffer (symbol-name id)))
+ ((erc-server-process-alive buf)))
+ (user-error "Session with ID %S already exists" id))
+ (symbol-name id))
+ (generate-new-buffer-name (if (and server port)
+ (if (with-suppressed-warnings
+ ((obsolete erc-reuse-buffers))
+ erc-reuse-buffers)
+ (format "%s:%s" server port)
+ (format "%s:%s/%s" server port server))
+ (or server port))))))
+
+(defun erc-get-buffer-create (server port target &optional tgt-info id)
"Create a new buffer based on the arguments."
- (get-buffer-create (erc-generate-new-buffer-name server port target)))
-
+ (when target ; compat
+ (setq tgt-info (erc--target-from-string target)))
+ (if (and erc--server-reconnecting
+ (not tgt-info)
+ (with-suppressed-warnings ((obsolete erc-reuse-buffers))
+ erc-reuse-buffers))
+ (current-buffer)
+ (get-buffer-create
+ (erc-generate-new-buffer-name server port nil tgt-info id))))
(defun erc-member-ignore-case (string list)
"Return non-nil if STRING is a member of LIST.
@@ -1759,12 +1943,7 @@ nil."
(lambda (bufname)
(let ((buf (if (consp bufname)
(cdr bufname) (get-buffer bufname))))
- (when buf
- (erc--buffer-p buf (lambda () t) proc)
- (with-current-buffer buf
- (and (derived-mode-p 'erc-mode)
- (or (null proc)
- (eq proc erc-server-process))))))))))
+ (and buf (erc--buffer-p buf (lambda () t) proc)))))))
(defun erc-switch-to-buffer (&optional arg)
"Prompt for an ERC buffer to switch to.
When invoked with prefix argument, use all ERC buffers. Without
@@ -1802,12 +1981,24 @@ all channel buffers on all servers."
;; Some local variables
+;; TODO eventually deprecate this variable
+;;
+;; In the ancient, pre-CVS days (prior to June 2001), this list may
+;; have been used for supporting the changing of a buffer's target on
+;; the fly (mid-session). Such usage, which allowed cons cells like
+;; (QUERY . bob) to serve as the list's head, was either never fully
+;; integrated or was partially clobbered prior to the introduction of
+;; version control. But vestiges remain (see `erc-dcc-chat-mode').
+;; And despite appearances, no evidence has emerged that ERC ever
+;; supported one-to-many target buffers. If such a thing was aspired
+;; to, it was never realized.
+;;
+;; New library code should use the `erc--target' struct instead.
+;; Third-party code can continue to use this until a getter for
+;; `erc--target' (or whatever replaces it) is exported.
(defvar-local erc-default-recipients nil
"List of default recipients of the current buffer.")
-(defvar-local erc-session-user-full-name nil
- "Full name of the user on the current server.")
-
(defvar-local erc-channel-user-limit nil
"Limit of users per channel.")
@@ -1948,7 +2139,10 @@ removed from the list will be disabled."
(defun erc-setup-buffer (buffer)
"Consults `erc-join-buffer' to find out how to display `BUFFER'."
- (pcase erc-join-buffer
+ (pcase (if (zerop (erc-with-server-buffer
+ erc--server-last-reconnect-count))
+ erc-join-buffer
+ (or erc-reconnect-display erc-join-buffer))
('window
(if (active-minibuffer-window)
(display-buffer buffer)
@@ -1974,8 +2168,8 @@ removed from the list will be disabled."
(defun erc-open (&optional server port nick full-name
connect passwd tgt-list channel process
- client-certificate)
- "Connect to SERVER on PORT as NICK with FULL-NAME.
+ client-certificate user id)
+ "Connect to SERVER on PORT as NICK with USER and FULL-NAME.
If CONNECT is non-nil, connect to the server. Otherwise assume
already connected and just create a separate buffer for the new
@@ -1991,15 +2185,17 @@ of the client certificate itself to use when connecting over TLS,
or t, which means that `auth-source' will be queried for the
private key and the certificate.
+When non-nil, ID should be a symbol for identifying the connection.
+
Returns the buffer for the given server or channel."
- (let ((server-announced-name (when (and (boundp 'erc-session-server)
- (string= server erc-session-server))
- erc-server-announced-name))
- (connected-p (unless connect erc-server-connected))
- (buffer (erc-get-buffer-create server port channel))
- (old-buffer (current-buffer))
- old-point
- continued-session)
+ (let* ((target (and channel (erc--target-from-string channel)))
+ (buffer (erc-get-buffer-create server port nil target id))
+ (old-buffer (current-buffer))
+ old-point
+ (continued-session (and erc--server-reconnecting
+ (with-suppressed-warnings
+ ((obsolete erc-reuse-buffers))
+ erc-reuse-buffers))))
(when connect (run-hook-with-args 'erc-before-connect server port nick))
(erc-update-modules)
(set-buffer buffer)
@@ -2007,8 +2203,9 @@ Returns the buffer for the given server or channel."
(let ((old-recon-count erc-server-reconnect-count))
(erc-mode)
(setq erc-server-reconnect-count old-recon-count))
- (setq erc-server-announced-name server-announced-name)
- (setq erc-server-connected connected-p)
+ (when (setq erc-server-connected (not connect))
+ (setq erc-server-announced-name
+ (buffer-local-value 'erc-server-announced-name old-buffer)))
;; connection parameters
(setq erc-server-process process)
(setq erc-insert-marker (make-marker))
@@ -2017,7 +2214,7 @@ Returns the buffer for the given server or channel."
;; (the buffer may have existed)
(goto-char (point-max))
(forward-line 0)
- (when (get-text-property (point) 'erc-prompt)
+ (when (or continued-session (get-text-property (point) 'erc-prompt))
(setq continued-session t)
(set-marker erc-input-marker
(or (next-single-property-change (point) 'erc-prompt)
@@ -2028,6 +2225,9 @@ Returns the buffer for the given server or channel."
(set-marker erc-insert-marker (point))
;; stack of default recipients
(setq erc-default-recipients tgt-list)
+ (when target
+ (setq erc--target target
+ erc-network (erc-network)))
(setq erc-server-current-nick nil)
;; Initialize erc-server-users and erc-channel-users
(if connect
@@ -2039,8 +2239,6 @@ Returns the buffer for the given server or channel."
(setq erc-server-users nil)
(setq erc-channel-users
(make-hash-table :test 'equal))))
- ;; clear last incomplete line read
- (setq erc-server-filter-data nil)
(setq erc-channel-topic "")
;; limit on the number of users on the channel (mode +l)
(setq erc-channel-user-limit nil)
@@ -2057,24 +2255,12 @@ Returns the buffer for the given server or channel."
(setq erc-logged-in nil)
;; The local copy of `erc-nick' - the list of nicks to choose
(setq erc-default-nicks (if (consp erc-nick) erc-nick (list erc-nick)))
- ;; password stuff
- (setq erc-session-password
- (or passwd
- (let ((secret
- (plist-get
- (nth 0
- (auth-source-search :host server
- :max 1
- :user nick
- ;; secrets.el wouldn’t accept a number
- :port (if (numberp port) (number-to-string port) port)
- :require '(:secret)))
- :secret)))
- (if (functionp secret)
- (funcall secret)
- secret))))
;; client certificate (only useful if connecting over TLS)
(setq erc-session-client-certificate client-certificate)
+ (setq erc-networks--id (if connect
+ (erc-networks--id-create id)
+ (buffer-local-value 'erc-networks--id
+ old-buffer)))
;; debug output buffer
(setq erc-dbuf
(when erc-log-p
@@ -2084,12 +2270,13 @@ Returns the buffer for the given server or channel."
(goto-char (point-max))
(insert "\n"))
(if continued-session
- (goto-char old-point)
+ (progn (goto-char old-point)
+ (erc--unhide-prompt))
(set-marker erc-insert-marker (point))
(erc-display-prompt)
(goto-char (point-max)))
- (erc-determine-parameters server port nick full-name)
+ (erc-determine-parameters server port nick full-name user passwd)
;; Saving log file on exit
(run-hook-with-args 'erc-connect-pre-hook buffer)
@@ -2187,11 +2374,9 @@ parameters SERVER and NICK."
(setq server user-input)
(setq passwd (if erc-prompt-for-password
- (if (and erc-password
- (y-or-n-p "Use the default password? "))
- erc-password
- (read-passwd "Password: "))
- erc-password))
+ (read-passwd "Server password: ")
+ (with-suppressed-warnings ((obsolete erc-password))
+ erc-password)))
(when (and passwd (string= "" passwd))
(setq passwd nil))
@@ -2210,8 +2395,10 @@ parameters SERVER and NICK."
(cl-defun erc (&key (server (erc-compute-server))
(port (erc-compute-port))
(nick (erc-compute-nick))
+ (user (erc-compute-user))
password
- (full-name (erc-compute-full-name)))
+ (full-name (erc-compute-full-name))
+ id)
"ERC is a powerful, modular, and extensible IRC client.
This function is the main entry point for ERC.
@@ -2221,8 +2408,10 @@ Non-interactively, it takes the keyword arguments
(server (erc-compute-server))
(port (erc-compute-port))
(nick (erc-compute-nick))
+ (user (erc-compute-user))
password
(full-name (erc-compute-full-name))
+ id
That is, if called with
@@ -2230,9 +2419,13 @@ That is, if called with
then the server and full-name will be set to those values,
whereas `erc-compute-port' and `erc-compute-nick' will be invoked
-for the values of the other parameters."
+for the values of the other parameters.
+
+When present, ID should be an opaque object used to identify the
+connection unequivocally. This is rarely needed and not available
+interactively."
(interactive (erc-select-read-args))
- (erc-open server port nick full-name t password))
+ (erc-open server port nick full-name t password nil nil nil nil user id))
;;;###autoload
(defalias 'erc-select #'erc)
@@ -2242,9 +2435,11 @@ for the values of the other parameters."
(cl-defun erc-tls (&key (server (erc-compute-server))
(port (erc-compute-port))
(nick (erc-compute-nick))
+ (user (erc-compute-user))
password
(full-name (erc-compute-full-name))
- client-certificate)
+ client-certificate
+ id)
"ERC is a powerful, modular, and extensible IRC client.
This function is the main entry point for ERC over TLS.
@@ -2258,6 +2453,7 @@ Non-interactively, it takes the keyword arguments
password
(full-name (erc-compute-full-name))
client-certificate
+ id
That is, if called with
@@ -2279,13 +2475,19 @@ Example usage:
(erc-tls :server \"irc.libera.chat\" :port 6697
:client-certificate
- '(\"/home/bandali/my-cert.key\"
- \"/home/bandali/my-cert.crt\"))"
+ \\='(\"/home/bandali/my-cert.key\"
+ \"/home/bandali/my-cert.crt\"))
+
+When present, ID should be an opaque object for identifying the
+connection unequivocally. (In most cases, this would be a string or a
+symbol composed of letters from the Latin alphabet.) This option is
+generally unneeded, however. See info node `(erc) Connecting' for use
+cases. Not available interactively."
(interactive (let ((erc-default-port erc-default-port-tls))
(erc-select-read-args)))
(let ((erc-server-connect-function 'erc-open-tls-stream))
(erc-open server port nick full-name t password
- nil nil nil client-certificate)))
+ nil nil nil client-certificate user id)))
(defun erc-open-tls-stream (name buffer host port &rest parameters)
"Open an TLS stream to an IRC server.
@@ -2341,8 +2543,6 @@ but you won't see it.
WARNING: Do not set this variable directly! Instead, use the
function `erc-toggle-debug-irc-protocol' to toggle its value.")
-(declare-function erc-network-name "erc-networks" ())
-
(defun erc-log-irc-protocol (string &optional outbound)
"Append STRING to the buffer *erc-protocol*.
@@ -2352,15 +2552,20 @@ The buffer is created if it doesn't exist.
If OUTBOUND is non-nil, STRING is being sent to the IRC server and
appears in face `erc-input-face' in the buffer. Lines must already
-contain CRLF endings. Peer is identified by the most precise label
-available at run time, starting with the network name, followed by the
-announced host name, and falling back to the dialed <server>:<port>."
+contain CRLF endings. A peer is identified by the most precise label
+available, starting with the session ID followed by the server-reported
+hostname, and falling back to the dialed <server>:<port> pair.
+
+When capturing logs for multiple peers and sorting them into buckets,
+such inconsistent labeling may pose a problem until the MOTD is
+received. Setting a fixed `erc-networks--id' can serve as a
+workaround."
(when erc-debug-irc-protocol
- (let ((esid (or (and (fboundp 'erc-network)
- (erc-network)
- (erc-network-name))
- erc-server-announced-name
- (format "%s:%s" erc-session-server erc-session-port)))
+ (let ((esid (if-let ((erc-networks--id)
+ (esid (erc-networks--id-symbol erc-networks--id)))
+ (symbol-name esid)
+ (or erc-server-announced-name
+ (format "%s:%s" erc-session-server erc-session-port))))
(ts (when erc-debug-irc-protocol-time-format
(format-time-string erc-debug-irc-protocol-time-format))))
(with-current-buffer (get-buffer-create "*erc-protocol*")
@@ -2403,7 +2608,8 @@ If ARG is non-nil, show the *erc-protocol* buffer."
(concat "This buffer displays all IRC protocol "
"traffic exchanged with servers."))
(erc-make-notice "Kill it to disable logging.")
- (erc-make-notice "Press `t' to toggle."))))
+ (erc-make-notice (substitute-command-keys
+ "Press \\`t' to toggle.")))))
(insert (string-join msg "\r\n")))
(use-local-map (make-sparse-keymap))
(local-set-key (kbd "t") 'erc-toggle-debug-irc-protocol))
@@ -2760,7 +2966,7 @@ returns non-nil."
(let* ((command (erc-response.command parsed))
(sender (car (erc-parse-user (erc-response.sender parsed))))
(channel (car (erc-response.command-args parsed)))
- (network (or (and (fboundp 'erc-network-name) (erc-network-name))
+ (network (or (and (erc-network) (erc-network-name))
(erc-shorten-server-name
(or erc-server-announced-name
erc-session-server))))
@@ -2816,20 +3022,19 @@ present."
(let ((prop-val (erc-get-parsed-vector position)))
(and prop-val (member (erc-response.command prop-val) list))))
-(defvar-local erc-send-input-line-function 'erc-send-input-line)
+(defvar-local erc-send-input-line-function 'erc-send-input-line
+ "Function for sending lines lacking a leading user command.
+When a line typed into a buffer contains an explicit command, like /msg,
+a corresponding handler (here, erc-cmd-MSG) is called. But lines typed
+into a channel or query buffer already have an implicit target and
+command (PRIVMSG). This function is called on such occasions and also
+for special purposes (see erc-dcc.el).")
(defun erc-send-input-line (target line &optional force)
- "Send LINE to TARGET.
-
-See also `erc-server-send'."
- (setq line (format "PRIVMSG %s :%s"
- target
- ;; If the line is empty, we still want to
- ;; send it - i.e. an empty pasted line.
- (if (string= line "\n")
- " \n"
- line)))
- (erc-server-send line force target))
+ "Send LINE to TARGET."
+ (when (string= line "\n")
+ (setq line " \n"))
+ (erc-message "PRIVMSG" (concat target " " line) force))
(defun erc-get-arglist (fun)
"Return the argument list of a function without the parens."
@@ -2967,7 +3172,7 @@ Commands for which no erc-cmd-xxx exists, are tunneled through
this function. LINE is sent to the server verbatim, and
therefore has to contain the command itself as well."
(erc-log (format "cmd: DEFAULT: %s" line))
- (erc-server-send (substring line 1))
+ (erc-server-send (string-trim-right (substring line 1) "[\r\n]"))
t)
(defvar erc--read-time-period-history nil)
@@ -3186,22 +3391,137 @@ For a list of user commands (/join /part, ...):
(defalias 'erc-cmd-H #'erc-cmd-HELP)
(put 'erc-cmd-HELP 'process-not-needed t)
+(defcustom erc-auth-source-server-function #'erc-auth-source-search
+ "Function to query auth-source for a server password.
+Called with a subset of keyword parameters known to
+`auth-source-search' and relevant to an opening \"PASS\" command,
+if any. In return, ERC expects a string to send as the server
+password, or nil, to skip the \"PASS\" command completely. An
+explicit `:password' argument to entry-point commands `erc' and
+`erc-tls' also inhibits lookup, as does setting this option to
+nil. See info node `(erc) Connecting' for details."
+ :package-version '(ERC . "5.4.1") ; FIXME update when publishing to ELPA
+ :group 'erc
+ :type '(choice (const erc-auth-source-search)
+ (const nil)
+ function))
+
+(defcustom erc-auth-source-join-function #'erc-auth-source-search
+ "Function to query auth-source on joining a channel.
+Called with a subset of keyword arguments known to
+`auth-source-search' and relevant to joining a password-protected
+channel. In return, ERC expects a string to use as the channel
+\"key\", or nil to just join the channel normally. Setting the
+option itself to nil tells ERC to always forgo consulting
+auth-source for channel keys. For more information, see info
+node `(erc) Connecting'."
+ :package-version '(ERC . "5.4.1") ; FIXME update when publishing to ELPA
+ :group 'erc
+ :type '(choice (const erc-auth-source-search)
+ (const nil)
+ function))
+
+(defun erc--auth-source-determine-params-defaults ()
+ (let* ((net (and-let* ((esid (erc-networks--id-symbol erc-networks--id))
+ ((symbol-name esid)))))
+ (localp (and erc--target (erc--target-channel-local-p erc--target)))
+ (hosts (if localp
+ (list erc-server-announced-name erc-session-server net)
+ (list net erc-server-announced-name erc-session-server)))
+ (ports (list (cl-typecase erc-session-port
+ (integer (number-to-string erc-session-port))
+ (string (and (string= erc-session-port "irc")
+ erc-session-port)) ; or nil
+ (t erc-session-port))
+ "irc")))
+ (list (cons :host (delq nil hosts))
+ (cons :port (delq nil ports))
+ (cons :require '(:secret)))))
+
+(defun erc--auth-source-determine-params-merge (&rest plist)
+ "Return a plist of merged keyword args to pass to `auth-source-search'.
+Combine items in PLIST with others derived from the current connection
+context, but prioritize the former. For keys not present in PLIST,
+favor a network ID over an announced server unless `erc--target' is a
+local channel. And treat the dialed server address as a fallback for
+the announced name in both cases."
+ (let ((defaults (erc--auth-source-determine-params-defaults)))
+ `(,@(cl-loop for (key value) on plist by #'cddr
+ for default = (assq key defaults)
+ do (when default (setq defaults (delq default defaults)))
+ append `(,key ,(delete-dups
+ `(,@(if (consp value) value (list value))
+ ,@(cdr default)))))
+ ,@(cl-loop for (k . v) in defaults append (list k v)))))
+
+(defun erc--auth-source-search (&rest defaults)
+ "Ask auth-source for a secret and return it if found.
+Use DEFAULTS as keyword arguments for querying auth-source and as a
+guide for narrowing results. Return a string if found or nil otherwise.
+The ordering of DEFAULTS influences how results are filtered, as does
+the ordering of the members of any individual composite values. If
+necessary, the former takes priority. For example, if DEFAULTS were to
+contain
+
+ :host (\"foo\" \"bar\") :port (\"123\" \"456\")
+
+the secret from an auth-source entry of host foo and port 456 would be
+chosen over another of host bar and port 123. However, if DEFAULTS
+looked like
+
+ :port (\"123\" \"456\") :host (\"foo\" \"bar\")
+
+the opposite would be true. In both cases, two entries with the same
+host but different ports would result in the one with port 123 getting
+the nod. Much the same would happen for entries sharing only a port:
+the one with host foo would win."
+ (when-let*
+ ((priority (map-keys defaults))
+ (test (lambda (a b)
+ (catch 'done
+ (dolist (key priority)
+ (let* ((d (plist-get defaults key))
+ (defval (if (listp d) d (list d)))
+ ;; featurep 'seq via auth-source > json > map
+ (p (seq-position defval (plist-get a key)))
+ (q (seq-position defval (plist-get b key))))
+ (unless (eql p q)
+ (throw 'done (when p (or (not q) (< p q))))))))))
+ (plist (copy-sequence defaults)))
+ (unless (plist-get plist :max)
+ (setq plist (plist-put plist :max 5000))) ; `auth-source-netrc-parse'
+ (unless (plist-get defaults :require)
+ (setq plist (plist-put plist :require '(:secret))))
+ (when-let* ((sorted (sort (apply #'auth-source-search plist) test))
+ (secret (plist-get (car sorted) :secret)))
+ (if (functionp secret) (funcall secret) secret))))
+
+(defun erc-auth-source-search (&rest plist)
+ "Call `auth-source-search', possibly with keyword params in PLIST."
+ ;; These exist as separate helpers in case folks should find them
+ ;; useful. If that's you, please request that they be exported.
+ (apply #'erc--auth-source-search
+ (apply #'erc--auth-source-determine-params-merge plist)))
+
(defun erc-server-join-channel (server channel &optional secret)
- (let* ((secret (or secret
- (plist-get (nth 0 (auth-source-search
- :max 1
- :host server
- :port "irc"
- :user channel))
- :secret)))
- (password (if (functionp secret)
- (funcall secret)
- secret)))
- (erc-log (format "cmd: JOIN: %s" channel))
- (erc-server-send (concat "JOIN " channel
- (if password
- (concat " " password)
- "")))))
+ "Join CHANNEL, optionally with SECRET.
+Without SECRET, consult auth-source, possibly passing SERVER as the
+`:host' query parameter."
+ (unless (or secret (not erc-auth-source-join-function))
+ (unless server
+ (when (and erc-server-announced-name
+ (erc--valid-local-channel-p channel))
+ (setq server erc-server-announced-name)))
+ (setq secret (apply erc-auth-source-join-function
+ `(,@(and server (list :host server)) :user ,channel))))
+ (erc-log (format "cmd: JOIN: %s" channel))
+ (erc-server-send (concat "JOIN " channel (and secret (concat " " secret)))))
+
+(defun erc--valid-local-channel-p (channel)
+ "Non-nil when channel is server-local on a network that allows them."
+ (and-let* (((eq ?& (aref channel 0)))
+ (chan-types (erc--get-isupport-entry 'CHANTYPES 'single))
+ ((string-search "&" chan-types)))))
(defun erc-cmd-JOIN (channel &optional key)
"Join the channel given in CHANNEL, optionally with KEY.
@@ -3215,18 +3535,12 @@ were most recently invited. See also `invitation'."
(setq chnl (erc-ensure-channel-name channel)))
(when chnl
;; Prevent double joining of same channel on same server.
- (let* ((joined-channels
- (mapcar (lambda (chanbuf)
- (with-current-buffer chanbuf (erc-default-target)))
- (erc-channel-list erc-server-process)))
- (server (with-current-buffer (process-buffer erc-server-process)
- (or erc-session-server erc-server-announced-name)))
- (chnl-name (car (erc-member-ignore-case chnl joined-channels))))
- (if chnl-name
- (switch-to-buffer (if (get-buffer chnl-name)
- chnl-name
- (concat chnl-name "/" server)))
- (erc-server-join-channel server chnl key)))))
+ (if-let* ((existing (erc-get-buffer chnl erc-server-process))
+ ((with-current-buffer existing
+ (erc-get-channel-user (erc-current-nick)))))
+ (switch-to-buffer existing)
+ (setq erc--server-last-reconnect-count 0)
+ (erc-server-join-channel nil chnl key))))
t)
(defalias 'erc-cmd-CHANNEL #'erc-cmd-JOIN)
@@ -3528,8 +3842,8 @@ The rest of LINE is the message to send."
(defun erc-cmd-NICK (nick)
"Change current nickname to NICK."
(erc-log (format "cmd: NICK: %s (erc-bad-nick: %S)" nick erc-bad-nick))
- (let ((nicklen (cdr (assoc "NICKLEN" (erc-with-server-buffer
- erc-server-parameters)))))
+ (let ((nicklen (erc-with-server-buffer
+ (erc--get-isupport-entry 'NICKLEN 'single))))
(and nicklen (> (length nick) (string-to-number nicklen))
(erc-display-message
nil 'notice 'active 'nick-too-long
@@ -3608,20 +3922,23 @@ other people should be displayed."
(defun erc-cmd-QUERY (&optional user)
"Open a query with USER.
-The type of query window/frame/etc will depend on the value of
-`erc-query-display'.
-
-If USER is omitted, close the current query buffer if one exists
-- except this is broken now ;-)"
+How the query is displayed (in a new window, frame, etc.) depends
+on the value of `erc-query-display'."
+ ;; FIXME: The doc string used to say at the end:
+ ;; "If USER is omitted, close the current query buffer if one exists
+ ;; - except this is broken now ;-)"
+ ;; Does it make sense to have that functionality? What's wrong with
+ ;; `kill-buffer'? If it makes sense, re-add it. -- SK @ 2021-11-11
(interactive
(list (read-string "Start a query with: ")))
- (let ((session-buffer (erc-server-buffer))
- (erc-join-buffer erc-query-display))
- (if user
- (erc-query user session-buffer)
+ (unless user
;; currently broken, evil hack to display help anyway
;(erc-delete-query))))
- (signal 'wrong-number-of-arguments ""))))
+ (signal 'wrong-number-of-arguments ""))
+ (let ((erc-join-buffer erc-query-display))
+ (erc-with-server-buffer
+ (erc--open-target user))))
+
(defalias 'erc-cmd-Q #'erc-cmd-QUERY)
(defun erc-quit/part-reason-default ()
@@ -3639,12 +3956,7 @@ If S is non-nil, it will be used as the quit reason."
"Zippy quit message.
If S is non-nil, it will be used as the quit reason."
- (or s
- (if (fboundp 'yow)
- (if (>= emacs-major-version 28)
- (string-replace "\n" "" (yow))
- (replace-regexp-in-string "\n" "" (yow)))
- (erc-quit/part-reason-default))))
+ (or s (erc-quit/part-reason-default)))
(make-obsolete 'erc-quit-reason-zippy "it will be removed." "24.4")
@@ -3668,12 +3980,7 @@ If S is non-nil, it will be used as the part reason."
"Zippy part message.
If S is non-nil, it will be used as the quit reason."
- (or s
- (if (fboundp 'yow)
- (if (>= emacs-major-version 28)
- (string-replace "\n" "" (yow))
- (replace-regexp-in-string "\n" "" (yow)))
- (erc-quit/part-reason-default))))
+ (or s (erc-quit/part-reason-default)))
(make-obsolete 'erc-part-reason-zippy "it will be removed." "24.4")
@@ -3754,13 +4061,21 @@ the message given by REASON."
(setq buffer (current-buffer)))
(with-current-buffer buffer
(setq erc-server-quitting nil)
- (setq erc-server-reconnecting t)
+ (with-suppressed-warnings ((obsolete erc-server-reconnecting))
+ (setq erc-server-reconnecting t))
+ (setq erc--server-reconnecting t)
(setq erc-server-reconnect-count 0)
(setq process (get-buffer-process (erc-server-buffer)))
- (if process
- (delete-process process)
- (erc-server-reconnect))
- (setq erc-server-reconnecting nil)))
+ (when process
+ (delete-process process))
+ (erc-server-reconnect)
+ (with-suppressed-warnings ((obsolete erc-server-reconnecting)
+ ((obsolete erc-reuse-buffers)))
+ (if erc-reuse-buffers
+ (progn (cl-assert (not erc--server-reconnecting))
+ (cl-assert (not erc-server-reconnecting)))
+ (setq erc--server-reconnecting nil
+ erc-server-reconnecting nil)))))
t)
(put 'erc-cmd-RECONNECT 'process-not-needed t)
@@ -4251,8 +4566,6 @@ This places `point' just after the prompt, or at the beginning of the line."
(defun erc-complete-word-at-point ()
(run-hook-with-args-until-success 'erc-complete-functions))
-(define-obsolete-function-alias 'erc-complete-word #'completion-at-point "24.1")
-
;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
;;
;; IRC SERVER INPUT HANDLING
@@ -4299,27 +4612,30 @@ See `erc-default-server-hook'."
(nconc erc-server-vectors (list parsed))
nil)
-(defun erc-query (target server)
- "Open a query buffer on TARGET, using SERVER.
+(defun erc--open-target (target)
+ "Open an ERC buffer on TARGET."
+ (erc-open erc-session-server
+ erc-session-port
+ (erc-current-nick)
+ erc-session-user-full-name
+ nil
+ nil
+ (list target)
+ target
+ erc-server-process
+ nil
+ erc-session-username
+ (erc-networks--id-given erc-networks--id)))
+
+(defun erc-query (target server-buffer)
+ "Open a query buffer on TARGET using SERVER-BUFFER.
To change how this query window is displayed, use `let' to bind
`erc-join-buffer' before calling this."
- (unless (and server
- (buffer-live-p server)
- (set-buffer server))
+ (declare (obsolete "bind `erc-cmd-query' and call `erc-cmd-QUERY'" "29.1"))
+ (unless (buffer-live-p server-buffer)
(error "Couldn't switch to server buffer"))
- (let ((buf (erc-open erc-session-server
- erc-session-port
- (erc-current-nick)
- erc-session-user-full-name
- nil
- nil
- (list target)
- target
- erc-server-process)))
- (unless buf
- (error "Couldn't open query window"))
- (erc-update-mode-line)
- buf))
+ (with-current-buffer server-buffer
+ (erc--open-target target)))
(defcustom erc-auto-query 'window-noselect
"If non-nil, create a query buffer each time you receive a private message.
@@ -4338,6 +4654,10 @@ a new window, but not to select it. See the documentation for
(const :tag "Use current buffer" buffer)
(const :tag "Use current buffer" t)))
+;; FIXME either retire this or put it to use after determining how
+;; it's meant to work. Clearly, the doc string does not describe
+;; current behavior. It's currently only used by the obsolete
+;; function `erc-auto-query'.
(defcustom erc-query-on-unjoined-chan-privmsg t
"If non-nil create query buffer on receiving any PRIVMSG at all.
This includes PRIVMSGs directed to channels. If you are using an IRC
@@ -4398,9 +4718,8 @@ See also `erc-display-error-notice'."
(format "Nickname %s is %s, try another." nick reason))
(setq erc-nick-change-attempt-count (+ erc-nick-change-attempt-count 1))
(let ((newnick (nth 1 erc-default-nicks))
- (nicklen (cdr (assoc "NICKLEN"
- (erc-with-server-buffer
- erc-server-parameters)))))
+ (nicklen (erc-with-server-buffer
+ (erc--get-isupport-entry 'NICKLEN 'single))))
(setq erc-bad-nick t)
;; try to use a different nick
(if erc-default-nicks
@@ -4461,6 +4780,8 @@ and as second argument the event parsed as a vector."
(erc-cmd-QUERY query))
nil))))
+(make-obsolete 'erc-auto-query "try erc-cmd-QUERY instead" "29.1")
+
(defun erc-is-message-ctcp-p (message)
"Check if MESSAGE is a CTCP message or not."
(string-match "^\C-a\\([^\C-a]*\\)\C-a?$" message))
@@ -4717,11 +5038,19 @@ Set user modes and run `erc-after-connect' hook."
(nick (car (erc-response.command-args parsed)))
(buffer (process-buffer proc)))
(setq erc-server-connected t)
- (setq erc-server-reconnect-count 0)
+ (setq erc--server-last-reconnect-count erc-server-reconnect-count
+ erc-server-reconnect-count 0)
(erc-update-mode-line)
(erc-set-initial-user-mode nick buffer)
(erc-server-setup-periodical-ping buffer)
- (run-hook-with-args 'erc-after-connect server nick)))))
+ (run-hook-with-args 'erc-after-connect server nick))))
+
+ (when erc-unhide-query-prompt
+ (erc-with-all-buffers-of-server proc
+ nil ; FIXME use `erc--target' after bug#48598
+ (when (and (erc-default-target)
+ (not (erc-channel-p (car erc-default-recipients))))
+ (erc--unhide-prompt)))))
(defun erc-set-initial-user-mode (nick buffer)
"If `erc-user-mode' is non-nil for NICK, set the user modes.
@@ -5003,8 +5332,7 @@ See also `erc-channel-begin-receiving-names'."
(defun erc-parse-prefix ()
"Return an alist of valid prefix character types and their representations.
Example: (operator) o => @, (voiced) v => +."
- (let ((str (or (cdr (assoc "PREFIX" (erc-with-server-buffer
- erc-server-parameters)))
+ (let ((str (or (erc-with-server-buffer (erc--get-isupport-entry 'PREFIX t))
;; provide a sane default
"(qaohv)~&@%+"))
types chars)
@@ -5544,7 +5872,7 @@ Specifically, return the position of `erc-insert-marker'."
(point-max))
(defvar erc-last-input-time 0
- "Time of last call to `erc-send-current-line'.
+ "Time of last successful call to `erc-send-current-line'.
If that function has never been called, the value is 0.")
(defcustom erc-accidental-paste-threshold-seconds 0.2
@@ -5560,6 +5888,68 @@ submitted line to be intentional."
:version "26.1"
:type '(choice number (other :tag "disabled" nil)))
+(defvar erc--input-line-delim-regexp (rx (| (: (? ?\r) ?\n) ?\r)))
+
+(defun erc--blank-in-multiline-input-p (lines)
+ "Detect whether LINES contains a blank line.
+When `erc-send-whitespace-lines' is in effect, return nil if
+LINES is multiline or the first line is non-empty. When
+`erc-send-whitespace-lines' is nil, return non-nil when any line
+is empty or consists of one or more spaces, tabs, or form-feeds."
+ (catch 'return
+ (let ((multilinep (cdr lines)))
+ (dolist (line lines)
+ (when (if erc-send-whitespace-lines
+ (and (string-empty-p line) (not multilinep))
+ (string-match (rx bot (* (in " \t\f")) eot) line))
+ (throw 'return t))))))
+
+(defun erc--check-prompt-input-for-excess-lines (_ lines)
+ "Return non-nil when trying to send too many LINES."
+ (when erc-inhibit-multiline-input
+ ;; Assume `erc--discard-trailing-multiline-nulls' is set to run
+ (let ((reversed (seq-drop-while #'string-empty-p (reverse lines)))
+ (max (if (eq erc-inhibit-multiline-input t)
+ 2
+ erc-inhibit-multiline-input))
+ (seen 0)
+ msg)
+ (while (and (pop reversed) (< (cl-incf seen) max)))
+ (when (= seen max)
+ (setq msg (format "(exceeded by %d)" (1+ (length reversed))))
+ (unless (and erc-ask-about-multiline-input
+ (y-or-n-p (concat "Send input " msg "?")))
+ (concat "Too many lines " msg))))))
+
+(defun erc--check-prompt-input-for-multiline-blanks (_ lines)
+ "Return non-nil when multiline prompt input has blank LINES."
+ (when (erc--blank-in-multiline-input-p lines)
+ (if erc-warn-about-blank-lines
+ "Blank line - ignoring..."
+ 'invalid)))
+
+(defun erc--check-prompt-input-for-point-in-bounds (_ _)
+ "Return non-nil when point is before prompt."
+ (when (< (point) (erc-beg-of-input-line))
+ "Point is not in the input area"))
+
+(defun erc--check-prompt-input-for-running-process (string _)
+ "Return non-nil unless in an active ERC server buffer."
+ (unless (or (erc-server-buffer-live-p)
+ (erc-command-no-process-p string))
+ "ERC: No process running"))
+
+(defvar erc--check-prompt-input-functions
+ '(erc--check-prompt-input-for-point-in-bounds
+ erc--check-prompt-input-for-multiline-blanks
+ erc--check-prompt-input-for-running-process
+ erc--check-prompt-input-for-excess-lines)
+ "Validators for user input typed at prompt.
+Called with latest input string submitted by user and the list of
+lines produced by splitting it. If any member function returns
+non-nil, processing is abandoned and input is left untouched.
+When the returned value is a string, pass it to `erc-error'.")
+
(defun erc-send-current-line ()
"Parse current line and send it to IRC."
(interactive)
@@ -5573,20 +5963,21 @@ submitted line to be intentional."
(eolp))
(expand-abbrev))
(widen)
- (if (< (point) (erc-beg-of-input-line))
- (erc-error "Point is not in the input area")
+ (if-let* ((str (erc-user-input))
+ (msg (run-hook-with-args-until-success
+ 'erc--check-prompt-input-functions str
+ (split-string str erc--input-line-delim-regexp))))
+ (when (stringp msg)
+ (erc-error msg))
(let ((inhibit-read-only t)
- (str (erc-user-input))
(old-buf (current-buffer)))
- (if (and (not (erc-server-buffer-live-p))
- (not (erc-command-no-process-p str)))
- (erc-error "ERC: No process running")
+ (progn ; unprogn this during next major surgery
(erc-set-active-buffer (current-buffer))
;; Kill the input and the prompt
(delete-region (erc-beg-of-input-line)
(erc-end-of-input-line))
(unwind-protect
- (erc-send-input str)
+ (erc-send-input str 'skip-ws-chk)
;; Fix the buffer if the command didn't kill it
(when (buffer-live-p old-buf)
(with-current-buffer old-buf
@@ -5601,8 +5992,8 @@ submitted line to be intentional."
(set-buffer-modified-p buffer-modified))))))
;; Only when last hook has been run...
- (run-hook-with-args 'erc-send-completed-hook str))))
- (setq erc-last-input-time now))
+ (run-hook-with-args 'erc-send-completed-hook str)))
+ (setq erc-last-input-time now)))
(switch-to-buffer "*ERC Accidental Paste Overflow*")
(lwarn 'erc :warning
"You seem to have accidentally pasted some text!"))))
@@ -5619,21 +6010,31 @@ submitted line to be intentional."
(cl-defstruct erc-input
string insertp sendp)
-(defun erc-send-input (input)
+(cl-defstruct (erc--input-split (:include erc-input))
+ lines cmdp)
+
+(defun erc--discard-trailing-multiline-nulls (state)
+ "Ensure last line of STATE's string is non-null.
+But only when `erc-send-whitespace-lines' is non-nil. STATE is
+an `erc--input-split' object."
+ (when (and erc-send-whitespace-lines (erc--input-split-lines state))
+ (let ((reversed (nreverse (erc--input-split-lines state))))
+ (when (string-empty-p (car reversed))
+ (pop reversed)
+ (setf (erc--input-split-cmdp state) nil))
+ (nreverse (seq-drop-while #'string-empty-p reversed)))))
+
+(defun erc-send-input (input &optional skip-ws-chk)
"Treat INPUT as typed in by the user.
It is assumed that the input and the prompt is already deleted.
Return non-nil only if we actually send anything."
;; Handle different kinds of inputs
- (cond
- ;; Ignore empty input
- ((if erc-send-whitespace-lines
- (string= input "")
- (string-match "\\`[ \t\r\f\n]*\\'" input))
- (when erc-warn-about-blank-lines
- (message "Blank line - ignoring...")
- (beep))
- nil)
- (t
+ (if (and (not skip-ws-chk)
+ (erc--check-prompt-input-for-multiline-blanks
+ input (split-string input erc--input-line-delim-regexp)))
+ (when erc-warn-about-blank-lines
+ (message "Blank line - ignoring...") ; compat
+ (beep))
;; This dynamic variable is used by `erc-send-pre-hook'. It's
;; obsolete, and when it's finally removed, this binding should
;; also be removed.
@@ -5653,48 +6054,28 @@ Return non-nil only if we actually send anything."
:insertp erc-insert-this
:sendp erc-send-this))
(run-hook-with-args 'erc-pre-send-functions state)
+ (setq state (make-erc--input-split
+ :string (erc-input-string state)
+ :insertp (erc-input-insertp state)
+ :sendp (erc-input-sendp state)
+ :lines (split-string (erc-input-string state)
+ erc--input-line-delim-regexp)
+ :cmdp (string-match erc-command-regexp
+ (erc-input-string state))))
+ (run-hook-with-args 'erc--pre-send-split-functions state)
(when (and (erc-input-sendp state)
- erc-send-this)
- (let ((string (erc-input-string state)))
- (if (or (if (>= emacs-major-version 28)
- (string-search "\n" string)
- (string-match "\n" string))
- (not (string-match erc-command-regexp string)))
- (mapc
- (lambda (line)
- (mapc
- (lambda (line)
- ;; Insert what has to be inserted for this.
- (when (erc-input-insertp state)
- (erc-display-msg line))
- (erc-process-input-line (concat line "\n")
- (null erc-flood-protect) t))
- (or (and erc-flood-protect (erc-split-line line))
- (list line))))
- (split-string string "\n"))
- (erc-process-input-line (concat string "\n") t nil))
- t))))))
-
-;; (defun erc-display-command (line)
-;; (when erc-insert-this
-;; (let ((insert-position (point)))
-;; (unless erc-hide-prompt
-;; (erc-display-prompt nil nil (erc-command-indicator)
-;; (and (erc-command-indicator)
-;; 'erc-command-indicator-face)))
-;; (let ((beg (point)))
-;; (insert line)
-;; (erc-put-text-property beg (point)
-;; 'font-lock-face 'erc-command-indicator-face)
-;; (insert "\n"))
-;; (when (processp erc-server-process)
-;; (set-marker (process-mark erc-server-process) (point)))
-;; (set-marker erc-insert-marker (point))
-;; (save-excursion
-;; (save-restriction
-;; (narrow-to-region insert-position (point))
-;; (run-hooks 'erc-send-modify-hook)
-;; (run-hooks 'erc-send-post-hook))))))
+ erc-send-this)
+ (let ((lines (erc--input-split-lines state)))
+ (if (and (erc--input-split-cmdp state) (not (cdr lines)))
+ (erc-process-input-line (concat (car lines) "\n") t nil)
+ (dolist (line lines)
+ (dolist (line (or (and erc-flood-protect (erc-split-line line))
+ (list line)))
+ (when (erc-input-insertp state)
+ (erc-display-msg line))
+ (erc-process-input-line (concat line "\n")
+ (null erc-flood-protect) t))))
+ t)))))
(defun erc-display-msg (line)
"Display LINE as a message of the user to the current target at point."
@@ -5786,6 +6167,27 @@ See also `erc-downcase'."
;; default target handling
+(defun erc--current-buffer-joined-p ()
+ "Return whether the current target buffer is joined."
+ ;; This may be a reliable means of detecting subscription status,
+ ;; but it's also roundabout and awkward. Perhaps it's worth
+ ;; discussing adding a joined slot to `erc--target' for this.
+ (cl-assert erc--target)
+ (and (erc--target-channel-p erc--target)
+ (erc-get-channel-user (erc-current-nick)) t))
+
+;; This function happens to return nil in channel buffers previously
+;; parted or those from which a user had been kicked. While this
+;; "works" for detecting whether a channel is currently subscribed to,
+;; new code should consider using
+;;
+;; (erc-get-channel-user (erc-current-nick))
+;;
+;; instead. For retrieving a target regardless of subscription or
+;; connection status, use replacements based on `erc--target'.
+;; (Coming soon.)
+;;
+;; TODO deprecate this
(defun erc-default-target ()
"Return the current default target (as a character string) or nil if none."
(let ((tgt (car erc-default-recipients)))
@@ -5796,12 +6198,14 @@ See also `erc-downcase'."
(defun erc-add-default-channel (channel)
"Add CHANNEL to the default channel list."
+ (declare (obsolete "use `erc-cmd-JOIN' or similar instead" "29.1"))
(let ((chl (downcase channel)))
(setq erc-default-recipients
(cons chl erc-default-recipients))))
(defun erc-delete-default-channel (channel &optional buffer)
"Delete CHANNEL from the default channel list."
+ (declare (obsolete "use `erc-cmd-PART' or similar instead" "29.1"))
(with-current-buffer (if (and buffer
(bufferp buffer))
buffer
@@ -5813,6 +6217,7 @@ See also `erc-downcase'."
"Add QUERY'd NICKNAME to the default channel list.
The previous default target of QUERY type gets removed."
+ (declare (obsolete "use `erc-cmd-QUERY' or similar instead" "29.1"))
(let ((d1 (car erc-default-recipients))
(d2 (cdr erc-default-recipients))
(qt (cons 'QUERY (downcase nickname))))
@@ -5823,7 +6228,7 @@ The previous default target of QUERY type gets removed."
(defun erc-delete-query ()
"Delete the topmost target if it is a QUERY."
-
+ (declare (obsolete "use one query buffer per target instead" "29.1"))
(let ((d1 (car erc-default-recipients))
(d2 (cdr erc-default-recipients)))
(if (and (listp d1)
@@ -6151,20 +6556,20 @@ user input."
erc-session-server
erc-session-user-full-name))
(if erc-session-password
- (erc-server-send (format "PASS %s" erc-session-password))
+ (erc-server-send (concat "PASS :" erc-session-password))
(message "Logging in without password"))
(erc-server-send (format "NICK %s" (erc-current-nick)))
(erc-server-send
(format "USER %s %s %s :%s"
;; hacked - S.B.
- (if erc-anonymous-login erc-email-userid (user-login-name))
+ erc-session-username
"0" "*"
erc-session-user-full-name))
(erc-update-mode-line))
;; connection properties' heuristics
-(defun erc-determine-parameters (&optional server port nick name)
+(defun erc-determine-parameters (&optional server port nick name user passwd)
"Determine the connection and authentication parameters.
Sets the buffer local variables:
@@ -6172,11 +6577,15 @@ Sets the buffer local variables:
- `erc-session-server'
- `erc-session-port'
- `erc-session-user-full-name'
+- `erc-session-username'
+- `erc-session-password'
- `erc-server-current-nick'"
(setq erc-session-connector erc-server-connect-function
erc-session-server (erc-compute-server server)
erc-session-port (or port erc-default-port)
- erc-session-user-full-name (erc-compute-full-name name))
+ erc-session-user-full-name (erc-compute-full-name name)
+ erc-session-username (erc-compute-user user)
+ erc-session-password (erc--compute-server-password passwd nick))
(erc-set-current-nick (erc-compute-nick nick)))
(defun erc-compute-server (&optional server)
@@ -6194,6 +6603,10 @@ non-nil value is found.
(getenv "IRCSERVER")
erc-default-server))
+(defun erc-compute-user (&optional user)
+ "Return a suitable value for the session user name."
+ (or user (if erc-anonymous-login erc-email-userid (user-login-name))))
+
(defun erc-compute-nick (&optional nick)
"Return user's IRC nick.
@@ -6209,6 +6622,12 @@ non-nil value is found.
(getenv "IRCNICK")
(user-login-name)))
+(defun erc--compute-server-password (password nick)
+ "Maybe provide a PASSWORD argument for the IRC \"PASS\" command.
+When `erc-auth-source-server-function' is non-nil, call it with NICK for
+the user field and use whatever it returns as the server password."
+ (or password (and erc-auth-source-server-function
+ (funcall erc-auth-source-server-function :user nick))))
(defun erc-compute-full-name (&optional full-name)
"Return user's full name.
@@ -6493,30 +6912,19 @@ This should be a string with substitution variables recognized by
(defun erc-format-network ()
"Return the name of the network we are currently on."
- (let ((network (and (fboundp 'erc-network-name) (erc-network-name))))
- (if (and network (symbolp network))
- (symbol-name network)
- "")))
+ (erc-network-name))
(defun erc-format-target-and/or-network ()
"Return the network or the current target and network combined.
If the name of the network is not available, then use the
shortened server name instead."
- (let ((network-name (or (and (fboundp 'erc-network-name) (erc-network-name))
- (erc-shorten-server-name
- (or erc-server-announced-name
- erc-session-server)))))
- (when (and network-name (symbolp network-name))
- (setq network-name (symbol-name network-name)))
- (cond ((erc-default-target)
- (concat (erc-string-no-properties (erc-default-target))
- "@" network-name))
- ((and network-name
- (not (get-buffer network-name)))
- (when erc-rename-buffers
- (rename-buffer network-name))
- network-name)
- (t (buffer-name (current-buffer))))))
+ (if-let ((erc--target)
+ (name (if-let ((esid (erc-networks--id-symbol erc-networks--id)))
+ (symbol-name esid)
+ (erc-shorten-server-name (or erc-server-announced-name
+ erc-session-server)))))
+ (concat (erc--target-string erc--target) "@" name)
+ (buffer-name)))
(defun erc-format-away-status ()
"Return a formatted `erc-mode-line-away-status-format' if `erc-away' is non-nil."
@@ -6597,21 +7005,12 @@ shortened server name instead."
(fill-region (point-min) (point-max))
(buffer-string))))
(setq header-line-format
- (if (>= emacs-major-version 28)
- (string-replace
- "%"
- "%%"
- (if face
- (propertize header 'help-echo help-echo
- 'face face)
- (propertize header 'help-echo help-echo)))
- (replace-regexp-in-string
- "%"
- "%%"
- (if face
- (propertize header 'help-echo help-echo
- 'face face)
- (propertize header 'help-echo help-echo)))))))
+ (string-replace
+ "%"
+ "%%"
+ (if face
+ (propertize header 'help-echo help-echo 'face face)
+ (propertize header 'help-echo help-echo))))))
(t (setq header-line-format
(if face
(propertize header 'face face)
@@ -6896,9 +7295,7 @@ functions."
nick user host channel
(if (not (string= reason ""))
(format ": %s"
- (if (>= emacs-major-version 28)
- (string-replace "%" "%%" reason)
- (replace-regexp-in-string "%" "%%" reason)))
+ (string-replace "%" "%%" reason))
"")))))
@@ -6933,23 +7330,29 @@ See also `format-spec'."
;;; Various hook functions
-;; FIXME: Don't set the hook globally!
-(add-hook 'kill-buffer-hook #'erc-kill-buffer-function)
-
-(defcustom erc-kill-server-hook '(erc-kill-server)
- "Invoked whenever a server buffer is killed via `kill-buffer'."
+(defcustom erc-kill-server-hook '(erc-kill-server
+ erc-networks-shrink-ids-and-buffer-names)
+ "Invoked whenever a live server buffer is killed via `kill-buffer'."
+ :package-version '(ERC . "5.4.1") ; FIXME increment upon publishing to ELPA
:group 'erc-hooks
:type 'hook)
-(defcustom erc-kill-channel-hook '(erc-kill-channel)
+(defcustom erc-kill-channel-hook
+ '(erc-kill-channel
+ erc-networks-shrink-ids-and-buffer-names
+ erc-networks-rename-surviving-target-buffer)
"Invoked whenever a channel-buffer is killed via `kill-buffer'."
+ :package-version '(ERC . "5.4.1") ; FIXME increment upon publishing to ELPA
:group 'erc-hooks
:type 'hook)
-(defcustom erc-kill-buffer-hook nil
- "Hook run whenever a non-server or channel buffer is killed.
+(defcustom erc-kill-buffer-hook
+ '(erc-networks-shrink-ids-and-buffer-names
+ erc-networks-rename-surviving-target-buffer)
+ "Hook run whenever a query buffer is killed.
See also `kill-buffer'."
+ :package-version '(ERC . "5.4.1") ; FIXME increment upon publishing to ELPA
:group 'erc-hooks
:type 'hook)
@@ -7022,6 +7425,7 @@ This function should be on `erc-kill-channel-hook'."
;; Teach url.el how to open irc:// URLs with ERC.
;; To activate, customize `url-irc-function' to `url-irc-erc'.
+;; FIXME change user to nick, and use API to find server buffer
;;;###autoload
(defun erc-handle-irc-url (host port channel user password)
"Use ERC to IRC on HOST:PORT in CHANNEL as USER with PASSWORD.
@@ -7043,9 +7447,12 @@ Otherwise, connect to HOST:PORT as USER and /join CHANNEL."
(provide 'erc)
+(require 'erc-backend)
+
;; Deprecated. We might eventually stop requiring the goodies automatically.
;; IMPORTANT: This require must appear _after_ the above (provide 'erc) to
;; avoid a recursive require error when byte-compiling the entire package.
(require 'erc-goodies)
+(require 'erc-networks)
;;; erc.el ends here