Mercurial > emacs
diff lisp/erc/erc.el @ 68451:fc745b05e928
Revision: emacs@sv.gnu.org/emacs--devo--0--patch-22
Creator: Michael Olson <mwolson@gnu.org>
Install ERC.
author | Miles Bader <miles@gnu.org> |
---|---|
date | Sun, 29 Jan 2006 13:08:58 +0000 |
parents | |
children | 04ad8d666956 |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/lisp/erc/erc.el Sun Jan 29 13:08:58 2006 +0000 @@ -0,0 +1,6144 @@ +;; erc.el --- An Emacs Internet Relay Chat client + +;; Copyright (C) 1997, 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, +;; 2006 Free Software Foundation, Inc. +;; Copyright (C) 2004 Brian Palmer + +;; Author: Alexander L. Belikoff (alexander@belikoff.net) +;; Contributors: Sergey Berezin (sergey.berezin@cs.cmu.edu), +;; Mario Lang (mlang@delysid.org), +;; Alex Schroeder (alex@gnu.org) +;; Andreas Fuchs (afs@void.at) +;; Gergely Nagy (algernon@midgard.debian.net) +;; David Edmondson (dme@dme.org) +;; Maintainer: Mario Lang (mlang@delysid.org) +;; Keywords: IRC, chat, client, Internet + +;; 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 2, 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; see the file COPYING. If not, write to the +;; Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, +;; Boston, MA 02110-1301, USA. + +;;; Commentary: + +;; ERC is an IRC client for Emacs. + +;; For more information, see the following URLs: +;; * http://sf.net/projects/erc/ +;; * http://www.emacswiki.org/cgi-bin/wiki.pl?EmacsIRCClient + +;; Jul-26-2001. erc.el is now in CVS on SourceForge. I invite everyone +;; who wants to hack it to contact me <mlang@delysid.org> in order to +;; get write access on the CVS. + +;; Installation: + +;; Put erc.el in your load-path, and put (require 'erc) in your .emacs. + +;; Configuration: + +;; Use M-x customize-group RET erc RET to get an overview +;; of all the variables you can tweak. + +;; Usage: + +;; To connect to an IRC server, do +;; +;; M-x erc-select RET +;; +;; After you are connected to a server, you can use C-h m or have a look at +;; the IRC menu. + +;;; History: +;; + +;;; Code: + +(defconst erc-version-string "Version 5.1 (Emacs 22)" + "ERC version. This is used by function `erc-version'.") + +(eval-when-compile (require 'cl)) +(require 'font-lock) +(require 'pp) +(require 'thingatpt) +(require 'erc-compat) +(require 'erc-menu) + +(defvar erc-official-location + "http://erc.sf.net (comments mailto://mlang@delysid.org)" + "Location of the ERC client on the Internet.") + +(defgroup erc nil + "Emacs Internet Relay Chat client." + :link '(url-link "http://www.emacswiki.org/cgi-bin/wiki.pl?EmacsIRCClient") + :prefix "erc-" + :group 'applications) + +(defgroup erc-buffers nil + "Creating new ERC buffers" + :group 'erc) + +(defgroup erc-display nil + "Settings for how various things are displayed" + :group 'erc) + +(defgroup erc-mode-line-and-header nil + "Displaying information in the mode-line and header" + :group 'erc-display) + +(defgroup erc-ignore nil + "Ignoring certain messages" + :group 'erc) + +(defgroup erc-query nil + "Using separate buffers for private discussions" + :group 'erc) + +(defgroup erc-quit-and-part nil + "Quitting and parting channels" + :group 'erc) + +(defgroup erc-paranoia nil + "Know what is sent and received; control the display of sensitive data." + :group 'erc) + +(defgroup erc-scripts nil + "Running scripts at startup and with /LOAD" + :group 'erc) + +(require 'erc-backend) + +;; compatibility with older ERC releases + +(if (fboundp 'defvaralias) + (progn + (defvaralias 'erc-announced-server-name 'erc-server-announced-name) + (erc-make-obsolete-variable 'erc-announced-server-name + 'erc-server-announced-name + "ERC 5.1") + (defvaralias 'erc-process 'erc-server-process) + (erc-make-obsolete-variable 'erc-process 'erc-server-process "ERC 5.1") + (defvaralias 'erc-default-coding-system 'erc-server-coding-system) + (erc-make-obsolete-variable 'erc-default-coding-system + 'erc-server-coding-system + "ERC 5.1")) + (message (concat "ERC: The function `defvaralias' is not bound. See the " + "NEWS file for variable name changes since ERC 5.0.4."))) + +(defalias 'erc-send-command 'erc-server-send) +(erc-make-obsolete 'erc-send-command 'erc-server-send "ERC 5.1") + +;; tunable connection and authentication parameters + +(defcustom erc-server nil + "IRC server to use. +See function `erc-compute-server' for more details on connection +parameters and authentication." + :group 'erc + :type '(choice (const nil) string)) + +(defcustom erc-port nil + "IRC port to use." + :group 'erc + :type '(choice (const nil) number string)) + +(defcustom erc-nick nil + "Nickname to use. + +Can be either a string, or a list of strings. +In the latter case, if the first nick in the list is already in use, +other nicks are tried in the list order. + +See function `erc-compute-nick' for more details on connection +parameters and authentication." + :group 'erc + :type '(choice (const nil) + (string :tag "Nickname") + (repeat string))) + +(defcustom erc-nick-uniquifier "`" + "The character to append to the nick if it is already in use." + :group 'erc + :type 'string) + +(defcustom erc-manual-set-nick-on-bad-nick-p nil + "If the nickname you chose isn't available, ERC should not automatically +attempt to set another nickname. You can manually set another nickname with +the /NICK command." + :group 'erc + :type 'boolean) + +(defcustom erc-user-full-name nil + "User full name. + +See function `erc-compute-full-name' for more details on connection +parameters and authentication." + :group 'erc + :type '(choice (const nil) string function) + :set (lambda (sym val) + (if (functionp val) + (set sym (funcall val)) + (set sym val)))) + +(defvar erc-password nil + "ERC password to use in authentication (not necessary).") + +(defcustom erc-user-mode nil + "Initial user modes to be set after a connection is established." + :group 'erc + :type '(choice (const nil) string function)) + + +(defcustom erc-prompt-for-password t + "Asks before using the default password, or whether to enter a new one." + :group 'erc + :type 'boolean) + +(defcustom erc-warn-about-blank-lines t + "Warn the user if they attempt to send a blank line." + :group 'erc + :type 'boolean) + +(defcustom erc-send-whitespace-lines nil + "If set to non-nil, send lines consisting of only whitespace." + :group 'erc + :type 'boolean) + +(defcustom erc-hide-prompt nil + "If non-nil, do not display the prompt for commands. + +\(A command is any input starting with a '/'). + +See also the variables `erc-prompt' and `erc-command-indicator'." + :group 'erc-display + :type 'boolean) + +;; tunable GUI stuff + +(defcustom erc-show-my-nick t + "If non-nil, display one's own nickname when sending a message. + +If non-nil, \"<nickname>\" will be shown. +If nil, only \"> \" will be shown." + :group 'erc-display + :type 'boolean) + +(define-widget 'erc-message-type 'set + "A set of standard IRC Message types." + :args '((const "JOIN") + (const "KICK") + (const "NICK") + (const "PART") + (const "QUIT") + (const "MODE") + (repeat :inline t :tag "Others" (string :tag "IRC Message Type")))) + +(defcustom erc-hide-list nil + "*List of IRC type messages to hide. +A typical value would be '(\"JOIN\" \"PART\" \"QUIT\")." + :group 'erc-ignore + :type 'erc-message-type) + +(defvar erc-session-password nil + "The password used for the current session.") +(make-variable-buffer-local 'erc-session-password) + +(defcustom erc-disconnected-hook nil + "Run this hook with arguments (NICK IP REASON) when disconnected. +This happens before automatic reconnection. Note, that +`erc-server-QUIT-functions' might not be run when we disconnect, +simply because we do not necessarily receive the QUIT event." + :group 'erc-hooks + :type 'hook) + +(defcustom erc-complete-functions nil + "These functions get called when the user hits TAB in ERC. +Each function in turn is called until one returns non-nil to +indicate it has handled the input." + :group 'erc-hooks + :type 'hook) + +(defcustom erc-join-hook nil + "Hook run when we join a channel. Hook functions are called +without arguments, with the current buffer set to the buffer of +the new channel. + +See also `erc-server-JOIN-functions', `erc-part-hook'." + :group 'erc-hooks + :type 'hook) + +(defcustom erc-quit-hook nil + "Hook run when processing a quit command directed at our nick. + +The hook receives one argument, the current PROCESS. +See also `erc-server-QUIT-functions' and `erc-disconnected-hook'." + :group 'erc-hooks + :type 'hook) + +(defcustom erc-part-hook nil + "Hook run when processing a PART message directed at our nick. + +The hook receives one argument, the current BUFFER. +See also `erc-server-QUIT-functions', `erc-quit-hook' and +`erc-disconnected-hook'." + :group 'erc-hooks + :type 'hook) + +(defcustom erc-kick-hook nil + "Hook run when processing a KICK message directed at our nick. + +The hook receives one argument, the current BUFFER. +See also `erc-server-PART-functions' and `erc-part-hook'." + :group 'erc-hooks + :type 'hook) + +(defcustom erc-nick-changed-functions nil + "List of functions run when your nick was successfully changed. + +Each function should accept two arguments, NEW-NICK and OLD-NICK." + :group 'erc-hooks + :type 'hook) + +(defcustom erc-connect-pre-hook '(erc-initialize-log-marker) + "Hook called just before `erc' calls `erc-connect'. +Functions are run in the buffer-to-be." + :group 'erc-hooks + :type 'hook) + + +(defvar erc-channel-users nil + "A hash table of members in the current channel, which +associates nicknames with cons cells of the form: +\(USER . MEMBER-DATA) where USER is a pointer to an +erc-server-user struct, and MEMBER-DATA is a pointer to an +erc-channel-user struct.") +(make-variable-buffer-local 'erc-channel-users) + +(defvar erc-server-users nil + "A hash table of users on the current server, which associates +nicknames with erc-server-user struct instances.") +(make-variable-buffer-local 'erc-server-users) + +(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)) + +(defstruct (erc-server-user (:type vector) :named) + ;; User data + nickname host login full-name info + ;; Buffers + ;; + ;; This is an alist of the form (BUFFER . CHANNEL-DATA), where + ;; CHANNEL-DATA is either nil or an erc-channel-user struct. + (buffers nil) + ) + +(defstruct (erc-channel-user (:type vector) :named) + op voice + ;; Last message time (in the form of the return value of + ;; (current-time) + ;; + ;; This is useful for ordered name completion. + (last-message-time nil)) + +(defsubst erc-get-channel-user (nick) + "Finds the (USER . CHANNEL-DATA) element corresponding to NICK +in the current buffer's `erc-channel-users' hash table." + (gethash (erc-downcase nick) erc-channel-users)) + +(defsubst erc-get-server-user (nick) + "Finds the USER corresponding to NICK in the current server's +`erc-server-users' hash table." + (with-current-buffer (process-buffer erc-server-process) + (gethash (erc-downcase nick) erc-server-users))) + +(defsubst erc-add-server-user (nick user) + "This function is for internal use only. + +Adds USER with nickname NICK to the `erc-server-users' hash table." + (with-current-buffer (process-buffer erc-server-process) + (puthash (erc-downcase nick) user erc-server-users))) + +(defsubst erc-remove-server-user (nick) + "This function is for internal use only. + +Removes the user with nickname NICK from the `erc-server-users' +hash table. This user is not removed from the +`erc-channel-users' lists of other buffers. + +See also: `erc-remove-user'." + (with-current-buffer (process-buffer erc-server-process) + (remhash (erc-downcase nick) erc-server-users))) + +(defun erc-change-user-nickname (user new-nick) + "This function is for internal use only. + +Changes the nickname of USER to NEW-NICK in the +`erc-server-users' hash table. The `erc-channel-users' lists of +other buffers are also changed." + (let ((nick (erc-server-user-nickname user))) + (setf (erc-server-user-nickname user) new-nick) + (with-current-buffer (process-buffer erc-server-process) + (remhash (erc-downcase nick) erc-server-users) + (puthash (erc-downcase new-nick) user erc-server-users)) + (dolist (buf (erc-server-user-buffers user)) + (if (buffer-live-p buf) + (with-current-buffer buf + (let ((cdata (erc-get-channel-user nick))) + (remhash (erc-downcase nick) erc-channel-users) + (puthash (erc-downcase new-nick) cdata + erc-channel-users))))))) + +(defun erc-remove-channel-user (nick) + "This function is for internal use only. + +Removes the user with nickname NICK from the `erc-channel-users' +list for this channel. If this user is not in the +`erc-channel-users' list of any other buffers, the user is also +removed from the server's `erc-server-users' list. + +See also: `erc-remove-server-user' and `erc-remove-user'." + (let ((channel-data (erc-get-channel-user nick))) + (when channel-data + (let ((user (car channel-data))) + (setf (erc-server-user-buffers user) + (delq (current-buffer) + (erc-server-user-buffers user))) + (remhash (erc-downcase nick) erc-channel-users) + (if (null (erc-server-user-buffers user)) + (erc-remove-server-user nick)))))) + +(defun erc-remove-user (nick) + "This function is for internal use only. + +Removes the user with nickname NICK from the `erc-server-users' +list as well as from all `erc-channel-users' lists. + +See also: `erc-remove-server-user' and +`erc-remove-channel-user'." + (let ((user (erc-get-server-user nick))) + (when user + (let ((buffers (erc-server-user-buffers user))) + (dolist (buf buffers) + (if (buffer-live-p buf) + (with-current-buffer buf + (remhash (erc-downcase nick) erc-channel-users) + (run-hooks 'erc-channel-members-changed-hook))))) + (erc-remove-server-user nick)))) + +(defun erc-remove-channel-users () + "This function is for internal use only. + +Removes all users in the current channel. This is called by +`erc-server-PART' and `erc-server-QUIT'." + (when (and erc-server-connected + (erc-server-process-alive) + (hash-table-p erc-channel-users)) + (maphash (lambda (nick cdata) + (erc-remove-channel-user nick)) + erc-channel-users) + (clrhash erc-channel-users))) + +(defsubst erc-channel-user-op-p (nick) + "Return `t' if NICK is an operator in the current channel." + (and nick + (hash-table-p erc-channel-users) + (let ((cdata (erc-get-channel-user nick))) + (and cdata (cdr cdata) + (erc-channel-user-op (cdr cdata)))))) + +(defsubst erc-channel-user-voice-p (nick) + "Return `t' if NICK has voice in the current channel." + (and nick + (hash-table-p erc-channel-users) + (let ((cdata (erc-get-channel-user nick))) + (and cdata (cdr cdata) + (erc-channel-user-voice (cdr cdata)))))) + +(defun erc-get-channel-user-list () + "Returns a list of users in the current channel. Each element +of the list is of the form (USER . CHANNEL-DATA), where USER is +an erc-server-user struct, and CHANNEL-DATA is either `nil' or an +erc-channel-user struct. + +See also: `erc-sort-channel-users-by-activity'" + (let (users) + (if (hash-table-p erc-channel-users) + (maphash (lambda (nick cdata) + (setq users (cons cdata users))) + erc-channel-users)) + users)) + +(defun erc-get-server-nickname-list () + "Returns a list of known nicknames on the current server." + (if (erc-server-process-alive) + (with-current-buffer (erc-server-buffer) + (let (nicks) + (when (hash-table-p erc-server-users) + (maphash (lambda (n user) + (setq nicks + (cons (erc-server-user-nickname user) + nicks))) + erc-server-users) + nicks))))) + +(defun erc-get-channel-nickname-list () + "Returns a list of known nicknames on the current channel." + (let (nicks) + (when (hash-table-p erc-channel-users) + (maphash (lambda (n cdata) + (setq nicks + (cons (erc-server-user-nickname (car cdata)) + nicks))) + erc-channel-users) + nicks))) + +(defun erc-get-server-nickname-alist () + "Returns an alist of known nicknames on the current server." + (if (erc-server-process-alive) + (with-current-buffer (erc-server-buffer) + (let (nicks) + (when (hash-table-p erc-server-users) + (maphash (lambda (n user) + (setq nicks + (cons (cons (erc-server-user-nickname user) nil) + nicks))) + erc-server-users) + nicks))))) + +(defun erc-get-channel-nickname-alist () + "Returns an alist of known nicknames on the current channel." + (let (nicks) + (when (hash-table-p erc-channel-users) + (maphash (lambda (n cdata) + (setq nicks + (cons (cons (erc-server-user-nickname (car cdata)) nil) + nicks))) + erc-channel-users) + nicks))) + +(defun erc-sort-channel-users-by-activity (list) + "Sorts LIST such that users which have spoken most recently are +listed first. LIST must be of the form (USER . CHANNEL-DATA). + +See also: `erc-get-channel-user-list'." + (sort list + (lambda (x y) + (when (and + (cdr x) (cdr y)) + (let ((tx (erc-channel-user-last-message-time (cdr x))) + (ty (erc-channel-user-last-message-time (cdr y)))) + (if tx + (if ty + (time-less-p ty tx) + t) + nil)))))) + +(defun erc-sort-channel-users-alphabetically (list) + "Sort LIST so that users' nicknames are in alphabetical order. +LIST must be of the form (USER . CHANNEL-DATA). + +See also: `erc-get-channel-user-list'." + (sort list + (lambda (x y) + (when (and + (cdr x) (cdr y)) + (let ((nickx (downcase (erc-server-user-nickname (car x)))) + (nicky (downcase (erc-server-user-nickname (car y))))) + (if nickx + (if nicky + (string-lessp nickx nicky) + t) + nil)))))) + +(defvar erc-channel-topic nil + "A topic string for the channel. Should only be used in channel-buffers.") +(make-variable-buffer-local 'erc-channel-topic) + +(defvar erc-channel-modes nil + "List of strings representing channel modes. +E.g. '(\"i\" \"m\" \"s\" \"b Quake!*@*\") +\(not sure the ban list will be here, but why not)") +(make-variable-buffer-local 'erc-channel-modes) + +(defvar erc-insert-marker nil + "The place where insertion of new text in erc buffers should happen.") +(make-variable-buffer-local 'erc-insert-marker) + +(defvar erc-input-marker nil + "The marker where input should be inserted.") +(make-variable-buffer-local 'erc-input-marker) + +(defun erc-string-no-properties (string) + "Return a copy of STRING will all text-properties removed." + (let ((newstring (copy-sequence string))) + (set-text-properties 0 (length newstring) nil newstring) + newstring)) + +(defcustom erc-prompt "ERC>" + "Prompt used by ERC. Trailing whitespace is not required." + :group 'erc-display + :type '(choice string function)) + +(defun erc-prompt () + "Return the input prompt as a string. + +See also the variable `erc-prompt'." + (let ((prompt (if (functionp erc-prompt) + (funcall erc-prompt) + erc-prompt))) + (if (> (length prompt) 0) + (concat prompt " ") + prompt))) + +(defcustom erc-command-indicator nil + "Indicator used by ERC for showing commands. + +If non-nil, this will be used in the ERC buffer to indicate +commands (i.e., input starting with a '/'). + +If nil, the prompt will be constructed from the variable `erc-prompt'." + :group 'erc-display + :type '(choice (const nil) string function)) + +(defun erc-command-indicator () + "Return the command indicator prompt as a string. + +This only has any meaning if the variable `erc-command-indicator' is non-nil." + (and erc-command-indicator + (let ((prompt (if (functionp erc-command-indicator) + (funcall erc-command-indicator) + erc-command-indicator))) + (if (> (length prompt) 0) + (concat prompt " ") + prompt)))) + +(defcustom erc-notice-prefix "*** " + "*Prefix for all notices." + :group 'erc-display + :type 'string) + +(defcustom erc-notice-highlight-type 'all + "*Determines how to highlight notices. +See `erc-notice-prefix'. + +The following values are allowed: + + 'prefix - highlight notice prefix only + 'all - highlight the entire notice + +Any other value disables notice's highlighting altogether." + :group 'erc-display + :type '(choice (const :tag "highlight notice prefix only" prefix) + (const :tag "highlight the entire notice" all) + (const :tag "don't highlight notices at all" nil))) + +(defcustom erc-echo-notice-hook nil + "*Specifies a list of functions to call to echo a private +notice. Each function is called with four arguments, the string +to display, the parsed server message, the target buffer (or +nil), and the sender. The functions are called in order, until a +function evaluates to non-nil. These hooks are called after +those specified in `erc-echo-notice-always-hook'. + +See also: `erc-echo-notice-always-hook', +`erc-echo-notice-in-default-buffer', +`erc-echo-notice-in-target-buffer', +`erc-echo-notice-in-minibuffer', +`erc-echo-notice-in-server-buffer', +`erc-echo-notice-in-active-non-server-buffer', +`erc-echo-notice-in-active-buffer', +`erc-echo-notice-in-user-buffers', +`erc-echo-notice-in-user-and-target-buffers', +`erc-echo-notice-in-first-user-buffer'" + :group 'erc-hooks + :type 'hook + :options '(erc-echo-notice-in-default-buffer + erc-echo-notice-in-target-buffer + erc-echo-notice-in-minibuffer + erc-echo-notice-in-server-buffer + erc-echo-notice-in-active-non-server-buffer + erc-echo-notice-in-active-buffer + erc-echo-notice-in-user-buffers + erc-echo-notice-in-user-and-target-buffers + erc-echo-notice-in-first-user-buffer)) + +(defcustom erc-echo-notice-always-hook + '(erc-echo-notice-in-default-buffer) + "*Specifies a list of functions to call to echo a private +notice. Each function is called with four arguments, the string +to display, the parsed server message, the target buffer (or +nil), and the sender. The functions are called in order, and all +functions are called. These hooks are called before those +specified in `erc-echo-notice-hook'. + +See also: `erc-echo-notice-hook', +`erc-echo-notice-in-default-buffer', +`erc-echo-notice-in-target-buffer', +`erc-echo-notice-in-minibuffer', +`erc-echo-notice-in-server-buffer', +`erc-echo-notice-in-active-non-server-buffer', +`erc-echo-notice-in-active-buffer', +`erc-echo-notice-in-user-buffers', +`erc-echo-notice-in-user-and-target-buffers', +`erc-echo-notice-in-first-user-buffer'" + :group 'erc-hooks + :type 'hook + :options '(erc-echo-notice-in-default-buffer + erc-echo-notice-in-target-buffer + erc-echo-notice-in-minibuffer + erc-echo-notice-in-server-buffer + erc-echo-notice-in-active-non-server-buffer + erc-echo-notice-in-active-buffer + erc-echo-notice-in-user-buffers + erc-echo-notice-in-user-and-target-buffers + erc-echo-notice-in-first-user-buffer)) + +;; other tunable parameters + +(defcustom erc-whowas-on-nosuchnick nil + "*If non-nil, do a whowas on a nick if no such nick." + :group 'erc + :type 'boolean) + +(defcustom erc-verbose-server-ping nil + "*If non-nil, show every time you get a PING or PONG from the server." + :group 'erc-paranoia + :type 'boolean) + +(defcustom erc-public-away-p nil + "*Let others know you are back when you are no longer marked away. +This happens in this form: +* <nick> is back (gone for <time>) + +Many consider it impolite to do so automatically." + :group 'erc + :type 'boolean) + +(defcustom erc-away-nickname nil + "*The nickname to take when you are marked as being away." + :group 'erc + :type '(choice (const nil) + string)) + +(defcustom erc-paranoid nil + "If non-nil, then all incoming CTCP requests will be shown." + :group 'erc-paranoia + :type 'boolean) + +(defcustom erc-disable-ctcp-replies nil + "Disable replies to CTCP requests that require a reply. +If non-nil, then all incoming CTCP requests that normally require +an automatic reply (like VERSION or PING) will be ignored. Good to +set if some hacker is trying to flood you away." + :group 'erc-paranoia + :type 'boolean) + +(defcustom erc-anonymous-login t + "Be paranoid, don't give away your machine name." + :group 'erc-paranoia + :type 'boolean) + +(defcustom erc-prompt-for-channel-key nil + "Prompt for channel key when using `erc-join-channel' interactively" + :group 'erc + :type 'boolean) + +(defcustom erc-email-userid "user" + "Use this as your email user ID." + :group 'erc + :type 'string) + +(defcustom erc-ignore-list nil + "*List of regexps matching user identifiers to ignore. + +A user identifier has the form \"nick!login@host\". If an +identifier matches, the message from the person will not be +processed." + :group 'erc-ignore + :type '(repeat regexp)) +(make-variable-buffer-local 'erc-ignore-list) + +(defcustom erc-ignore-reply-list nil + "*List of regexps matching user identifiers to ignore completely. + +This differs from `erc-ignore-list' in that it also ignores any +messages directed at the user. + +A user identifier has the form \"nick!login@host\". + +If an identifier matches, or a message is addressed to a nick +whose identifier matches, the message will not be processed. + +CAVEAT: ERC doesn't know about the user and host of anyone who +was already in the channel when you joined, but never said +anything, so it won't be able to match the user and host of those +people. You can update the ERC internal info using /WHO *." + :group 'erc-ignore + :type '(repeat regexp)) + +(defvar erc-flood-protect t + "*If non-nil, flood protection is enabled. +Flooding is sending too much information to the server in too +short of an interval, which may cause the server to terminate the +connection. + +See `erc-server-flood-margin' for other flood-related parameters.") + +;; Script parameters + +(defcustom erc-startup-file-list + '("~/.ercrc.el" "~/.ercrc" ".ercrc.el" ".ercrc") + "List of files to try for a startup script. +The first existent and readable one will get executed. + +If the filename ends with `.el' it is presumed to be an emacs-lisp +script and it gets (load)ed. Otherwise is is treated as a bunch of +regular IRC commands" + :group 'erc-scripts + :type '(repeat file)) + +(defcustom erc-script-path nil + "List of directories to look for a script in /load command. +The script is first searched in the current directory, then in each +directory in the list." + :group 'erc-scripts + :type '(repeat directory)) + +(defcustom erc-script-echo t + "*If not-NIL, echo the IRC script commands locally." + :group 'erc-scripts + :type 'boolean) + +(defvar erc-last-saved-position nil + "A marker containing the position the current buffer was last saved at.") +(make-variable-buffer-local 'erc-last-saved-position) + +(defcustom erc-kill-buffer-on-part nil + "Kill the channel buffer on PART. +This variable should probably stay nil, as ERC can reuse buffers if +you rejoin them later." + :group 'erc-quit-and-part + :type 'boolean) + +(defcustom erc-kill-queries-on-quit nil + "Kill all query (also channel) buffers of this server on QUIT. +See the variable `erc-kill-buffer-on-part' for details." + :group 'erc-quit-and-part + :type 'boolean) + +(defcustom erc-kill-server-buffer-on-quit nil + "Kill the server buffer of the process on QUIT." + :group 'erc-quit-and-part + :type 'boolean) + +(defcustom erc-quit-reason-various-alist nil + "Alist of possible arguments to the /quit command. + +Each element has the form: + (REGEXP RESULT) + +If REGEXP matches the argument to /quit, then its relevant RESULT +will be used. RESULT may be either a string, or a function. If +a function, it should return the quit message as a string. + +If no elements match, then the empty string is used. + +As an example: + (setq erc-quit-reason-various-alist + '((\"zippy\" erc-quit-reason-zippy) + (\"xmms\" dme:now-playing) + (\"version\" erc-quit-reason-normal) + (\"home\" \"Gone home !\") + (\"\" \"Default Reason\"))) +If the user types \"/quit zippy\", then a Zippy the Pinhead quotation +will be used as the quit message." + :group 'erc-quit-and-part + :type '(repeat (list regexp (choice (string) (function))))) + +(defcustom erc-part-reason-various-alist nil + "Alist of possible arguments to the /part command. + +Each element has the form: + (REGEXP RESULT) + +If REGEXP matches the argument to /part, then its relevant RESULT +will be used. RESULT may be either a string, or a function. If +a function, it should return the part message as a string. + +If no elements match, then the empty string is used. + +As an example: + (setq erc-part-reason-various-alist + '((\"zippy\" erc-part-reason-zippy) + (\"xmms\" dme:now-playing) + (\"version\" erc-part-reason-normal) + (\"home\" \"Gone home !\") + (\"\" \"Default Reason\"))) +If the user types \"/part zippy\", then a Zippy the Pinhead quotation +will be used as the part message." + :group 'erc-quit-and-part + :type '(repeat (list regexp (choice (string) (function))))) + +(defcustom erc-quit-reason 'erc-quit-reason-normal + "*A function which returns the reason for quitting. + +The function is passed a single argument, the string typed by the +user after \"/quit\"." + :group 'erc-quit-and-part + :type '(choice (const erc-quit-reason-normal) + (const erc-quit-reason-zippy) + (const erc-quit-reason-various) + (symbol))) + +(defcustom erc-part-reason 'erc-part-reason-normal + "A function which returns the reason for parting a channel. + +The function is passed a single argument, the string typed by the +user after \"/PART\"." + :group 'erc-quit-and-part + :type '(choice (const erc-part-reason-normal) + (const erc-part-reason-zippy) + (const erc-part-reason-various) + (symbol))) + +(defvar erc-grab-buffer-name "*erc-grab*" + "The name of the buffer created by `erc-grab-region'.") + +;; variables available for IRC scripts + +(defvar erc-user-information "ERC User" + "USER_INFORMATION IRC variable.") + +;; Hooks + +(defgroup erc-hooks nil + "Hook variables for fancy customizations of ERC." + :group 'erc) + +(defcustom erc-mode-hook nil + "Hook run after `erc-mode' setup is finished." + :group 'erc-hooks + :type 'hook + :options '(erc-add-scroll-to-bottom)) + +(defcustom erc-timer-hook nil + "Put functions which should get called more or less periodically here. +The idea is that servers always play ping pong with the client, and so there +is no need for any idle-timer games with Emacs." + :group 'erc-hooks + :type 'hook) + +(defcustom erc-insert-pre-hook nil + "Hook called first when some text is inserted through `erc-display-line'. +It gets called with one argument, STRING. +To be able to modify the inserted text, use `erc-insert-modify-hook' instead. +Filtering functions can set `erc-insert-this' to nil to avoid +display of that particular string at all." + :group 'erc-hooks + :type 'hook) + +(defcustom erc-send-pre-hook nil + "Hook called first when some text is sent through `erc-send-current-line'. +It gets called with one argument, STRING. + +To change the text that will be sent, set the variable STR which is +used in `erc-send-current-line'. + +To change the text inserted into the buffer without changing the text +that will be sent, use `erc-send-modify-hook' instead. + +Filtering functions can set `erc-send-this' to nil to avoid sending of +that particular string at all and `erc-insert-this' to prevent +inserting that particular string into the buffer. + +Note that it's useless to set `erc-send-this' to nil and +`erc-insert-this' to t. ERC is sane enough to not insert the text +anyway." + :group 'erc-hooks + :type 'hook) + +(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 +if they wish to avoid insertion of a particular string.") + +(defvar erc-send-this t + "Send the text to the target or not. +Functions on `erc-send-pre-hook' can set this variable to nil +if they wish to avoid sending of a particular string.") + +(defcustom erc-insert-modify-hook () + "Insertion hook for functions that will change the text's appearance. +This hook is called just after `erc-insert-pre-hook' when the value +of `erc-insert-this' is t. +While this hook is run, narrowing is in effect and `current-buffer' is +the buffer where the text got inserted. One possible value to add here +is `erc-fill'." + :group 'erc-hooks + :type 'hook) + +(defcustom erc-insert-post-hook nil + "This hook is called just after `erc-insert-modify-hook'. +At this point, all modifications from prior hook functions are done." + :group 'erc-hooks + :type 'hook + :options '(erc-truncate-buffer + erc-make-read-only + erc-save-buffer-in-logs)) + +(defcustom erc-send-modify-hook nil + "Sending hook for functions that will change the text's appearance. +This hook is called just after `erc-send-pre-hook' when the values +of `erc-send-this' and `erc-insert-this' are both t. +While this hook is run, narrowing is in effect and `current-buffer' is +the buffer where the text got inserted. + +Note that no function in this hook can change the appearance of the +text that is sent. Only changing the sent text's appearance on the +sending user's screen is possible. One possible value to add here +is `erc-fill'." + :group 'erc-hooks + :type 'hook) + +(defcustom erc-send-post-hook nil + "This hook is called just after `erc-send-modify-hook'. +At this point, all modifications from prior hook functions are done. +NOTE: The functions on this hook are called _before_ sending a command +to the server. + +This function is called with narrowing, ala `erc-send-modify-hook'" + :group 'erc-hooks + :type 'hook + :options '(erc-make-read-only)) + +(defcustom erc-send-completed-hook + (when (featurep 'emacspeak) + (list (byte-compile + (lambda (str) + (emacspeak-auditory-icon 'select-object))))) + "Hook called after a message has been parsed by ERC. + +The single argument to the functions is the unmodified string +which the local user typed." + :group 'erc-hooks + :type 'hook) +;; mode-specific tables + +(defvar erc-mode-syntax-table + (let ((syntax-table (make-syntax-table))) + (modify-syntax-entry ?\" ". " syntax-table) + (modify-syntax-entry ?\\ ". " syntax-table) + (modify-syntax-entry ?' "w " syntax-table) + ;; Make dabbrev-expand useful for nick names + (modify-syntax-entry ?< "." syntax-table) + (modify-syntax-entry ?> "." syntax-table) + syntax-table) + "Syntax table used while in ERC mode.") + +(defvar erc-mode-abbrev-table nil + "Abbrev table used while in ERC mode.") +(define-abbrev-table 'erc-mode-abbrev-table ()) + +(defvar erc-mode-map + (let ((map (make-sparse-keymap))) + (define-key map "\C-m" 'erc-send-current-line) + (define-key map "\C-a" 'erc-bol) + (define-key map [home] 'erc-bol) + (define-key map "\C-c\C-a" 'erc-bol) + (define-key map "\C-c\C-b" 'erc-iswitchb) + (define-key map "\C-c\C-c" 'erc-toggle-interpret-controls) + (define-key map "\C-c\C-d" 'erc-input-action) + (define-key map "\C-c\C-e" 'erc-toggle-ctcp-autoresponse) + (define-key map "\C-c\C-f" 'erc-toggle-flood-control) + (define-key map "\C-c\C-i" 'erc-invite-only-mode) + (define-key map "\C-c\C-j" 'erc-join-channel) + (define-key map "\C-c\C-n" 'erc-channel-names) + (define-key map "\C-c\C-o" 'erc-get-channel-mode-from-keypress) + (define-key map "\C-c\C-p" 'erc-part-from-channel) + (define-key map "\C-c\C-q" 'erc-quit-server) + (define-key map "\C-c\C-r" 'erc-remove-text-properties-region) + (define-key map "\C-c\C-t" 'erc-set-topic) + (define-key map "\C-c\C-u" 'erc-kill-input) + (define-key map "\M-\t" 'ispell-complete-word) + (define-key map "\t" 'erc-complete-word) + + ;; Suppress `font-lock-fontify-block' key binding since it + ;; destroys face properties. + (if (fboundp 'command-remapping) + (define-key map [remap font-lock-fontify-block] 'undefined) + (substitute-key-definition + 'font-lock-fontify-block 'undefined map global-map)) + + map) + "ERC keymap.") + +;; Faces + +; Honestly, I have a horrible sense of color and the "defaults" below +; are supposed to be really bad. But colors ARE required in IRC to +; convey different parts of conversation. If you think you know better +; defaults - send them to me. + +;; Now colors are a bit nicer, at least to my eyes. +;; You may still want to change them to better fit your background.-- S.B. + +(defgroup erc-faces nil + "Faces for ERC." + :group 'erc) + +(defface erc-default-face '((t)) + "ERC default face." + :group 'erc-faces) + +(defface erc-direct-msg-face '((t (:foreground "IndianRed"))) + "ERC face used for messages you receive in the main erc buffer." + :group 'erc-faces) + +(defface erc-input-face '((t (:foreground "brown"))) + "ERC face used for your input." + :group 'erc-faces) + +(defface erc-prompt-face + '((t (:bold t :foreground "Black" :background"lightBlue2"))) + "ERC face for the prompt." + :group 'erc-faces) + +(defface erc-command-indicator-face + '((t (:bold t))) + "ERC face for the command indicator. +See the variable `erc-command-indicator'." + :group 'erc-faces) + +(defface erc-notice-face '((t (:bold t :foreground "SlateBlue"))) + "ERC face for notices." + :group 'erc-faces) + +(defface erc-action-face '((t (:bold t))) + "ERC face for actions generated by /ME." + :group 'erc-faces) + +(defface erc-error-face '((t (:foreground "red"))) + "ERC face for errors." + :group 'erc-faces) + +(defface erc-nick-default-face '((t (:bold t))) + "ERC nickname default face." + :group 'erc-faces) + +(defface erc-nick-msg-face '((t (:bold t :foreground "IndianRed"))) + "ERC nickname face for private messages." + :group 'erc-faces) + +;; Debugging support + +(defvar erc-log-p nil + "When set to t, generate debug messages in a separate debug buffer.") + +(defvar erc-debug-log-file (expand-file-name "ERC.debug") + "Debug log file name.") + +(defvar erc-dbuf nil) +(make-variable-buffer-local 'erc-dbuf) + +(defmacro define-erc-module (name alias doc enable-body disable-body + &optional local-p) + "Define a new minor mode using ERC conventions. +Symbol NAME is the name of the module. +Symbol ALIAS is the alias to use, or nil. +DOC is the documentation string to use for the minor mode. +ENABLE-BODY is a list of expressions used to enable the mode. +DISABLE-BODY is a list of expressions used to disable the mode. +If LOCAL-P is non-nil, the mode will be created as a buffer-local +mode. Rather than a global one. + +This will define a minor mode called erc-NAME-mode, possibly +an alias erc-ALIAS-mode, as well as the helper functions +erc-NAME-enable, and erc-NAME-disable. + +Example: + + ;;;###autoload (autoload 'erc-replace-mode \"erc-replace\") + (define-erc-module replace nil + \"This mode replaces incoming text according to `erc-replace-alist'.\" + ((add-hook 'erc-insert-modify-hook + 'erc-replace-insert)) + ((remove-hook 'erc-insert-modify-hook + 'erc-replace-insert)))" + (let* ((sn (symbol-name name)) + (mode (intern (format "erc-%s-mode" (downcase sn)))) + (group (intern (format "erc-%s" (downcase sn)))) + (enable (intern (format "erc-%s-enable" (downcase sn)))) + (disable (intern (format "erc-%s-disable" (downcase sn))))) + `(progn + (erc-define-minor-mode + ,mode + ,(format "Toggle ERC %S mode. +With arg, turn ERC %S mode on if and only if arg is positive. +%s" name name doc) + nil nil nil + :global ,(not local-p) :group (quote ,group) + (if ,mode + (,enable) + (,disable))) + (defun ,enable () + ,(format "Enable ERC %S mode." + name) + (interactive) + (add-to-list 'erc-modules (quote ,name)) + (setq ,mode t) + ,@enable-body) + (defun ,disable () + ,(format "Disable ERC %S mode." + name) + (interactive) + (setq erc-modules (delq (quote ,name) erc-modules)) + (setq ,mode nil) + ,@disable-body) + ,(when (and alias (not (eq name alias))) + `(defalias + (quote + ,(intern + (format "erc-%s-mode" + (downcase (symbol-name alias))))) + (quote + ,mode)))))) + +(put 'define-erc-module 'doc-string-elt 3) + +(defun erc-once-with-server-event (event &rest forms) + "Execute FORMS the next time EVENT occurs in the `current-buffer'. + +You should make sure that `current-buffer' is a server buffer. + +This function temporarily adds a function to EVENT's hook to +execute FORMS. After FORMS are run, the function is removed from +EVENT's hook. The last expression of FORMS should be either nil +or t. nil indicates that the other functions on EVENT's hook +should be run too, and t indicates that other functions should +not be run. + +Please be sure to use this function in server-buffers. In +channel-buffers it may not work at all, as it uses the LOCAL +argument of `add-hook' and `remove-hook' to ensure multiserver +capabilities." + (unless (erc-server-buffer-p) + (error + "You should only run `erc-once-with-server-event' in a server buffer")) + (let ((fun (erc-gensym)) + (hook (erc-get-hook event))) + (put fun 'erc-original-buffer (current-buffer)) + (fset fun `(lambda (proc parsed) + (with-current-buffer (get ',fun 'erc-original-buffer) + (remove-hook ',hook ',fun t)) + (fmakunbound ',fun) + ,@forms)) + (add-hook hook fun nil t) + fun)) + +(defun erc-once-with-server-event-global (event &rest forms) + "Execute FORMS the next time EVENT occurs in any server buffer. + +This function temporarily prepends a function to EVENT's hook to +execute FORMS. After FORMS are run, the function is removed from +EVENT's hook. The last expression of FORMS should be either nil +or t. nil indicates that the other functions on EVENT's hook +should be run too, and t indicates that other functions should +not be run. + +When FORMS execute, the current buffer is the server buffer associated with the +connection over which the data was received that triggered EVENT." + (let ((fun (erc-gensym)) + (hook (erc-get-hook event))) + (fset fun `(lambda (proc parsed) + (remove-hook ',hook ',fun) + (fmakunbound ',fun) + ,@forms)) + (add-hook hook fun nil nil) + fun)) + +(defmacro erc-log (string) + "Logs STRING if logging is on (see `erc-log-p')." + `(when erc-log-p + (erc-log-aux ,string))) + +(defun erc-server-buffer () + "Return the server buffer for the current buffer's process. +The buffer-local variable `erc-server-process' is used to find +the process buffer." + (and (erc-server-buffer-live-p) + (process-buffer erc-server-process))) + +(defun erc-server-buffer-live-p () + "Return t if the buffer associated with `erc-server-process' +has not been killed." + (and (processp erc-server-process) + (buffer-live-p (process-buffer erc-server-process)))) + +(defun erc-server-buffer-p (&optional buffer) + "Return non-nil if argument BUFFER is an ERC server buffer. + +If BUFFER is nil, the current buffer is used." + (with-current-buffer (or buffer (current-buffer)) + (and (eq major-mode 'erc-mode) + (null (erc-default-target))))) + +(defun erc-query-buffer-p (&optional buffer) + "Return non-nil if BUFFER is an ERC query buffer. +If BUFFER is nil, the current buffer is used." + (with-current-buffer (or buffer (current-buffer)) + (let ((target (erc-default-target))) + (and (eq major-mode 'erc-mode) + target + (not (memq (aref target 0) '(?# ?& ?+ ?!))))))) + +(defun erc-ison-p (nick) + "Return non-nil if NICK is online." + (interactive "sNick: ") + (with-current-buffer (erc-server-buffer) + (let ((erc-online-p 'unknown)) + (erc-once-with-server-event + 303 + `(let ((ison (split-string (aref parsed 3)))) + (setq erc-online-p (car (erc-member-ignore-case ,nick ison))) + t)) + (erc-server-send (format "ISON %s" nick)) + (while (eq erc-online-p 'unknown) (accept-process-output)) + (if (interactive-p) + (message "%s is %sonline" + (or erc-online-p nick) + (if erc-online-p "" "not ")) + erc-online-p)))) + +(defun erc-log-aux (string) + "Do the debug logging of STRING." + (let ((cb (current-buffer)) + (point 1) + (was-eob nil) + (session-buffer (erc-server-buffer))) + (if session-buffer + (progn + (set-buffer session-buffer) + (if (not (and erc-dbuf (bufferp erc-dbuf) (buffer-live-p erc-dbuf))) + (progn + (setq erc-dbuf (get-buffer-create + (concat "*ERC-DEBUG: " + erc-session-server "*"))))) + (set-buffer erc-dbuf) + (setq point (point)) + (setq was-eob (eobp)) + (goto-char (point-max)) + (insert (concat "** " string "\n")) + (if was-eob (goto-char (point-max)) + (goto-char point)) + (set-buffer cb)) + (message "ERC: ** %s" string)))) + +;; Last active buffer, to print server messages in the right place + +(defvar erc-active-buffer nil + "The current active buffer, the one where the user typed the last command. +Defaults to the server buffer, and should only be set in the +server buffer") +(make-variable-buffer-local 'erc-active-buffer) + +(defun erc-active-buffer () + "Return the value of `erc-active-buffer' for the current server. +Defaults to the server buffer." + (with-current-buffer (erc-server-buffer) erc-active-buffer)) + +(defun erc-set-active-buffer (buffer) + "Set the value of `erc-active-buffer' to BUFFER." + (cond ((erc-server-buffer) + (with-current-buffer (erc-server-buffer) + (setq erc-active-buffer buffer))) + (t (setq erc-active-buffer buffer)))) + +;; Mode activation routines + +(defun erc-mode () + "Major mode for Emacs IRC. +Special commands: + +\\{erc-mode-map} + +Turning on `erc-mode' runs the hook `erc-mode-hook'." + (kill-all-local-variables) + (use-local-map erc-mode-map) + (setq mode-name "ERC" + major-mode 'erc-mode + local-abbrev-table erc-mode-abbrev-table) + (set-syntax-table erc-mode-syntax-table) + (when (boundp 'next-line-add-newlines) + (set (make-local-variable 'next-line-add-newlines) nil)) + (setq line-move-ignore-invisible t) + (set (make-local-variable 'paragraph-separate) + (concat "\C-l\\|\\(^" (regexp-quote (erc-prompt)) "\\)")) + (set (make-local-variable 'paragraph-start) + (concat "\\(" (regexp-quote (erc-prompt)) "\\)")) + ;; Run the mode hooks + (run-hooks 'erc-mode-hook)) + +;; activation + +(defconst erc-default-server "irc.freenode.net" + "IRC server to use if it cannot be detected otherwise.") + +(defconst erc-default-port "ircd" + "IRC port to use if it cannot be detected otherwise.") + +(defcustom erc-join-buffer 'buffer + "Determines how to display the newly created IRC buffer. +'window - in another window, +'window-noselect - in another window, but don't select that one, +'frame - in another frame, +'bury - bury it in a new buffer, +any other value - in place of the current buffer" + :group 'erc-buffers + :type '(choice (const window) + (const window-noselect) + (const frame) + (const bury) + (const buffer))) + +(defcustom erc-frame-alist nil + "*Alist of frame parameters for creating erc frames. +A value of `nil means to use `default-frame-alist'." + :group 'erc-buffers + :type '(repeat (cons :format "%v" + (symbol :tag "Parameter") + (sexp :tag "Value")))) + +(defcustom erc-frame-dedicated-flag nil + "*Non-nil means the erc frames are dedicated to that buffer. +This only has effect when `erc-join-buffer' is set to `frame'." + :group 'erc-buffers + :type 'boolean) + +(defun erc-channel-p (channel) + "Return non-nil if CHANNEL seems to be an IRC channel name." + (cond ((stringp channel) + (memq (aref channel 0) '(?# ?& ?+ ?!))) + ((and (bufferp channel) (buffer-live-p channel)) + (with-current-buffer channel + (erc-channel-p (erc-default-target)))) + (t nil))) + +(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 +channels with same names on different servers, or have query buffers +open with nicks of the same name on different servers. Otherwise, +the existing buffers will be reused." + :group 'erc-buffers + :type 'boolean) + +(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 +symbol, it may have these values: +* irc -> 194 +* ircs -> 994 +* ircd -> 6667 +* ircd-dalnet -> 7000" + (cond + ((symbolp port) + (erc-normalize-port (symbol-name port))) + ((stringp port) + (let ((port-nr (string-to-number port))) + (cond + ((> port-nr 0) + port-nr) + ((string-equal port "irc") + 194) + ((string-equal port "ircs") + 994) + ((string-equal port "ircd") + 6667) + ((string-equal port "ircd-dalnet") + 7000) + (t + nil)))) + ((numberp port) + port) + (t + nil))) + +(defun erc-port-equal (a b) + "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 &optional proc) + "Create a new buffer name based on the arguments." + (when (numberp port) (setq port (number-to-string port))) + (let* ((buf-name (or target + (or (let ((name (concat server ":" port))) + (when (> (length name) 1) + name)) + ; This fallback should in fact never happen + "*erc-server-buffer*")))) + ;; 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 (and erc-reuse-buffers + (get-buffer buf-name) + (or target + (with-current-buffer (get-buffer buf-name) + (and (erc-server-buffer-p) + (not erc-server-connected)))) + (with-current-buffer (get-buffer buf-name) + (and (string= erc-session-server server) + (erc-port-equal erc-session-port port)))) + buf-name + (generate-new-buffer-name buf-name)))) + +(defun erc-get-buffer-create (server port target &optional proc) + "Create a new buffer based on the arguments." + (get-buffer-create (erc-generate-new-buffer-name server port target proc))) + + +(defun erc-member-ignore-case (string list) + "Return non-nil if STRING is a member of LIST. + +All strings are compared according to IRC protocol case rules, see +`erc-downcase'." + (setq string (erc-downcase string)) + (catch 'result + (while list + (if (string= string (erc-downcase (car list))) + (throw 'result list) (setq list (cdr list)))))) + +(defmacro erc-with-buffer (spec &rest body) + "Execute BODY in the buffer associated with SPEC. + +SPEC should have the form + + (TARGET [PROCESS]) + +If TARGET is a buffer, use it. Otherwise, use the buffer +matching TARGET in the process specified by PROCESS. + +If PROCESS is nil, use the current `erc-server-process' +See `erc-get-buffer' for details. + +See also `with-current-buffer'. + +\(fn (TARGET [PROCESS]) BODY...)" + (let ((buf (erc-gensym)) + (proc (erc-gensym)) + (target (erc-gensym)) + (process (erc-gensym))) + `(let* ((,target ,(car spec)) + (,process ,(cadr spec)) + (,buf (if (bufferp ,target) + ,target + (let ((,proc (or ,process + (and (processp erc-server-process) + erc-server-process)))) + (if (and ,target ,proc) + (erc-get-buffer ,target ,proc)))))) + (when ,buf + (with-current-buffer ,buf + ,@body))))) +(put 'erc-with-buffer 'lisp-indent-function 1) +(put 'erc-with-buffer 'edebug-form-spec '((form &optional form) body)) + +(defun erc-get-buffer (target &optional proc) + "Return the buffer matching TARGET in the process PROC. +If PROC is not supplied, all processes are searched." + (let ((downcased-target (erc-downcase target))) + (catch 'buffer + (erc-buffer-filter + (lambda () + (let ((current (erc-default-target))) + (and (stringp current) + (string-equal downcased-target (erc-downcase current)) + (throw 'buffer (current-buffer))))) + proc)))) + +(defun erc-buffer-filter (predicate &optional proc) + "Return a list of `erc-mode' buffers matching certain criteria. +PREDICATE is a function executed with each buffer, if it returns t, that buffer +is considered a valid match. + +PROC is either an `erc-server-process', identifying a certain +server connection, or nil which means all open connections." + (save-excursion + (delq + nil + (mapcar (lambda (buf) + (with-current-buffer buf + (and (eq major-mode 'erc-mode) + (or (not proc) + (eq proc erc-server-process)) + (funcall predicate) + buf))) + (buffer-list))))) + +(defun erc-buffer-list (&optional predicate proc) + "Return a list of ERC buffers. +PREDICATE is a function which executes with every buffer satisfying +the predicate. If PREDICATE is passed as nil, return a list of all ERC +buffers. If PROC is given, the buffers local variable `erc-server-process' +needs to match PROC." + (unless predicate + (setq predicate (lambda () t))) + (erc-buffer-filter predicate proc)) + +(defmacro erc-with-all-buffers-of-server (process pred &rest forms) + "Execute FORMS in all buffers which have same process as this server. +FORMS will be evaluated in all buffers having the process PROCESS and +where PRED matches or in all buffers of the server process if PRED is +nil." + ;; Make the evaluation have the correct order + (let ((pre (erc-gensym)) + (pro (erc-gensym))) + `(let ((,pro ,process) + (,pre ,pred)) + (mapcar (lambda (buffer) + (with-current-buffer buffer + ,@forms)) + (erc-buffer-list ,pre + ,pro))))) +(put 'erc-with-all-buffers-of-server 'lisp-indent-function 1) +(put 'erc-with-all-buffers-of-server 'edebug-form-spec '(form form body)) + +(defun erc-iswitchb (&optional arg) + "Use `iswitchb-read-buffer' to prompt for a ERC buffer to switch to. +When invoked with prefix argument, use all erc buffers. Without prefix +ARG, allow only buffers related to same session server. +If `erc-track-mode' is in enabled, put the last element of +`erc-modified-channels-alist' in front of the buffer list. + +Due to some yet unresolved reason, global function `iswitchb-mode' +needs to be active for this function to work." + (interactive "P") + (eval-when-compile + (require 'iswitchb)) + (let ((iswitchb-make-buflist-hook + (lambda () + (setq iswitchb-temp-buflist + (mapcar 'buffer-name + (erc-buffer-list + nil + (when (and arg (boundp 'erc-server-process)) + erc-server-process))))))) + (switch-to-buffer + (iswitchb-read-buffer + "Switch-to: " + (if (boundp 'erc-modified-channels-alist) + (buffer-name (caar (last erc-modified-channels-alist))) + nil) + t)))) + +(defun erc-channel-list (proc) + "Return a list of channel buffers. +PROC is the process for the server connection. If PROC is nil, return +all channel buffers on all servers." + (erc-buffer-filter + (lambda () + (and (erc-default-target) + (erc-channel-p (erc-default-target)))) + proc)) + +(defun erc-buffer-list-with-nick (nick proc) + "Return buffers containing NICK in the `erc-channel-users' list." + (with-current-buffer (process-buffer proc) + (let ((user (gethash (erc-downcase nick) erc-server-users))) + (if user + (erc-server-user-buffers user) + nil)))) + +;; Some local variables + +(defvar erc-default-recipients nil + "List of default recipients of the current buffer.") +(make-variable-buffer-local 'erc-default-recipients) + +(defvar erc-session-user-full-name nil + "Full name of the user on the current server.") +(make-variable-buffer-local 'erc-session-user-full-name) + +(defvar erc-channel-user-limit nil + "Limit of users per channel.") +(make-variable-buffer-local 'erc-channel-user-limit) + +(defvar erc-channel-key nil + "Key needed to join channel.") +(make-variable-buffer-local 'erc-channel-key) + +(defvar erc-invitation nil + "Last invitation channel.") +(make-variable-buffer-local 'erc-invitation) + +(defvar erc-away nil + "Non-nil indicates that we are away.") +(make-variable-buffer-local 'erc-away) + +(defvar erc-channel-list nil + "Server channel list.") +(make-variable-buffer-local 'erc-channel-list) + +(defvar erc-bad-nick nil + "Non-nil indicates that we got a `nick in use' error while connecting.") +(make-variable-buffer-local 'erc-bad-nick) + +(defvar erc-logged-in nil + "Non-nil indicates that we are logged in.") +(make-variable-buffer-local 'erc-logged-in) + +(defvar erc-default-nicks nil + "The local copy of `erc-nick' - the list of nicks to choose from.") +(make-variable-buffer-local 'erc-default-nicks) + +(defvar erc-nick-change-attempt-count 0 + "Used to keep track of how many times an attempt at changing nick is made.") +(make-variable-buffer-local 'erc-nick-change-attempt-count) + +(defcustom erc-modules '(netsplit fill button match track pcomplete readonly + ring autojoin noncommands irccontrols + stamp) + "A list of modules which erc should enable. +If you set the value of this without using `customize' remember to call +\(erc-update-modules) after you change it. When using `customize', modules +removed from the list will be disabled." + :set (lambda (sym val) + ;; disable modules which have just been removed + (when (and (boundp 'erc-modules) erc-modules val) + (dolist (module erc-modules) + (unless (member module val) + (let ((f (intern-soft (format "erc-%s-mode" module)))) + (when (and (fboundp f) (boundp f) (symbol-value f)) + (message "Disabling `erc-%s'" module) + (funcall f 0)))))) + (set-default sym val) + ;; this test is for the case where erc hasn't been loaded yet + (when (fboundp 'erc-update-modules) + (erc-update-modules))) + :type '(set :greedy t + (const :tag "Set away status automatically" autoaway) + (const :tag "Join channels automatically" autojoin) + (const :tag "Integrate with Big Brother Database" bbdb) + (const :tag "Buttonize URLs, nicknames, and other text" button) + (const :tag "Wrap long lines" fill) + (const :tag "Highlight or remove IRC control characters" + irccontrols) + (const :tag "Save buffers in logs" log) + (const :tag "Highlight pals, fools, and other keywords" match) + (const :tag "Detect netsplits" netsplit) + (const :tag "Don't display non-IRC commands after evaluation" + noncommands) + (const :tag + "Notify when the online status of certain users changes" + notify) + (const :tag "Complete nicknames and commands (programmable)" + pcomplete) + (const :tag "Complete nicknames and commands (old)" completion) + (const :tag "Make displayed lines read-only" readonly) + (const :tag "Replace text in messages" replace) + (const :tag "Enable an input history" ring) + (const :tag "Scroll to the bottom of the buffer" scrolltobottom) + (const :tag "Identify to Nickserv (IRC Services) automatically" + services) + (const :tag "Convert smileys to pretty icons" smiley) + (const :tag "Play sounds when you receive CTCP SOUND requests" + sound) + (const :tag "Add timestamps to messages" stamp) + (const :tag "Check spelling" spelling) + (const :tag "Track channel activity in the mode-line" track) + (const :tag "Truncate buffers to a certain size" truncate) + (const :tag "Translate morse code in messages" unmorse) + (repeat :tag "Others" :inline t symbol)) + :group 'erc) + +(defun erc-update-modules () + "Run this to enable erc-foo-mode for all modules in `erc-modules'." + (let (req) + (dolist (mod erc-modules) + (setq req (concat "erc-" (symbol-name mod))) + (cond + ;; yuck. perhaps we should bring the filenames into sync? + ((string= req "erc-completion") + (setq req "erc-pcomplete") + (setq mod 'pcomplete)) + ((string= req "erc-services") + (setq req "erc-nickserv") + (setq mod 'services))) + (condition-case nil + (require (intern req)) + (error nil)) + (funcall (or (intern-soft (concat "erc-" (symbol-name mod) "-mode")) + (error "`%s' is not a known ERC module" mod)) + 1)))) + +(defun erc-setup-buffer (buffer) + "Consults `erc-join-buffer' to find out how to display `BUFFER'." + (cond ((eq erc-join-buffer 'window) + (if (active-minibuffer-window) + (display-buffer buffer) + (switch-to-buffer-other-window buffer))) + ((eq erc-join-buffer 'window-noselect) + (display-buffer buffer)) + ((eq erc-join-buffer 'bury) + nil) + ((eq erc-join-buffer 'frame) + (funcall '(lambda (frame) + (raise-frame frame) + (select-frame frame)) + (make-frame (or erc-frame-alist + default-frame-alist))) + (switch-to-buffer buffer) + (when erc-frame-dedicated-flag + (set-window-dedicated-p (selected-window) t))) + (t + (if (active-minibuffer-window) + (display-buffer buffer) + (switch-to-buffer buffer))))) + +(defun erc (&optional server port nick full-name + connect passwd tgt-list channel process) + "ERC is a powerful, modular, and extensible IRC client. + +Connect to SERVER on PORT as NICK with FULL-NAME. + +If CONNECT is non-nil, connect to the server. Otherwise assume +already connected and just create a separate buffer for the new +target CHANNEL. + +Use PASSWD as user password on the server. If TGT-LIST is +non-nil, use it to initialise `erc-default-recipients'. + +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)) + continued-session) + (erc-update-modules) + (set-buffer buffer) + (erc-mode) + (setq erc-server-announced-name server-announced-name) + (setq erc-server-connected connected-p) + ;; connection parameters + (setq erc-server-process process) + (setq erc-insert-marker (make-marker)) + (setq erc-input-marker (make-marker)) + ;; go to the end of the buffer and open a new line + ;; (the buffer may have existed) + (goto-char (point-max)) + (forward-line 0) + (when (get-text-property (point) 'erc-prompt) + (setq continued-session t) + (set-marker erc-input-marker + (or (next-single-property-change (point) 'erc-prompt) + (point-max)))) + (unless continued-session + (goto-char (point-max)) + (insert "\n")) + (set-marker erc-insert-marker (point)) + ;; stack of default recipients + (setq erc-default-recipients tgt-list) + (setq erc-server-current-nick nil) + ;; Initialize erc-server-users and erc-channel-users + (if connect + (progn ;; server buffer + (setq erc-server-users + (make-hash-table :test 'equal)) + (setq erc-channel-users nil)) + (progn ;; target buffer + (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) + (setq erc-channel-key nil) + ;; last active buffer, defaults to this one + (erc-set-active-buffer buffer) + ;; last invitation channel + (setq erc-invitation nil) + ;; away flag + ;; Should only be used in session-buffers + (setq erc-away (let ((serverbuf (erc-server-buffer))) + (and serverbuf (with-current-buffer serverbuf erc-away)))) + ;; Server channel list + (setq erc-channel-list ()) + ;; login-time 'nick in use' error + (setq erc-bad-nick nil) + ;; whether we have logged in + (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 passwd) + ;; debug output buffer + (setq erc-dbuf + (when erc-log-p + (get-buffer-create (concat "*ERC-DEBUG: " server "*")))) + (erc-determine-parameters server port nick full-name) + + ;; Saving log file on exit + (run-hooks 'erc-connect-pre-hook) + + (when connect + (erc-server-connect erc-session-server erc-session-port)) + (erc-update-mode-line) + (set-marker erc-insert-marker (point)) + (unless continued-session + (goto-char (point-max)) + (insert "\n")) + (set-marker (process-mark erc-server-process) (point)) + (unless continued-session + (set-marker erc-insert-marker (point)) + (erc-display-prompt) + (goto-char (point-max))) + + ;; Now display the buffer in a window as per user wishes. + (unless (eq buffer old-buffer) + (when erc-log-p + ;; we can't log to debug buffer, it may not exist yet + (message "erc: old buffer %s, switching to %s" + old-buffer buffer)) + (erc-setup-buffer buffer)) + + buffer)) + +(defun erc-initialize-log-marker () + "Initialize the `erc-last-saved-position' marker to a sensible position." + (setq erc-last-saved-position (make-marker)) + (move-marker erc-last-saved-position + (1- (marker-position erc-insert-marker)))) + +;; interactive startup + +(defvar erc-server-history-list nil + "IRC server interactive selection history list.") + +(defvar erc-nick-history-list nil + "Nickname interactive selection history list.") + +(defun erc-already-logged-in (server port nick) + "Return the buffers corresponding to a NICK on PORT of a session SERVER. +This is determined by looking for the appropriate buffer and checking +whether the connection is still alive. +If no buffer matches, return nil." + (erc-buffer-list + (lambda () + (and (erc-server-process-alive) + (string= erc-session-server server) + (erc-port-equal erc-session-port port) + (erc-current-nick-p nick))))) + +(if (not (fboundp 'read-passwd)) + (defun read-passwd (prompt) + "Substitute for read-passwd in early emacsen" + (read-from-minibuffer prompt))) + +(defcustom erc-before-connect nil + "Hook called before connecting to a server. +This hook gets executed before `erc-select' actually invokes `erc-mode' +with your input data. The functions in here get called with three +parameters, SERVER, PORT and NICK." + :group 'erc-hooks + :type 'hook) + +(defcustom erc-after-connect nil + "Hook called after connecting to a server. +This hook gets executed when an end of MOTD has been received. All +functions in here get called with the parameters SERVER and NICK." + :group 'erc-hooks + :type 'hook) + +;;;###autoload +(defun erc-select-read-args () + "Prompt the user for values of nick, server, port, and password." + (let (user-input server port nick passwd) + (setq user-input (read-from-minibuffer + "IRC server: " + (erc-compute-server) nil nil 'erc-server-history-list)) + + (if (string-match "\\(.*\\):\\(.*\\)\\'" user-input) + (setq port (erc-string-to-port (match-string 2 user-input)) + user-input (match-string 1 user-input)) + (setq port + (erc-string-to-port (read-from-minibuffer + "IRC port: " (erc-port-to-string + (erc-compute-port)))))) + + (if (string-match "\\`\\(.*\\)@\\(.*\\)" user-input) + (setq nick (match-string 1 user-input) + user-input (match-string 2 user-input)) + (setq nick + (if (erc-already-logged-in server port nick) + (read-from-minibuffer + (erc-format-message 'nick-in-use ?n nick) + nick + nil nil 'erc-nick-history-list) + (read-from-minibuffer + "Nickname: " (erc-compute-nick nick) + nil nil 'erc-nick-history-list)))) + + (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)) + (when (and passwd (string= "" passwd)) + (setq passwd nil)) + + (while (erc-already-logged-in server port nick) + ;; hmm, this is a problem when using multiple connections to a bnc + ;; with the same nick. Currently this code prevents using more than one + ;; bnc with the same nick. actually it would be nice to have + ;; bncs transparent, so that erc-compute-buffer-name displays + ;; the server one is connected to. + (setq nick (read-from-minibuffer + (erc-format-message 'nick-in-use ?n nick) + nick + nil nil 'erc-nick-history-list))) + (list :server server :port port :nick nick :password passwd))) + +;;;###autoload +(defun* erc-select (&key (server (erc-compute-server)) + (port (erc-compute-port)) + (nick (erc-compute-nick)) + password + (full-name (erc-compute-full-name))) + "Select connection parameters and run ERC. +Non-interactively, it takes keyword arguments + (server (erc-compute-server)) + (port (erc-compute-port)) + (nick (erc-compute-nick)) + password + (full-name (erc-compute-full-name))) + +That is, if called with + (erc-select :server \"irc.freenode.net\" :full-name \"Harry S Truman\") +server and full-name will be set to those values, whereas +erc-compute-port, erc-compute-nick and erc-compute-full-name will +be invoked for those parameters' values" + (interactive (erc-select-read-args)) + + (run-hook-with-args 'erc-before-connect server port nick) + (erc server port nick erc-user-full-name t password)) + + +(defun erc-select-ssl (&rest r) + "Interactively select SSL connection parameters and run ERC. +Arguments are as to erc-select." + (interactive (erc-select-read-args)) + (let ((erc-server-connect-function 'erc-open-ssl-stream)) + (apply 'erc-select r))) + +(defun erc-open-ssl-stream (name buffer host port) + "Open an SSL stream to an IRC server. +The process will be given the name NAME, its target buffer will be +BUFFER. HOST and PORT specify the connection target." + (when (require 'ssl) + (let ((proc (open-ssl-stream name buffer host port))) + ;; Ugly hack, but it works for now. Problem is it is + ;; very hard to detect when ssl is established, because s_client + ;; doesn't give any CONNECTIONESTABLISHED kind of message, and + ;; most IRC servers send nothing and wait for you to identify. + (sit-for 5) + proc))) + +;;; Debugging the protocol + +(defvar erc-debug-irc-protocol nil + "If non-nil, log all IRC protocol traffic to the buffer \"*erc-protocol*\". + +The buffer is created if it doesn't exist. + +NOTE: If this variable is non-nil, and you kill the the only +visible \"*erc-protocol*\" buffer, it will be recreated shortly, +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.") + +(defun erc-log-irc-protocol (string &optional outbound) + "Append STRING to the buffer *erc-protocol*. + +This only has any effect if `erc-debug-irc-protocol' is non-nil. + +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 erc-input-face in the buffer." + (when erc-debug-irc-protocol + (let ((network-name (or (ignore-errors (erc-network-name)) + "???"))) + (with-current-buffer (get-buffer-create "*erc-protocol*") + (save-excursion + (goto-char (point-max)) + (let ((inhibit-read-only t)) + (insert (if (not outbound) + ;; Cope with the fact that string might + ;; contain multiple lines of text. + (let ((lines (delete "" (split-string string + "\n\\|\r\n"))) + (result "")) + (dolist (line lines) + (setq result (concat result network-name + " << " line "\n"))) + result) + (erc-propertize + (concat network-name " >> " string + (if (/= ?\n + (aref string + (1- (length string)))) + "\n")) + 'face 'erc-input-face))))) + (let ((orig-win (selected-window)) + (debug-buffer-window (get-buffer-window (current-buffer) t))) + (when debug-buffer-window + (select-window debug-buffer-window) + (when (= 1 (count-lines (point) (point-max))) + (goto-char (point-max)) + (recenter -1)) + (select-window orig-win))))))) + +(defun erc-toggle-debug-irc-protocol (&optional arg) + "Toggle the value of `erc-debug-irc-protocol'. + +If ARG is non-nil, show the *erc-protocol* buffer." + (interactive "P") + (let* ((buf (get-buffer-create "*erc-protocol*"))) + (with-current-buffer buf + (erc-view-mode-enter 1) + (when (null (current-local-map)) + (let ((inhibit-read-only t)) + (insert (erc-make-notice "This buffer displays all IRC protocol traffic exchanged with each server.\n")) + (insert (erc-make-notice "Kill this buffer to terminate protocol logging.\n\n"))) + (use-local-map (make-sparse-keymap)) + (local-set-key (kbd "RET") 'erc-toggle-debug-irc-protocol)) + (add-hook 'kill-buffer-hook + #'(lambda () (setq erc-debug-irc-protocol nil)) + nil 'local) + (goto-char (point-max)) + (let ((inhibit-read-only t)) + (insert (erc-make-notice + (format "IRC protocol logging %s at %s -- Press ENTER to toggle logging.\n" + (if erc-debug-irc-protocol "disabled" "enabled") + (current-time-string)))))) + (setq erc-debug-irc-protocol (not erc-debug-irc-protocol)) + (if (and arg + (not (get-buffer-window "*erc-protocol*" t))) + (display-buffer buf t)) + (message "IRC protocol traffic logging %s (see buffer *erc-protocol*)." + (if erc-debug-irc-protocol "enabled" "disabled")))) + +;;; I/O interface + +;; send interface + +(defun erc-send-action (tgt str &optional force) + "Send CTCP ACTION information described by STR to TGT." + (erc-send-ctcp-message tgt (format "ACTION %s" str) force) + (erc-display-message + nil 'input (current-buffer) + 'ACTION ?n (erc-current-nick) ?a str ?u "" ?h "")) + +;; Display interface + +(defun erc-string-invisible-p (string) + "Check whether STRING is invisible or not. +I.e. any char in it has the `invisible' property set." + (text-property-any 0 (length string) 'invisible t string)) + +(defun erc-display-line-1 (string buffer) + "Display STRING in `erc-mode' BUFFER. +Auxiliary function used in `erc-display-line'. The line gets filtered to +interpret the control characters. Then, `erc-insert-pre-hook' gets called. +If `erc-insert-this' is still t, STRING gets inserted into the buffer. +Afterwards, `erc-insert-modify' and `erc-insert-post-hook' get called. +If STRING is nil, the function does nothing." + (when string + (save-excursion + (set-buffer (or buffer (process-buffer erc-server-process))) + (let ((insert-position (or (marker-position erc-insert-marker) + (point-max)))) + (let ((string string) ;; FIXME! Can this be removed? + (buffer-undo-list t) + (inhibit-read-only t)) + (unless (string-match "\n$" string) + (setq string (concat string "\n")) + (when (erc-string-invisible-p string) + (erc-put-text-properties 0 (length string) string + '(invisible intangible)))) + (erc-log (concat "erc-display-line: " string + (format "(%S)" string) " in buffer " + (format "%s" buffer))) + (setq erc-insert-this t) + (run-hook-with-args 'erc-insert-pre-hook string) + (if (null erc-insert-this) + ;; Leave erc-insert-this set to t as much as possible. Fran + ;; Litterio <franl> has seen erc-insert-this set to nil while + ;; erc-send-pre-hook is running, which should never happen. This + ;; may cure it. + (setq erc-insert-this t) + (save-excursion ;; to restore point in the new buffer + (save-restriction + (widen) + (goto-char insert-position) + (insert-before-markers string) + ;; run insertion hook, with point at restored location + (save-restriction + (narrow-to-region insert-position (point)) + (run-hooks 'erc-insert-modify-hook) + (run-hooks 'erc-insert-post-hook)))))) + (erc-update-undo-list (- (or (marker-position erc-insert-marker) + (point-max)) + insert-position)))))) + +(defun erc-update-undo-list (shift) + ;; Translate buffer positions in buffer-undo-list by SHIFT. + (unless (or (zerop shift) (atom buffer-undo-list)) + (let ((list buffer-undo-list) elt) + (while list + (setq elt (car list)) + (cond ((integerp elt) ; POSITION + (incf (car list) shift)) + ((or (atom elt) ; nil, EXTENT + ;; (eq t (car elt)) ; (t HIGH . LOW) + (markerp (car elt))) ; (MARKER . DISTANCE) + nil) + ((integerp (car elt)) ; (BEGIN . END) + (incf (car elt) shift) + (incf (cdr elt) shift)) + ((stringp (car elt)) ; (TEXT . POSITION) + (incf (cdr elt) (* (if (natnump (cdr elt)) 1 -1) shift))) + ((null (car elt)) ; (nil PROPERTY VALUE BEG . END) + (let ((cons (nthcdr 3 elt))) + (incf (car cons) shift) + (incf (cdr cons) shift))) + ((and (featurep 'xemacs) + (extentp (car elt))) ; (EXTENT START END) + (incf (nth 1 elt) shift) + (incf (nth 2 elt) shift))) + (setq list (cdr list)))))) + +(defvar erc-valid-nick-regexp "[]a-zA-Z^[;\\`_{}|][]^[;\\`_{}|a-zA-Z0-9-]*" + "Regexp which matches all legal characters in a IRC nickname.") + +(defun erc-is-valid-nick-p (nick) + "Check if NICK is a valid IRC nickname." + (string-match (concat "^" erc-valid-nick-regexp "$") nick)) + +(defun erc-display-line (string &optional buffer) + "Display STRING in the ERC BUFFER. +All screen output must be done through this function. If BUFFER is nil +or omitted, the default ERC buffer for the `erc-session-server' is used. +The BUFFER can be an actual buffer, a list of buffers, 'all or 'active. +If BUFFER = 'all, the string is displayed in all the ERC buffers for the +current session. 'active means the current active buffer +\(`erc-active-buffer'). If the buffer can't be resolved, the current +buffer is used. `erc-display-line-1' is used to display STRING. + +If STRING is nil, the function does nothing." + (let ((inhibit-point-motion-hooks t) + new-bufs) + (dolist (buf (cond + ((bufferp buffer) (list buffer)) + ((listp buffer) buffer) + ((processp buffer) (list (process-buffer buffer))) + ((eq 'all buffer) + (and (boundp 'erc-server-process) + ;; Hmm, or all of the same session server? + (erc-buffer-list nil erc-server-process))) + ((and (eq 'active buffer) (erc-active-buffer)) + (list (erc-active-buffer))) + ((erc-server-buffer-live-p) + (list (process-buffer erc-server-process))) + (t (list (current-buffer))))) + (when (buffer-live-p buf) + (erc-display-line-1 string buf) + (add-to-list 'new-bufs buf))) + (when (null new-bufs) + (if (erc-server-buffer-live-p) + (erc-display-line-1 string (process-buffer erc-server-process)) + (erc-display-line-1 string (current-buffer)))))) + +(defun erc-display-message-highlight (type string) + "Highlight STRING according to TYPE, where erc-TYPE-face is an erc face. + +See also `erc-make-notice'" + (cond ((eq type 'notice) + (erc-make-notice string)) + (t + (erc-put-text-property + 0 (length string) + 'face (or (intern-soft + (concat "erc-" (symbol-name type) "-face")) + "erc-default-face") + string) + string))) + +(defun erc-display-message (parsed type buffer msg &rest args) + "Display MSG in BUFFER. + +ARGS, PARSED, and TYPE are used to format MSG sensibly. + +See also `erc-format-message' and `erc-display-line'." + (let ((string (if (symbolp msg) + (apply 'erc-format-message msg args) + msg))) + (setq string + (cond + ((listp type) + (mapc (lambda (type) + (setq string + (erc-display-message-highlight type string))) + type) + string) + ((symbolp type) + (erc-display-message-highlight type string)))) + + (if (not (erc-response-p parsed)) + (erc-display-line string buffer) + (unless (member (erc-response.command parsed) erc-hide-list) + (erc-put-text-property 0 (length string) 'erc-parsed parsed string) + (erc-put-text-property 0 (length string) 'rear-sticky t string) + (erc-display-line string buffer))))) + +(defun erc-message-type-member (position list) + "Return non-nil if the erc-parsed text-property at POSITION is in LIST. + +This function relies on the erc-parsed text-property being +present." + (let ((prop-val (get-text-property position 'erc-parsed))) + (and prop-val (member (erc-response.command prop-val) list)))) + +(defvar erc-send-input-line-function 'erc-send-input-line) +(make-variable-buffer-local 'erc-send-input-line-function) + +(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)) + +(defun erc-get-arglist (fun) + "Return the argument list of a function without the parens." + (let ((arglist (format "%S" (erc-function-arglist fun)))) + (if (string-match "^(\\(.*\\))$" arglist) + (match-string 1 arglist) + arglist))) + +(defun erc-command-name (cmd) + "For CMD being the function name of a ERC command, something like +erc-cmd-FOO, this returns a string /FOO." + (let ((command-name (symbol-name cmd))) + (if (string-match "^erc-cmd-\\(.*\\)$" command-name) + (concat "/" (match-string 1 command-name)) + command-name))) + +(defun erc-process-input-line (line &optional force no-command) + "Translate LINE to an RFC1459 command and send it based. +Returns non-nil if the command is actually sent to the server, and nil +otherwise. + +If the command in the LINE is not bound as a function `erc-cmd-<COMMAND>', +it is passed to `erc-cmd-default'. If LINE is not a command (ie. doesn't +start with /<COMMAND>) then it is sent as a message. + +An optional FORCE argument forces sending the line when flood +protection is in effect. The optional NO-COMMAND argument prohibits +this function from interpreting the line as a command." + (let ((command-list (erc-extract-command-from-line line))) + (if (and command-list + (not no-command)) + (let* ((cmd (nth 0 command-list)) + (args (nth 1 command-list))) + (condition-case nil + (if (listp args) + (apply cmd args) + (funcall cmd args)) + (wrong-number-of-arguments + (erc-display-message nil 'error (current-buffer) 'incorrect-args + ?c (erc-command-name cmd) + ?u (or (erc-get-arglist cmd) + "") + ?d (format "%s\n" + (or (documentation cmd) ""))) + nil))) + (let ((r (erc-default-target))) + (if r + (funcall erc-send-input-line-function r line force) + (erc-display-message nil 'error (current-buffer) 'no-target) + nil))))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; Input commands handlers +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +(defun erc-cmd-AMSG (line) + "Send LINE to all channels of the current server that you are on." + (interactive "sSend to all channels you're on: ") + (setq line (erc-trim-string line)) + (erc-with-all-buffers-of-server nil + (lambda () + (erc-channel-p (erc-default-target))) + (erc-send-message line))) +(put 'erc-cmd-AMSG 'do-not-parse-args t) + +(defun erc-cmd-SAY (line) + "Send LINE to the current query or channel as a message, not a command. + +Use this when you want to send a message with a leading '/'. Note +that since multi-line messages are never a command, you don't +need this when pasting multiple lines of text." + (if (string-match "^\\s-*$" line) + nil + (string-match "^ ?\\(.*\\)" line) + (erc-process-input-line (match-string 1 line) nil t))) +(put 'erc-cmd-SAY 'do-not-parse-args t) + +(defun erc-cmd-SET (line) + "Set the variable named by the first word in LINE to some VALUE. +VALUE is computed by evaluating the rest of LINE in Lisp." + (cond + ((string-match "^\\s-*\\(\\S-+\\)\\s-+\\(.*\\)$" line) + (let ((var (read (concat "erc-" (match-string 1 line)))) + (val (read (match-string 2 line)))) + (if (boundp var) + (progn + (set var (eval val)) + (erc-display-message + nil nil 'active (format "Set %S to %S" var val)) + t) + (setq var (read (match-string 1 line))) + (if (boundp var) + (progn + (set var (eval val)) + (erc-display-message + nil nil 'active (format "Set %S to %S" var val)) + t) + (erc-display-message nil 'error 'active 'variable-not-bound) + nil)))) + ((string-match "^\\s-*$" line) + (erc-display-line + (concat "Available user variables:\n" + (apply + 'concat + (mapcar + (lambda (var) + (let ((val (symbol-value var))) + (concat (format "%S:" var) + (if (consp val) + (concat "\n" (pp-to-string val)) + (format " %S\n" val))))) + (apropos-internal "^erc-" 'user-variable-p)))) + (current-buffer)) t) + (t nil))) +(defalias 'erc-cmd-VAR 'erc-cmd-SET) +(defalias 'erc-cmd-VARIABLE 'erc-cmd-SET) +(put 'erc-cmd-SET 'do-not-parse-args t) + +(defun erc-cmd-default (line) + "Fallback command. + +Commands for which no erc-cmd-xxx exists, are tunnelled 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)) + t) + +(defun erc-cmd-IGNORE (&optional user) + "Ignore USER. This should be a regexp matching nick!user@host. +If no USER argument is specified, list the contents of `erc-ignore-list'." + (if user + (progn + (erc-display-line + (erc-make-notice (format "Now ignoring %s" user)) + 'active) + (with-current-buffer (erc-server-buffer) + (add-to-list 'erc-ignore-list user))) + (if (null (with-current-buffer (erc-server-buffer) erc-ignore-list)) + (erc-display-line (erc-make-notice "Ignore list is empty") 'active) + (erc-display-line (erc-make-notice "Ignore list:") 'active) + (mapc #'(lambda (item) + (erc-display-line (erc-make-notice item) + 'active)) + (with-current-buffer (erc-server-buffer) erc-ignore-list)))) + t) + +(defun erc-cmd-UNIGNORE (user) + "Remove the user specified in USER from the ignore list." + (let ((ignored-nick (car (with-current-buffer (erc-server-buffer) + (erc-member-ignore-case user erc-ignore-list))))) + (if (null ignored-nick) + (erc-display-line + (erc-make-notice (format "%s is not currently ignored!" user)) + 'active) + (erc-display-line + (erc-make-notice (format "No longer ignoring %s" user)) + 'active)) + (with-current-buffer (erc-server-buffer) + (setq erc-ignore-list (delete ignored-nick erc-ignore-list)))) + t) + +(defun erc-cmd-CLEAR () + "Clear the window content." + (recenter 0) + t) + +(defun erc-cmd-OPS () + "Show the ops in the current channel." + (interactive) + (let ((ops nil)) + (if erc-channel-users + (maphash (lambda (nick user-data) + (let ((cuser (cdr user-data))) + (if (and cuser + (erc-channel-user-op cuser)) + (setq ops (cons (erc-server-user-nickname + (car user-data)) + ops))))) + erc-channel-users)) + (setq ops (sort ops 'string-lessp)) + (if ops + (erc-display-message + nil 'notice (current-buffer) 'ops + ?i (length ops) ?s (if (> (length ops) 1) "s" "") + ?o (mapconcat 'identity ops " ")) + (erc-display-message nil 'notice (current-buffer) 'ops-none))) + t) + +(defun erc-cmd-COUNTRY (tld) + "Display the country associated with the top level domain TLD." + (require 'mail-extr) + (let ((co (ignore-errors (what-domain tld)))) + (if co + (erc-display-message + nil 'notice 'active 'country ?c co ?d tld) + (erc-display-message + nil 'notice 'active 'country-unknown ?d tld)) + t)) + +(defun erc-cmd-AWAY (line) + "Mark the user as being away, the reason being indicated by LINE. +If no reason is given, unset away status." + (when (string-match "^\\s-*\\(.*\\)$" line) + (let ((reason (match-string 1 line))) + (erc-log (format "cmd: AWAY: %s" reason)) + (erc-server-send + (if (string= reason "") + "AWAY" + (concat "AWAY :" reason)))) + t)) +(put 'erc-cmd-AWAY 'do-not-parse-args t) + +(defun erc-cmd-GAWAY (line) + "Mark the user as being away everywhere, the reason being indicated by LINE." + ;; on all server buffers. + (erc-with-all-buffers-of-server nil + #'erc-server-buffer-p + (erc-cmd-AWAY line))) +(put 'erc-cmd-GAWAY 'do-not-parse-args t) + +(defun erc-cmd-CTCP (nick cmd &rest args) + "Send a Client To Client Protocol message to NICK. + +CMD is the CTCP command, possible values being ECHO, FINGER, CLIENTINFO, TIME, +VERSION and so on. It is called with ARGS." + (let ((str (concat cmd + (when args + (concat " " (mapconcat #'identity args " ")))))) + (erc-log (format "cmd: CTCP [%s]: [%s]" nick str)) + (erc-send-ctcp-message nick str) + t)) + +(defun erc-cmd-HELP (&optional func) + "Popup help information. + +If FUNC contains a valid function or variable, help about that +will be displayed. If FUNC is empty, display an apropos about +erc commands. Otherwise, do apropos in the erc namespace +\(\"erc-.*LINE\"\). + +Examples: +To find out about erc and bbdb, do + /help bbdb.* + +For help about the WHOIS command, do: + /help whois + +For a list of user commands (/join /part, ...): + /help." + (if func + (let* ((sym (or (let ((sym (intern-soft + (concat "erc-cmd-" (upcase func))))) + (if (and sym (or (boundp sym) (fboundp sym))) + sym + nil)) + (let ((sym (intern-soft func))) + (if (and sym (or (boundp sym) (fboundp sym))) + sym + nil)) + (let ((sym (intern-soft (concat "erc-" func)))) + (if (and sym (or (boundp sym) (fboundp sym))) + sym + nil))))) + (if sym + (cond + ((boundp sym) (describe-variable sym)) + ((fboundp sym) (describe-function sym)) + (t nil)) + (apropos-command (concat "erc-.*" func) nil + (lambda (x) + (or (commandp x) + (get x 'custom-type)))) + t)) + (apropos "erc-cmd-") + (message "Type C-h m to get additional information about keybindings.") + t)) + +(defalias 'erc-cmd-H 'erc-cmd-HELP) + +(defun erc-cmd-JOIN (channel &optional key) + "Join the channel given in CHANNEL, optionally with KEY. +If CHANNEL is specified as \"-invite\", join the channel to which you +were most recently invited. See also `invitation'." + (let (chnl) + (if (string= (upcase channel) "-INVITE") + (if erc-invitation + (setq chnl erc-invitation) + (erc-display-message nil 'error (current-buffer) 'no-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)))) + (if (erc-member-ignore-case chnl joined-channels) + (switch-to-buffer (car (erc-member-ignore-case chnl + joined-channels))) + (erc-log (format "cmd: JOIN: %s" chnl)) + (if (and chnl key) + (erc-server-send (format "JOIN %s %s" chnl key)) + (erc-server-send (format "JOIN %s" chnl))))))) + t) + +(defalias 'erc-cmd-CHANNEL 'erc-cmd-JOIN) +(defalias 'erc-cmd-J 'erc-cmd-JOIN) + +(defvar erc-channel-new-member-names nil + "If non-nil, a names list is currently being received. + +If non-nil, this variable is a hash-table that associates +received nicks with t.") +(make-variable-buffer-local 'erc-channel-new-member-names) + +(defun erc-cmd-NAMES (&optional channel) + "Display the users in CHANNEL. +If CHANNEL is not specified, display the users in the current channel. +This function clears the channel name list first, then sends the +command." + (let ((tgt (or (and (erc-channel-p channel) channel) + (erc-default-target)))) + (if (and tgt (erc-channel-p tgt)) + (progn + (erc-log (format "cmd: DEFAULT: NAMES %s" tgt)) + (erc-with-buffer + (tgt) + (erc-channel-begin-receiving-names)) + (erc-server-send (concat "NAMES " tgt))) + (erc-display-message nil 'error (current-buffer) 'no-default-channel))) + t) +(defalias 'erc-cmd-N 'erc-cmd-NAMES) + +(defun erc-cmd-KICK (target &optional reason-or-nick &rest reasonwords) + "Kick the user indicated in LINE from the current channel. +LINE has the format: \"#CHANNEL NICK REASON\" or \"NICK REASON\"." + (let ((reasonstring (mapconcat 'identity reasonwords " "))) + (if (string= "" reasonstring) + (setq reasonstring (format "Kicked by %s" (erc-current-nick)))) + (if (erc-channel-p target) + (let ((nick reason-or-nick)) + (erc-log (format "cmd: KICK: %s/%s: %s" nick target reasonstring)) + (erc-server-send (format "KICK %s %s :%s" target nick reasonstring) + nil target) + t) + (when target + (let ((ch (erc-default-target))) + (setq reasonstring (concat + (if reason-or-nick (concat reason-or-nick " ")) + reasonstring)) + (if ch + (progn + (erc-log + (format "cmd: KICK: %s/%s: %s" target ch reasonstring)) + (erc-server-send + (format "KICK %s %s :%s" ch target reasonstring) nil ch)) + (erc-display-message nil 'error (current-buffer) + 'no-default-channel)) + t))))) + +(defvar erc-script-args nil) + +(defun erc-cmd-LOAD (line) + "Load the script provided in the LINE. +If LINE continues beyond the file name, +the rest of it is put in a (local) variable +`erc-script-args', which can be used in elisp scripts. + +The optional FORCE argument is ignored here - you can't force loading +a script after exceeding the flood threshold." + (cond + ((string-match "^\\s-*\\(\\S-+\\)\\(.*\\)$" line) + (let* ((file-to-find (match-string 1 line)) + (erc-script-args (match-string 2 line)) + (file (erc-find-file file-to-find erc-script-path))) + (erc-log (format "cmd: LOAD: %s" file-to-find)) + (cond + ((not file) + (erc-display-message nil 'error (current-buffer) + 'cannot-find-file ?f file-to-find)) + ((not (file-readable-p file)) + (erc-display-message nil 'error (current-buffer) + 'cannot-read-file ?f file)) + (t + (message "Loading \'%s\'..." file) + (erc-load-script file) + (message "Loading \'%s\'...done" file)))) + t) + (t nil))) + +(defun erc-cmd-WHOIS (user &optional server) + "Display whois information for USER. + +If SERVER is non-nil, use that, rather than the current server." + ;; FIXME: is the above docstring correct? -- Lawrence 2004-01-08 + (let ((send (if server + (format "WHOIS %s %s" user server) + (format "WHOIS %s" user)))) + (erc-log (format "cmd: %s" send)) + (erc-server-send send) + t)) +(defalias 'erc-cmd-WI 'erc-cmd-WHOIS) + +(defun erc-cmd-WHOAMI () + "Display whois information about yourself." + (erc-cmd-WHOIS (erc-current-nick)) + t) + +(defun erc-cmd-IDLE (nick) + "Show the length of time NICK has been idle." + (let ((serverbuf (erc-server-buffer)) + (origbuf (current-buffer)) + symlist) + (with-current-buffer serverbuf + (add-to-list 'symlist + (cons (erc-once-with-server-event + 311 `(string= ,nick + (second + (erc-response.command-args parsed)))) + 'erc-server-311-functions)) + (add-to-list 'symlist + (cons (erc-once-with-server-event + 312 `(string= ,nick + (second + (erc-response.command-args parsed)))) + 'erc-server-312-functions)) + (add-to-list 'symlist + (cons (erc-once-with-server-event + 318 `(string= ,nick + (second + (erc-response.command-args parsed)))) + 'erc-server-318-functions)) + (add-to-list 'symlist + (cons (erc-once-with-server-event + 319 `(string= ,nick + (second + (erc-response.command-args parsed)))) + 'erc-server-319-functions)) + (add-to-list 'symlist + (cons (erc-once-with-server-event + 320 `(string= ,nick + (second + (erc-response.command-args parsed)))) + 'erc-server-320-functions)) + (add-to-list 'symlist + (cons (erc-once-with-server-event + 330 `(string= ,nick + (second + (erc-response.command-args parsed)))) + 'erc-server-330-functions)) + (add-to-list 'symlist + (cons (erc-once-with-server-event + 317 + `(let ((idleseconds + (string-to-number + (third + (erc-response.command-args parsed))))) + (erc-display-line + (erc-make-notice + (format "%s has been idle for %s." + (erc-string-no-properties ,nick) + (erc-seconds-to-string idleseconds))) + ,origbuf)) + t) + 'erc-server-317-functions)) + + ;; Send the WHOIS command. + (erc-cmd-WHOIS nick) + + ;; Remove the uninterned symbols from the server hooks that did not run. + (run-at-time 20 nil `(lambda () + (with-current-buffer ,(current-buffer) + (dolist (sym ',symlist) + (let ((hooksym (cdr sym)) + (funcsym (car sym))) + (remove-hook hooksym funcsym t)))))))) + t) + +(defun erc-cmd-DESCRIBE (line) + "Pose some action to a certain user. +LINE has the format \"USER ACTION\"." + (cond + ((string-match + "^\\s-*\\(\\S-+\\)\\s-\\(.*\\)$" line) + (let ((dst (match-string 1 line)) + (s (match-string 2 line))) + (erc-log (format "cmd: DESCRIBE: [%s] %s" dst s)) + (erc-send-action dst s)) + t) + (t nil))) +(put 'erc-cmd-DESCRIBE 'do-not-parse-args t) + +(defun erc-cmd-ME (line) + "Send LINE as an action." + (cond + ((string-match "^\\s-\\(.*\\)$" line) + (let ((s (match-string 1 line))) + (erc-log (format "cmd: ME: %s" s)) + (erc-send-action (erc-default-target) s)) + t) + (t nil))) +(put 'erc-cmd-ME 'do-not-parse-args t) + +(defun erc-cmd-LASTLOG (line) + "Show all lines in the current buffer matching the regexp LINE. + +If a match spreads across multiple lines, all those lines are shown. + +The lines are shown in a buffer named `*Occur*'. +It serves as a menu to find any of the occurrences in this buffer. +\\[describe-mode] in that buffer will explain how. + +If LINE contains upper case characters (excluding those preceded by `\'), +the matching is case-sensitive." + (occur line) + t) +(put 'erc-cmd-LASTLOG 'do-not-parse-args t) + +(defun erc-send-message (line &optional force) + "Send LINE to the current channel or user and display it. + +See also `erc-message' and `erc-display-line'." + (erc-message "PRIVMSG" (concat (erc-default-target) " " line) force) + (erc-display-line + (concat (erc-format-my-nick) line) + (current-buffer)) + ;; FIXME - treat multiline, run hooks, or remove me? + t) + +(defun erc-cmd-MODE (line) + "Change or display the mode value of a channel or user. +The first word specifies the target. The rest is the mode string +to send. + +If only one word is given, display the mode of that target. + +A list of valid mode strings for Freenode may be found at +`http://freenode.net/using_the_network.shtml'." + (cond + ((string-match "^\\s-\\(.*\\)$" line) + (let ((s (match-string 1 line))) + (erc-log (format "cmd: MODE: %s" s)) + (erc-server-send (concat "MODE " line))) + t) + (t nil))) +(put 'erc-cmd-MODE 'do-not-parse-args t) + +(defun erc-cmd-NOTICE (channel-or-user &rest message) + "Send a notice to the channel or user given as the first word. +The rest is the message to send." + (erc-message "NOTICE" (concat channel-or-user " " + (mapconcat #'identity message " ")))) + +(defun erc-cmd-MSG (line) + "Send a message to the channel or user given as the first word in LINE. + +The rest of LINE is the message to send." + (erc-message "PRIVMSG" line)) + +(defalias 'erc-cmd-M 'erc-cmd-MSG) +(put 'erc-cmd-MSG 'do-not-parse-args t) + +(defun erc-cmd-SQUERY (line) + "Send a Service Query to the service given as the first word in LINE. + +The rest of LINE is the message to send." + (erc-message "SQUERY" line)) + +(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" (with-current-buffer (erc-server-buffer) + erc-server-parameters))))) + (and nicklen (> (length nick) (string-to-number nicklen)) + (erc-display-message + nil 'notice 'active 'nick-too-long + ?i (length nick) ?l nicklen))) + (erc-server-send (format "NICK %s" nick)) + (cond (erc-bad-nick + (erc-set-current-nick nick) + (erc-update-mode-line) + (setq erc-bad-nick nil))) + t) + +(defun erc-cmd-PART (line) + "When LINE is an empty string, leave the current channel. +Otherwise leave the channel indicated by LINE." + (cond + ((string-match "^\\s-*\\([&#+!]\\S-+\\)\\s-?\\(.*\\)$" line) + (let* ((ch (match-string 1 line)) + (msg (match-string 2 line)) + (reason (funcall erc-part-reason (if (equal msg "") nil msg)))) + (erc-log (format "cmd: PART: %s: %s" ch reason)) + (erc-server-send (if (string= reason "") + (format "PART %s" ch) + (format "PART %s :%s" ch reason)) + nil ch)) + t) + ((string-match "^\\s-*\\(.*\\)$" line) + (let* ((ch (erc-default-target)) + (msg (match-string 1 line)) + (reason (funcall erc-part-reason (if (equal msg "") nil msg)))) + (if (and ch (erc-channel-p ch)) + (progn + (erc-log (format "cmd: PART: %s: %s" ch reason)) + (erc-server-send (if (string= reason "") + (format "PART %s" ch) + (format "PART %s :%s" ch reason)) + nil ch)) + (erc-display-message nil 'error (current-buffer) 'no-target))) + t) + (t nil))) +(put 'erc-cmd-PART 'do-not-parse-args t) + +(defalias 'erc-cmd-LEAVE 'erc-cmd-PART) + +(defun erc-cmd-PING (recipient) + "Ping RECIPIENT." + (let ((time (format "%f" (erc-current-time)))) + (erc-log (format "cmd: PING: %s" time)) + (erc-cmd-CTCP recipient "PING" time))) + +(defun erc-cmd-QUOTE (line) + "Send LINE directly to the server. +All the text given as argument is sent to the sever as unmodified, +just as you provided it. Use this command with care!" + (cond + ((string-match "^\\s-\\(.+\\)$" line) + (erc-server-send (match-string 1 line))) + (t nil))) +(put 'erc-cmd-QUOTE 'do-not-parse-args t) + +(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-join-buffer'. If USER is omitted, close the current query buffer if one +exists - except this is broken now ;-)" + (interactive + (list (read-from-minibuffer "Start a query with: " nil))) + (let ((session-buffer (erc-server-buffer))) + (if user + (erc-query user session-buffer) + ;; currently broken, evil hack to display help anyway + ;(erc-delete-query)))) + (signal 'wrong-number-of-arguments "")))) +(defalias 'erc-cmd-Q 'erc-cmd-QUERY) + +(defun erc-quit-reason-normal (&optional s) + "Normal quit message. + +If S is non-nil, it will be used as the quit reason." + (or s + (format "\C-bERC\C-b %s (IRC client for Emacs)"; - \C-b%s\C-b" + erc-version-string) ; erc-official-location) + )) + +(defun erc-quit-reason-zippy (&optional s) + "Zippy quit message. + +If S is non-nil, it will be used as the quit reason." + (or s + (erc-replace-regexp-in-string "\n" "" (yow)))) + +(defun erc-quit-reason-various (s) + "Choose a quit reason based on S (a string)." + (when (featurep 'xemacs) (require 'poe)) + (let ((res (car (assoc-default (or s "") + erc-quit-reason-various-alist 'string-match)))) + (cond + ((functionp res) (funcall res)) + ((stringp res) res) + ;; hopefully never reached + (s)))) + +(defun erc-part-reason-normal (&optional s) + "Normal part message. + +If S is non-nil, it will be used as the quit reason." + (or s + (format "\C-bERC\C-b %s (IRC client for Emacs)"; - \C-b%s\C-b" + erc-version-string) ; erc-official-location) + )) + +(defun erc-part-reason-zippy (&optional s) + "Zippy part message. + +If S is non-nil, it will be used as the quit reason." + (or s + (erc-replace-regexp-in-string "\n" "" (yow)))) + +(defun erc-part-reason-various (s) + "Choose a part reason based on S (a string)." + (when (featurep 'xemacs) (require 'poe)) + (let ((res (car (assoc-default (or s "") + erc-part-reason-various-alist 'string-match)))) + (cond + ((functionp res) (funcall res)) + ((stringp res) res) + (s)))) + +(defun erc-cmd-QUIT (reason) + "Disconnect from the current server. +If REASON is omitted, display a default quit message, otherwise display +the message given by REASON." + (unless reason + (setq reason "")) + (cond + ((string-match "^\\s-*\\(.*\\)$" reason) + (let* ((s (match-string 1 reason)) + (buffer (erc-server-buffer)) + (reason (funcall erc-quit-reason (if (equal s "") nil s)))) + (with-current-buffer (if (and buffer + (bufferp buffer)) + buffer + (current-buffer)) + (erc-log (format "cmd: QUIT: %s" reason)) + (setq erc-server-quitting t) + (erc-set-active-buffer (erc-server-buffer)) + (erc-server-send (format "QUIT :%s" reason))) + (run-hook-with-args 'erc-quit-hook erc-server-process) + (when erc-kill-queries-on-quit + (erc-kill-query-buffers erc-server-process))) + t) + (t nil))) + +(defalias 'erc-cmd-BYE 'erc-cmd-QUIT) +(defalias 'erc-cmd-EXIT 'erc-cmd-QUIT) +(defalias 'erc-cmd-SIGNOFF 'erc-cmd-QUIT) +(put 'erc-cmd-QUIT 'do-not-parse-args t) + +(defun erc-cmd-GQUIT (reason) + "Disconnect from all servers at once with the same quit REASON." + (erc-with-all-buffers-of-server nil #'(lambda () + (and (erc-server-buffer-p) + (erc-server-process-alive))) + (erc-cmd-QUIT reason))) + +(defalias 'erc-cmd-GQ 'erc-cmd-GQUIT) +(put 'erc-cmd-GQUIT 'do-not-parse-args t) + +(defun erc-cmd-SERVER (server) + "Connect to SERVER, leaving existing connection intact." + (erc-log (format "cmd: SERVER: %s" server)) + (condition-case nil + (erc-select :server server :nick (erc-current-nick)) + (error + (message "Cannot find host %s." server) + (beep))) + t) + +(eval-when-compile + (defvar motif-version-string) + (defvar gtk-version-string)) + +(defun erc-cmd-SV () + "Say the current ERC and Emacs version into channel." + (erc-send-message (format "I'm using ERC %s with %s %s (%s%s%s)!" + erc-version-string + (if (featurep 'xemacs) "XEmacs" "GNU Emacs") + emacs-version + system-configuration + (concat + (cond ((featurep 'motif) + (concat ", " (substring + motif-version-string 4))) + ((featurep 'gtk) + (concat ", GTK+ Version " + gtk-version-string)) + ((featurep 'mac-carbon) ", Mac Carbon") + ((featurep 'x-toolkit) ", X toolkit") + (t "")) + (if (and (boundp 'x-toolkit-scroll-bars) + (memq x-toolkit-scroll-bars + '(xaw xaw3d))) + (format ", %s scroll bars" + (capitalize (symbol-name + x-toolkit-scroll-bars))) + "") + (if (featurep 'multi-tty) ", multi-tty" "")) + (concat ", built " erc-emacs-build-time))) + t) + +(defun erc-cmd-SM () + "Say the current ERC modes into channel." + (erc-send-message (format "I'm using the following modules: %s!" + (erc-modes))) + t) + +(defun erc-cmd-SMV () + "Say the current ERC module versions into channel." + (erc-send-message (format "I'm using the following module revisions: %s!" + (erc-version-modules))) + t) + +(defun erc-cmd-DEOP (&rest people) + "Remove the operator setting from user(s) given in PEOPLE." + (when (> (length people) 0) + (erc-server-send (concat "MODE " (erc-default-target) + " -" + (make-string (length people) ?o) + " " + (mapconcat 'identity people " "))) + t)) + +(defun erc-cmd-OP (&rest people) + "Add the operator setting to users(s) given in PEOPLE." + (when (> (length people) 0) + (erc-server-send (concat "MODE " (erc-default-target) + " +" + (make-string (length people) ?o) + " " + (mapconcat 'identity people " "))) + t)) + +(defun erc-cmd-TIME (&optional line) + "Request the current time and date from the current server." + (cond + ((and line (string-match "^\\s-*\\(.*\\)$" line)) + (let ((args (match-string 1 line))) + (erc-log (format "cmd: TIME: %s" args)) + (erc-server-send (concat "TIME " args))) + t) + (t (erc-server-send "TIME")))) +(defalias 'erc-cmd-DATE 'erc-cmd-TIME) + +(defun erc-cmd-TOPIC (topic) + "Set or request the topic for a channel. +LINE has the format: \"#CHANNEL TOPIC\", \"#CHANNEL\", \"TOPIC\" +or the empty string. + +If no #CHANNEL is given, the default channel is used. If TOPIC is +given, the channel topic is modified, otherwise the current topic will +be displayed." + (cond + ;; /topic #channel TOPIC + ((string-match "^\\s-*\\([&#+!]\\S-+\\)\\s-\\(.*\\)$" topic) + (let ((ch (match-string 1 topic)) + (topic (match-string 2 topic))) + (erc-log (format "cmd: TOPIC [%s]: %s" ch topic)) + (erc-server-send (format "TOPIC %s :%s" ch topic) nil ch)) + t) + ;; /topic #channel + ((string-match "^\\s-*\\([&#+!]\\S-+\\)" topic) + (let ((ch (match-string 1 topic))) + (erc-server-send (format "TOPIC %s" ch) nil ch) + t)) + ;; /topic + ((string-match "^\\s-*$" topic) + (let ((ch (erc-default-target))) + (erc-server-send (format "TOPIC %s" ch) nil ch) + t)) + ;; /topic TOPIC + ((string-match "^\\s-*\\(.*\\)$" topic) + (let ((ch (erc-default-target)) + (topic (match-string 1 topic))) + (if (and ch (erc-channel-p ch)) + (progn + (erc-log (format "cmd: TOPIC [%s]: %s" ch topic)) + (erc-server-send (format "TOPIC %s :%s" ch topic) nil ch)) + (erc-display-message nil 'error (current-buffer) 'no-target))) + t) + (t nil))) +(defalias 'erc-cmd-T 'erc-cmd-TOPIC) +(put 'erc-cmd-TOPIC 'do-not-parse-args t) + +(defun erc-cmd-APPENDTOPIC (topic) + "Append TOPIC to the current channel topic, separated by a space." + (let ((oldtopic erc-channel-topic)) + ;; display help when given no arguments + (when (string-match "^\\s-*$" topic) + (signal 'wrong-number-of-arguments nil)) + ;; strip trailing ^O + (when (string-match "\\(.*\\)\C-o" oldtopic) + (erc-cmd-TOPIC (concat (match-string 1 oldtopic) topic))))) +(defalias 'erc-cmd-AT 'erc-cmd-APPENDTOPIC) +(put 'erc-cmd-APPENDTOPIC 'do-not-parse-args t) + +(defun erc-cmd-CLEARTOPIC (&optional channel) + "Clear the topic for a CHANNEL. +If CHANNEL is not specified, clear the topic for the default channel." + (interactive "sClear topic of channel (RET is current channel): ") + (let ((chnl (or (and (erc-channel-p channel) channel) (erc-default-target)))) + (when chnl + (erc-server-send (format "TOPIC %s :" chnl)) + t))) + +;;; Banlists + +(defvar erc-channel-banlist nil + "A list of bans seen for the current channel. + +Each ban is an alist of the form: + (WHOSET . MASK) + +The property `received-from-server' indicates whether +or not the ban list has been requested from the server.") +(make-variable-buffer-local 'erc-channel-banlist) +(put 'erc-channel-banlist 'received-from-server nil) + +(defun erc-cmd-BANLIST () + "Pretty-print the contents of `erc-channel-banlist'. + +The ban list is fetched from the server if necessary." + (let ((chnl (erc-default-target)) + (chnl-name (buffer-name))) + + (cond + ((not (erc-channel-p chnl)) + (erc-display-line (erc-make-notice "You're not on a channel\n") + 'active)) + + ((not (get 'erc-channel-banlist 'received-from-server)) + (let ((old-367-hook erc-server-367-functions)) + (setq erc-server-367-functions 'erc-banlist-store + erc-channel-banlist nil) + ;; fetch the ban list then callback + (with-current-buffer (erc-server-buffer) + (erc-once-with-server-event + 368 + `(with-current-buffer ,chnl-name + (put 'erc-channel-banlist 'received-from-server t) + (setq erc-server-367-functions ',old-367-hook) + (erc-cmd-BANLIST) + t)) + (erc-server-send (format "MODE %s b" chnl))))) + + ((null erc-channel-banlist) + (erc-display-line (erc-make-notice + (format "No bans for channel: %s\n" chnl)) + 'active) + (put 'erc-channel-banlist 'received-from-server nil)) + + (t + (let* ((erc-fill-column (or (and (boundp 'erc-fill-column) + erc-fill-column) + (and (boundp 'fill-column) + fill-column) + (1- (window-width)))) + (separator (make-string erc-fill-column (string-to-char "="))) + (fmt (concat + "%-" (number-to-string (/ erc-fill-column 2)) "s" + "%" (number-to-string (/ erc-fill-column 2)) "s"))) + + (erc-display-line + (erc-make-notice (format "Ban list for channel: %s\n" + (erc-default-target))) + 'active) + + (erc-display-line separator 'active) + (erc-display-line (format fmt "Ban Mask" "Banned By") 'active) + (erc-display-line separator 'active) + + (mapc + (lambda (x) + (erc-display-line + (format fmt + (truncate-string-to-width (cdr x) (/ erc-fill-column 2)) + (if (car x) + (truncate-string-to-width (car x) (/ erc-fill-column 2)) + "")) + 'active)) + erc-channel-banlist) + + (erc-display-line (erc-make-notice "End of Ban list") + 'active) + (put 'erc-channel-banlist 'received-from-server nil))))) + t) + +(defalias 'erc-cmd-BL 'erc-cmd-BANLIST) + +(defun erc-cmd-MASSUNBAN () + "Mass Unban. + +Unban all currently banned users in the current channel." + (let ((chnl (erc-default-target))) + (cond + + ((not (erc-channel-p chnl)) + (erc-display-line + (erc-make-notice "You're not on a channel\n") + 'active)) + + ((not (get 'erc-channel-banlist 'received-from-server)) + (let ((old-367-hook erc-server-367-functions)) + (setq erc-server-367-functions 'erc-banlist-store) + ;; fetch the ban list then callback + (with-current-buffer (erc-server-buffer) + (erc-once-with-server-event + 368 + `(with-current-buffer ,chnl + (put 'erc-channel-banlist 'received-from-server t) + (setq erc-server-367-functions ,old-367-hook) + (erc-cmd-MASSUNBAN) + t)) + (erc-server-send (format "MODE %s b" chnl))))) + + (t (let ((bans (mapcar 'cdr erc-channel-banlist))) + (when bans + ;; Glob the bans into groups of three, and carry out the unban. + ;; eg. /mode #foo -bbb a*!*@* b*!*@* c*!*@* + (mapc + (lambda (x) + (erc-server-send + (format "MODE %s -%s %s" (erc-default-target) + (make-string (length x) (string-to-char "b")) + (mapconcat 'identity x " ")))) + (erc-group-list bans 3)))) + t)))) + +(defalias 'erc-cmd-MUB 'erc-cmd-MASSUNBAN) + +;;;; End of IRC commands + +(defun erc-ensure-channel-name (channel) + "Return CHANNEL if it is a valid channel name. +Eventually add a # in front of it, if that turns it into a valid channel name." + (if (erc-channel-p channel) + channel + (concat "#" channel))) + +(defun erc-grab-region (start end) + "Copy the region between START and END in a recreatable format. + +Converts all the IRC text properties in each line of the region +into control codes and writes them to a separate buffer. The +resulting text may be used directly as a script to generate this +text again." + (interactive "r") + (erc-set-active-buffer (current-buffer)) + (save-excursion + (let* ((cb (current-buffer)) + (buf (generate-new-buffer erc-grab-buffer-name)) + (region (buffer-substring start end)) + (lines (erc-split-multiline-safe region))) + (set-buffer buf) + (dolist (line lines) + (insert (concat line "\n"))) + (set-buffer cb) + (switch-to-buffer-other-window buf))) + (message "erc-grab-region doesn't grab colors etc. anymore. If you use this, please tell the maintainers.") + (ding)) + +(defun erc-display-prompt (&optional buffer pos prompt face) + "Display PROMPT in BUFFER at position POS. +Display an ERC prompt in BUFFER. + +If PROMPT is nil, one is constructed with the function `erc-prompt'. +If BUFFER is nil, the `current-buffer' is used. +If POS is nil, PROMPT will be displayed at `point'. +If FACE is non-nil, it will be used to propertize the prompt. If it is nil, +`erc-prompt-face' will be used." + (let* ((prompt (or prompt (erc-prompt))) + (l (length prompt)) + (ob (current-buffer))) + ;; We cannot use save-excursion because we move point, therefore + ;; we resort to the ol' ob trick to restore this. + (when (and buffer (bufferp buffer)) + (set-buffer buffer)) + + ;; now save excursion again to store where point and mark are + ;; in the current buffer + (save-excursion + (setq pos (or pos (point))) + (goto-char pos) + (when (> l 0) + ;; Do not extend the text properties when typing at the end + ;; of the prompt, but stuff typed in front of the prompt + ;; shall remain part of the prompt. + (setq prompt (erc-propertize prompt + 'start-open t ; XEmacs + 'rear-nonsticky t ; Emacs + 'erc-prompt t + 'front-sticky t + 'read-only t)) + (erc-put-text-property 0 (1- (length prompt)) + 'face (or face 'erc-prompt-face) + prompt) + (insert prompt)) + ;; Set the input marker + (set-marker erc-input-marker (point))) + + ;; Now we are back at the old position. If the prompt was + ;; inserted here or before us, advance point by the length of + ;; the prompt. + (when (or (not pos) (<= (point) pos)) + (forward-char l)) + ;; Clear the undo buffer now, so the user can undo his stuff, + ;; but not the stuff we did. Sneaky! + (setq buffer-undo-list nil) + (set-buffer ob))) + +;; interactive operations + +(defun erc-input-message () + "Read input from the minibuffer." + (interactive) + (let ((minibuffer-allow-text-properties t) + (read-map minibuffer-local-map)) + (insert (read-from-minibuffer "Message: " + (string last-command-char) read-map)) + (erc-send-current-line))) + +(defvar erc-action-history-list () + "History list for interactive action input.") + +(defun erc-input-action () + "Interactively input a user action and send it to IRC." + (interactive "") + (erc-set-active-buffer (current-buffer)) + (let ((action (read-from-minibuffer + "Action: " nil nil nil 'erc-action-history-list))) + (if (not (string-match "^\\s-*$" action)) + (erc-send-action (erc-default-target) action)))) + +(defun erc-join-channel (channel &optional key) + "Join CHANNEL. + +If `point' is at the beginning of a channel name, use that as default." + (interactive + (list + (let ((chnl (if (looking-at "\\([&#+!][^ ]+\\)") (match-string 1) "")) + (table (when (erc-server-buffer-live-p) + (set-buffer (process-buffer erc-server-process)) + erc-channel-list))) + (completing-read "Join channel: " table nil nil nil nil chnl)) + (when erc-prompt-for-channel-key + (read-from-minibuffer "Channel key (RET for none): " nil)))) + (erc-cmd-JOIN channel (when (>= (length key) 1) key))) + +(defun erc-part-from-channel (reason) + "Part from the current channel and prompt for a REASON." + (interactive + (list + (if (and (boundp 'reason) (stringp reason) (not (string= reason ""))) + reason + (read-from-minibuffer (concat "Leave " (erc-default-target) + ", Reason? ") + (cons "No reason" 0))))) + (erc-cmd-PART (concat (erc-default-target)" " reason))) + +(defun erc-set-topic (topic) + "Prompt for a TOPIC for the current channel." + (interactive + (list + (read-from-minibuffer + (concat "Set topic of " (erc-default-target) ": ") + (when erc-channel-topic + (cons (apply 'concat (butlast (split-string erc-channel-topic "\C-o"))) + 0))))) + (let ((topic-list (split-string topic "\C-o"))) ; strip off the topic setter + (erc-cmd-TOPIC (concat (erc-default-target) " " (car topic-list))))) + +(defun erc-set-channel-limit (&optional limit) + "Set a LIMIT for the current channel. Remove limit if nil. +Prompt for one if called interactively." + (interactive (list (read-from-minibuffer + (format "Limit for %s (RET to remove limit): " + (erc-default-target))))) + (let ((tgt (erc-default-target))) + (if (and limit (>= (length limit) 1)) + (erc-server-send (format "MODE %s +l %s" tgt limit)) + (erc-server-send (format "MODE %s -l" tgt))))) + +(defun erc-set-channel-key (&optional key) + "Set a KEY for the current channel. Remove key if nil. +Prompt for one if called interactively." + (interactive (list (read-from-minibuffer + (format "Key for %s (RET to remove key): " + (erc-default-target))))) + (let ((tgt (erc-default-target))) + (if (and key (>= (length key) 1)) + (erc-server-send (format "MODE %s +k %s" tgt key)) + (erc-server-send (format "MODE %s -k" tgt))))) + +(defun erc-quit-server (reason) + "Disconnect from current server after prompting for REASON. +`erc-quit-reason' works with this just like with `erc-cmd-QUIT'." + (interactive (list (read-from-minibuffer + (format "Reason for quitting %s: " + (or erc-server-announced-name + erc-session-server))))) + (erc-cmd-QUIT reason)) + +;; Movement of point + +(defun erc-bol () + "Move `point' to the beginning of the current line. + +This places `point' just after the prompt, or at the beginning of the line." + (interactive) + (forward-line 0) + (when (get-text-property (point) 'erc-prompt) + (goto-char erc-input-marker)) + (point)) + +(defun erc-kill-input () + "Kill current input line using `erc-bol' followed by `kill-line'." + (interactive) + (when (and (erc-bol) + (/= (point) (point-max))) ;; Prevent a (ding) and an error when + ;; there's nothing to kill + (if (boundp 'erc-input-ring-index) + (setq erc-input-ring-index nil)) + (kill-line))) + +(defun erc-complete-word () + "Complete the word before point. + +This function uses `erc-complete-functions'." + (interactive) + (unless (run-hook-with-args-until-success 'erc-complete-functions) + (beep))) + +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; +;; +;; IRC SERVER INPUT HANDLING +;; +;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; + +;;;; New Input parsing + +; Stolen from ZenIRC. I just wanna test this code, so here is +; experiment area. + +(defcustom erc-default-server-hook '(erc-debug-missing-hooks + erc-default-server-handler) + "*Default for server messages which aren't covered by `erc-server-hooks'." + :group 'erc-server-hooks + :type 'hook) + +(defun erc-default-server-handler (proc parsed) + "Default server handler. + +Displays PROC and PARSED appropriately using `erc-display-message'." + (erc-display-message + parsed 'notice proc + (mapconcat + 'identity + (let (res) + (mapc #'(lambda (x) + (if (stringp x) + (setq res (append res (list x))))) + parsed) + res) + " "))) + +(defvar erc-server-vectors + '(["msgtype" "sender" "to" "arg1" "arg2" "arg3" "..."]) + "List of received server messages which ERC does not specifically handle. +See `erc-debug-missing-hooks'.") +;(make-variable-buffer-local 'erc-server-vectors) + +(defun erc-debug-missing-hooks (proc parsed) + "Add PARSED server message ERC does not yet handle to `erc-server-vectors'. +These vectors can be helpful when adding new server message handlers to ERC. +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. +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)) + (error "Couldn't switch to server buffer")) + (let ((buf (erc 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)) + +(defcustom erc-auto-query nil + "If non-nil, create a query buffer each time you receive a private message. + +If the buffer doesn't already exist it is created. This can be +set to a symbol, to control how the new query window should +appear. See the documentation for `erc-join-buffer' for +available choices." + :group 'erc-query + :type '(choice (const nil) + (const buffer) + (const window) + (const window-noselect) + (const bury) + (const frame))) + +(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 +bouncer, such as dircproxy, to keep a log of channels when you are +disconnected, you should set this option to t." + :group 'erc-query + :type 'boolean) + +(defcustom erc-format-query-as-channel-p t + "If non-nil, format text from others in a query buffer like in a channel, +otherwise format like a private message." + :group 'erc-query + :type 'boolean) + +(defcustom erc-minibuffer-notice nil + "If non-nil, print ERC notices for the user in the minibuffer. +Only happens when the session buffer isn't visible." + :group 'erc-display + :type 'boolean) + +(defcustom erc-minibuffer-ignored nil + "If non-nil, print a message in the minibuffer if we ignored something." + :group 'erc-ignore + :type 'boolean) + +(defun erc-wash-quit-reason (reason nick login host) + "Remove duplicate text from quit REASON. +Specifically in relation to NICK (user@host) information. Returns REASON +unmodified if nothing can be removed. +E.g. \"Read error to Nick [user@some.host]: 110\" would be shortened to +\"Read error: 110\". The same applies for \"Ping Timeout\"." + (setq nick (regexp-quote nick) + login (regexp-quote login) + host (regexp-quote host)) + (or (when (string-match (concat "^\\(Read error\\) to " + nick "\\[" host "\\]: " + "\\(.+\\)$") reason) + (concat (match-string 1 reason) ": " (match-string 2 reason))) + (when (string-match (concat "^\\(Ping timeout\\) for " + nick "\\[" host "\\]$") reason) + (match-string 1 reason)) + reason)) + +(defun erc-nickname-in-use (nick reason) + "If NICK is unavailable, tell the user the REASON. + +See also `erc-display-error-notice'." + (if (or erc-manual-set-nick-on-bad-nick-p + ;; how many default-nicks are left + one more try... + (eq erc-nick-change-attempt-count + (if (consp erc-nick) + (+ (length erc-nick) 1) + 1))) + (erc-display-error-notice + nil + (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" + (with-current-buffer (erc-server-buffer) + erc-server-parameters))))) + (setq erc-bad-nick t) + ;; try to use a different nick + (if erc-default-nicks + (setq erc-default-nicks (cdr erc-default-nicks))) + (if (not newnick) + (setq newnick (concat (truncate-string-to-width + nick + (if (and erc-server-connected nicklen) + (- (string-to-number nicklen) 1) + ;; rfc2812 max nick length = 9 + ;; we must assume this is the + ;; server's setting if we haven't + ;; established a connection yet + 8)) + erc-nick-uniquifier))) + (erc-cmd-NICK newnick) + (erc-display-error-notice + nil + (format "Nickname %s is %s, trying %s" + nick reason newnick))))) + +;;; Server messages + +(defgroup erc-server-hooks nil + "Server event callbacks. +Every server event - like numeric replies - has it's own hook. +Those hooks are all called using `run-hook-with-args-until-success'. +They receive as first argument the process object from where the event +originated from, +and as second argument the event parsed as a vector." + :group 'erc-hooks) + +(defun erc-display-server-message (proc parsed) + "Display the message sent by the server as a notice." + (erc-display-message + parsed 'notice 'active (erc-response.contents parsed))) + +(defun erc-auto-query (proc parsed) + ;; FIXME: This needs more documentation, unless it's not a user function -- + ;; Lawrence 2004-01-08 + "Put this on `erc-server-PRIVMSG-functions'." + (when erc-auto-query + (let* ((nick (car (erc-parse-user (erc-response.sender parsed)))) + (target (car (erc-response.command-args parsed))) + (msg (erc-response.contents parsed)) + (query (if (not erc-query-on-unjoined-chan-privmsg) + nick + (if (erc-current-nick-p target) + nick + target)))) + (and (not (erc-ignored-user-p (erc-response.sender parsed))) + (or erc-query-on-unjoined-chan-privmsg + (string= target (erc-current-nick))) + (not (erc-get-buffer query proc)) + (not (erc-is-message-ctcp-and-not-action-p msg)) + (let ((erc-join-buffer erc-auto-query)) + (erc-cmd-QUERY query)) + nil)))) + +(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)) + +(defun erc-is-message-ctcp-and-not-action-p (message) + "Check if MESSAGE is a CTCP message or not." + (and (erc-is-message-ctcp-p message) + (not (string-match "^\C-a\\ACTION.*\C-a$" message)))) + +(defun erc-format-privmessage (nick msg privp msgp) + "Format a PRIVMSG in an insertible fashion." + (let* ((mark-s (if msgp (if privp "*" "<") "-")) + (mark-e (if msgp (if privp "*" ">") "-")) + (str (format "%s%s%s %s" mark-s nick mark-e msg)) + (nick-face (if privp 'erc-nick-msg-face 'erc-nick-default-face)) + (msg-face (if privp 'erc-direct-msg-face 'erc-default-face))) + ;; add text properties to text before the nick, the nick and after the nick + (erc-put-text-property 0 (length mark-s) 'face msg-face str) + (erc-put-text-property (length mark-s) (+ (length mark-s) (length nick)) + 'face nick-face str) + (erc-put-text-property (+ (length mark-s) (length nick)) (length str) + 'face msg-face str) + str)) + +(defcustom erc-format-nick-function 'erc-format-nick + "*Function to format a nickname for message display." + :group 'erc-display + :type 'function) + +(defun erc-format-nick (&optional user channel-data) + "Standard nickname formatting function. Only returns the value +of NICK." + (if user + (erc-server-user-nickname user))) + +(defun erc-format-@nick (&optional user channel-data) + "Format a nickname such that @ or + are prefix for the NICK +if OP or VOICE are t respectively." + (if user + (let (op voice) + (if channel-data + (setq op (erc-channel-user-op channel-data) + voice (erc-channel-user-voice channel-data))) + (concat (if voice "+" "") + (if op "@" "") + (erc-server-user-nickname user))))) + +(defun erc-format-my-nick () + "Return the beginning of this user's message, correctly propertized" + (if erc-show-my-nick + (let ((open "<") + (close "> ") + (nick (erc-current-nick))) + (concat + (erc-propertize open 'face 'erc-default-face) + (erc-propertize nick 'face 'erc-nick-default-face) + (erc-propertize close 'face 'erc-default-face))) + (let ((prefix "> ")) + (erc-propertize prefix 'face 'erc-default-face)))) + +(defun erc-echo-notice-in-default-buffer (s parsed buffer sender) + "Echos a private notice in the default buffer, namely the +target buffer specified by BUFFER, or there is no target buffer, +the server buffer. This function is designed to be added to +either `erc-echo-notice-hook' or `erc-echo-notice-always-hook', +and always returns t." + (erc-display-message parsed nil buffer s) + t) + +(defun erc-echo-notice-in-target-buffer (s parsed buffer sender) + "Echos a private notice in BUFFER, if BUFFER is non-nil. This +function is designed to be added to either `erc-echo-notice-hook' +or `erc-echo-notice-always-hook', and returns non-nil iff BUFFER +is non-nil." + (if buffer + (progn (erc-display-message parsed nil buffer s) t) + nil)) + +(defun erc-echo-notice-in-minibuffer (s parsed buffer sender) + "Echos a private notice in the minibuffer. This function is +designed to be added to either `erc-echo-notice-hook' or +`erc-echo-notice-always-hook', and always returns t." + (message "%s" (concat "NOTICE: " s)) + t) + +(defun erc-echo-notice-in-server-buffer (s parsed buffer sender) + "Echos a private notice in the server buffer. This function is +designed to be added to either `erc-echo-notice-hook' or +`erc-echo-notice-always-hook', and always returns t." + (erc-display-message parsed nil nil s) + t) + +(defun erc-echo-notice-in-active-non-server-buffer (s parsed buffer sender) + "Echos a private notice in the active buffer if the active +buffer is not the server buffer. This function is designed to be +added to either `erc-echo-notice-hook' or +`erc-echo-notice-always-hook', and returns non-nil iff the active +buffer is not the server buffer." + (if (not (eq (erc-server-buffer) (erc-active-buffer))) + (progn (erc-display-message parsed nil 'active s) t) + nil)) + +(defun erc-echo-notice-in-active-buffer (s parsed buffer sender) + "Echos a private notice in the active buffer. This function is +designed to be added to either `erc-echo-notice-hook' or +`erc-echo-notice-always-hook', and always returns t." + (erc-display-message parsed nil 'active s) + t) + +(defun erc-echo-notice-in-user-buffers (s parsed buffer sender) + "Echos a private notice in all of the buffers for which SENDER +is a member. This function is designed to be added to either +`erc-echo-notice-hook' or `erc-echo-notice-always-hook', and +returns non-nil iff there is at least one buffer for which the +sender is a member. + +See also: `erc-echo-notice-in-first-user-buffer', +`erc-buffer-list-with-nick'" + (let ((buffers (erc-buffer-list-with-nick sender erc-server-process))) + (if buffers + (progn (erc-display-message parsed nil buffers s) t) + nil))) + +(defun erc-echo-notice-in-user-and-target-buffers (s parsed buffer sender) + "Echos a private notice in BUFFER and in all of the buffers for +which SENDER is a member. This function is designed to be added +to either `erc-echo-notice-hook' or +`erc-echo-notice-always-hook', and returns non-nil iff there is +at least one buffer for which the sender is a member or the +default target. + +See also: `erc-echo-notice-in-user-buffers', +`erc-buffer-list-with-nick'" + (let ((buffers (erc-buffer-list-with-nick sender erc-server-process))) + (add-to-list 'buffers buffer) + (if buffers + (progn (erc-display-message parsed nil buffers s) t) + nil))) + +(defun erc-echo-notice-in-first-user-buffer (s parsed buffer sender) + "Echos a private notice in one of the buffers for which SENDER +is a member. This function is designed to be added to either +`erc-echo-notice-hook' or `erc-echo-notice-always-hook', and +returns non-nil iff there is at least one buffer for which the +sender is a member. + +See also: `erc-echo-notice-in-user-buffers', +`erc-buffer-list-with-nick'" + (let ((buffers (erc-buffer-list-with-nick sender erc-server-process))) + (if buffers + (progn (erc-display-message parsed nil (car buffers) s) t) + nil))) + +;;; Ban manipulation + +(defun erc-banlist-store (proc parsed) + "Record ban entries for a channel." + (multiple-value-bind (channel mask whoset) + (cdr (erc-response.command-args parsed)) + ;; Determine to which buffer the message corresponds + (let ((buffer (erc-get-buffer channel proc))) + (with-current-buffer buffer + (unless (member (cons whoset mask) erc-channel-banlist) + (setq erc-channel-banlist (cons (cons whoset mask) + erc-channel-banlist)))))) + nil) + +(defun erc-banlist-finished (proc parsed) + "Record that we have received the banlist." + (let* ((channel (second (erc-response.command-args parsed))) + (buffer (erc-get-buffer channel proc))) + (with-current-buffer buffer + (put 'erc-channel-banlist 'received-from-server t))) + t) ; suppress the 'end of banlist' message + +(defun erc-banlist-update (proc parsed) + "Check MODE commands for bans and update the banlist appropriately." + ;; FIXME: Possibly incorrect. -- Lawrence 2004-05-11 + (let* ((tgt (first (erc-response.command-args parsed))) + (mode (erc-response.contents parsed)) + (whoset (erc-response.sender parsed)) + (buffer (erc-get-buffer tgt proc))) + (when buffer + (with-current-buffer buffer + (cond ((not (get 'erc-channel-banlist 'received-from-server)) nil) + ((string-match "^\\([+-]\\)b" mode) + ;; This is a ban + (cond + ((string-match "^-" mode) + ;; Remove the unbanned masks from the ban list + (setq erc-channel-banlist + (erc-delete-if + #'(lambda (y) + (member (upcase (cdr y)) + (mapcar #'upcase + (cdr (split-string mode))))) + erc-channel-banlist))) + ((string-match "^+" mode) + ;; Add the banned mask(s) to the ban list + (mapc + (lambda (mask) + (unless (member (cons whoset mask) erc-channel-banlist) + (setq erc-channel-banlist + (cons (cons whoset mask) erc-channel-banlist)))) + (cdr (split-string mode)))))))))) + nil) + +;; used for the banlist cmds +(defun erc-group-list (list n) + "Group LIST into sublists of length N." + (cond ((null list) nil) + ((null (nthcdr n list)) (list list)) + (t (cons (erc-subseq list 0 n) (erc-group-list (nthcdr n list) n))))) + + +;;; MOTD numreplies + +(defun erc-handle-login () + "Handle the logging in process of connection." + (unless erc-logged-in + (setq erc-logged-in t) + (message "Logging in as \'%s\'... done" (erc-current-nick)) + ;; execute a startup script + (let ((f (erc-select-startup-file))) + (when f + (erc-load-script f))))) + +(defun erc-connection-established (proc parsed) + "Run just after connection. + +Set user modes and run `erc-after-connect hook'." + (unless erc-server-connected ; only once per session + (let ((server (or erc-server-announced-name (erc-response.sender parsed))) + (nick (car (erc-response.command-args parsed )))) + (setq erc-server-connected t) + (erc-update-mode-line) + (erc-set-initial-user-mode nick) + (erc-server-setup-periodical-server-ping) + (run-hook-with-args 'erc-after-connect server nick)))) + +(defun erc-set-initial-user-mode (nick) + "If `erc-user-mode' is non-nil for NICK, set the user modes." + (when erc-user-mode + (let ((mode (if (functionp erc-user-mode) + (funcall erc-user-mode) + erc-user-mode))) + (when (stringp mode) + (erc-log (format "changing mode for %s to %s" nick mode)) + (erc-server-send (format "MODE %s %s" nick mode)))))) + +(defun erc-display-error-notice (parsed string) + "Display STRING as an error notice. + +See also `erc-display-message'." + (erc-display-message + parsed '(notice error) 'active string)) + +(defun erc-process-ctcp-query (proc parsed nick login host) + ;; FIXME: This needs a proper docstring -- Lawrence 2004-01-08 + "Process a CTCP query." + (let ((queries (delete "" (split-string (erc-response.contents parsed) + "\C-a")))) + (if (> (length queries) 4) + (erc-display-message + parsed (list 'notice 'error) proc 'ctcp-too-many) + (if (= 0 (length queries)) + (erc-display-message + parsed (list 'notice 'error) proc + 'ctcp-empty ?n nick) + (while queries + (let* ((type (upcase (car (split-string (car queries))))) + (hook (intern-soft (concat "erc-ctcp-query-" type "-hook")))) + (if (and hook (boundp hook)) + (if (string-equal type "ACTION") + (run-hook-with-args-until-success + hook proc parsed nick login host + (car (erc-response.command-args parsed)) + (car queries)) + (when erc-paranoid + (if (erc-current-nick-p + (car (erc-response.command-args parsed))) + (erc-display-message + parsed 'error 'active 'ctcp-request + ?n nick ?u login ?h host ?r (car queries)) + (erc-display-message + parsed 'error 'active 'ctcp-request-to + ?n nick ?u login ?h host ?r (car queries) + ?t (car (erc-response.command-args parsed))))) + (run-hook-with-args-until-success + hook proc nick login host + (car (erc-response.command-args parsed)) + (car queries))) + (erc-display-message + parsed (list 'notice 'error) proc + 'undefined-ctcp))) + (setq queries (cdr queries))))))) + +(defvar erc-ctcp-query-ACTION-hook '(erc-ctcp-query-ACTION)) + +(defun erc-ctcp-query-ACTION (proc parsed nick login host to msg) + "Respond to a CTCP ACTION query." + (when (string-match "^ACTION\\s-\\(.*\\)\\s-*$" msg) + (let ((s (match-string 1 msg)) + (buf (or (erc-get-buffer to proc) + (erc-get-buffer nick proc) + (process-buffer proc)))) + (erc-display-message + parsed 'action buf + 'ACTION ?n nick ?u login ?h host ?a s)))) + +(defvar erc-ctcp-query-CLIENTINFO-hook '(erc-ctcp-query-CLIENTINFO)) + +(defun erc-ctcp-query-CLIENTINFO (proc nick login host to msg) + "Respond to a CTCP CLIENTINFO query." + (when (string-match "^CLIENTINFO\\(\\s-*\\|\\s-+.*\\)$" msg) + (let ((s (erc-client-info (erc-trim-string (match-string 1 msg))))) + (unless erc-disable-ctcp-replies + (erc-send-ctcp-notice nick (format "CLIENTINFO %s" s))))) + nil) + +(defvar erc-ctcp-query-ECHO-hook '(erc-ctcp-query-ECHO)) +(defun erc-ctcp-query-ECHO (proc nick login host to msg) + "Respond to a CTCP ECHO query." + (when (string-match "^ECHO\\s-+\\(.*\\)\\s-*$" msg) + (let ((s (match-string 1 msg))) + (unless erc-disable-ctcp-replies + (erc-send-ctcp-notice nick (format "ECHO %s" s))))) + nil) + +(defvar erc-ctcp-query-FINGER-hook '(erc-ctcp-query-FINGER)) +(defun erc-ctcp-query-FINGER (proc nick login host to msg) + "Respond to a CTCP FINGER query." + (unless erc-disable-ctcp-replies + (let ((s (if erc-anonymous-login + (format "FINGER I'm %s." (erc-current-nick)) + (format "FINGER %s (%s@%s)." + (user-full-name) + (user-login-name) + (system-name)))) + (ns (erc-time-diff erc-server-last-sent-time (erc-current-time)))) + (when (> ns 0) + (setq s (concat s " Idle for " (erc-sec-to-time ns)))) + (erc-send-ctcp-notice nick s))) + nil) + +(defvar erc-ctcp-query-PING-hook '(erc-ctcp-query-PING)) +(defun erc-ctcp-query-PING (proc nick login host to msg) + "Respond to a CTCP PING query." + (when (string-match "^PING\\s-+\\(.*\\)" msg) + (unless erc-disable-ctcp-replies + (let ((arg (match-string 1 msg))) + (erc-send-ctcp-notice nick (format "PING %s" arg))))) + nil) + +(defvar erc-ctcp-query-TIME-hook '(erc-ctcp-query-TIME)) +(defun erc-ctcp-query-TIME (proc nick login host to msg) + "Respond to a CTCP TIME query." + (unless erc-disable-ctcp-replies + (erc-send-ctcp-notice nick (format "TIME %s" (current-time-string)))) + nil) + +(defvar erc-ctcp-query-USERINFO-hook '(erc-ctcp-query-USERINFO)) +(defun erc-ctcp-query-USERINFO (proc nick login host to msg) + "Respond to a CTCP USERINFO query." + (unless erc-disable-ctcp-replies + (erc-send-ctcp-notice nick (format "USERINFO %s" erc-user-information))) + nil) + +(defvar erc-ctcp-query-VERSION-hook '(erc-ctcp-query-VERSION)) +(defun erc-ctcp-query-VERSION (proc nick login host to msg) + "Respond to a CTCP VERSION query." + (unless erc-disable-ctcp-replies + (erc-send-ctcp-notice + nick (format + "VERSION \C-bERC\C-b %s - an IRC client for emacs (\C-b%s\C-b)" + erc-version-string + erc-official-location))) + nil) + +(defun erc-process-ctcp-reply (proc parsed nick login host msg) + "Process MSG as a CTCP reply." + (let* ((type (car (split-string msg))) + (hook (intern (concat "erc-ctcp-reply-" type "-hook")))) + (if (boundp hook) + (run-hook-with-args-until-success + hook proc nick login host + (car (erc-response.command-args parsed)) msg) + (erc-display-message + parsed 'notice 'active + 'CTCP-UNKNOWN ?n nick ?u login ?h host ?m msg)))) + +(defvar erc-ctcp-reply-ECHO-hook '(erc-ctcp-reply-ECHO)) +(defun erc-ctcp-reply-ECHO (proc nick login host to msg) + "Handle a CTCP ECHO reply." + (when (string-match "^ECHO\\s-+\\(.*\\)\\s-*$" msg) + (let ((message (match-string 1 msg))) + (erc-display-message + nil '(notice action) 'active + 'CTCP-ECHO ?n nick ?m message))) + nil) + +(defvar erc-ctcp-reply-CLIENTINFO-hook '(erc-ctcp-reply-CLIENTINFO)) +(defun erc-ctcp-reply-CLIENTINFO (proc nick login host to msg) + "Handle a CTCP CLIENTINFO reply." + (when (string-match "^CLIENTINFO\\s-+\\(.*\\)\\s-*$" msg) + (let ((message (match-string 1 msg))) + (erc-display-message + nil 'notice 'active + 'CTCP-CLIENTINFO ?n nick ?m message))) + nil) + +(defvar erc-ctcp-reply-FINGER-hook '(erc-ctcp-reply-FINGER)) +(defun erc-ctcp-reply-FINGER (proc nick login host to msg) + "Handle a CTCP FINGER reply." + (when (string-match "^FINGER\\s-+\\(.*\\)\\s-*$" msg) + (let ((message (match-string 1 msg))) + (erc-display-message + nil 'notice 'active + 'CTCP-FINGER ?n nick ?m message))) + nil) + +(defvar erc-ctcp-reply-PING-hook '(erc-ctcp-reply-PING)) +(defun erc-ctcp-reply-PING (proc nick login host to msg) + "Handle a CTCP PING reply." + (if (not (string-match "^PING\\s-+\\([0-9.]+\\)" msg)) + nil + (let ((time (match-string 1 msg))) + (condition-case nil + (let ((delta (erc-time-diff (string-to-number time) + (erc-current-time)))) + (erc-display-message + nil 'notice 'active + 'CTCP-PING ?n nick + ?t (erc-sec-to-time delta))) + (range-error + (erc-display-message + nil 'error 'active + 'bad-ping-response ?n nick ?t time)))))) + +(defvar erc-ctcp-reply-TIME-hook '(erc-ctcp-reply-TIME)) +(defun erc-ctcp-reply-TIME (proc nick login host to msg) + "Handle a CTCP TIME reply." + (when (string-match "^TIME\\s-+\\(.*\\)\\s-*$" msg) + (let ((message (match-string 1 msg))) + (erc-display-message + nil 'notice 'active + 'CTCP-TIME ?n nick ?m message))) + nil) + +(defvar erc-ctcp-reply-VERSION-hook '(erc-ctcp-reply-VERSION)) +(defun erc-ctcp-reply-VERSION (proc nick login host to msg) + "Handle a CTCP VERSION reply." + (when (string-match "^VERSION\\s-+\\(.*\\)\\s-*$" msg) + (let ((message (match-string 1 msg))) + (erc-display-message + nil 'notice 'active + 'CTCP-VERSION ?n nick ?m message))) + nil) + +(defun erc-process-away (proc away-p) + ;; FIXME: This docstring is AWFUL -- Lawrence 2004-01-08 + "Process the user being away, or returning from an away break." + (let ((sessionbuf (process-buffer proc))) + (when sessionbuf + (with-current-buffer sessionbuf + (when erc-away-nickname + (erc-log (format "erc-process-away: away-nick: %s, away-p: %s" + erc-away-nickname away-p)) + (erc-cmd-NICK (if away-p + erc-away-nickname + erc-nick))) + (cond + (away-p + (erc-with-all-buffers-of-server proc nil + (setq erc-away (current-time)))) + (t + (let ((away-time erc-away)) + ;; away must be set to NIL BEFORE sending anything to prevent + ;; an infinite recursion + (erc-with-all-buffers-of-server proc nil + (setq erc-away nil)) + (save-excursion + (set-buffer (erc-active-buffer)) + (when erc-public-away-p + (erc-send-action + (erc-default-target) + (if away-time + (format "is back (gone for %s)" + (erc-sec-to-time + (erc-time-diff + (erc-emacs-time-to-erc-time away-time) + (erc-current-time)))) + "is back"))))))))) + (erc-update-mode-line))) + +;;;; List of channel members handling + +(defun erc-channel-begin-receiving-names () + "Internal function. + +Used when a channel names list is about to be received. Should +be called with the current buffer set to the channel buffer. + +See also `erc-channel-end-receiving-names'." + (setq erc-channel-new-member-names (make-hash-table :test 'equal))) + +(defun erc-channel-end-receiving-names () + "Internal function. + +Used to fix `erc-channel-users' after a channel names list has been +received. Should be called with the current buffer set to the +channel buffer. + +See also `erc-channel-begin-receiving-names'." + (maphash (lambda (nick user) + (if (null (gethash nick erc-channel-new-member-names)) + (erc-remove-channel-user nick))) + erc-channel-users) + (setq erc-channel-new-member-names nil)) + +(defun erc-channel-receive-names (names-string) + "This function is for internal use only. + +Update `erc-channel-users' according to NAMES-STRING. +NAMES-STRING is a string listing some of the names on the +channel." + (let (names name op voice) + ;; We need to delete "" because in XEmacs, (split-string "a ") + ;; returns ("a" ""). + (setq names (delete "" (split-string names-string))) + (let ((erc-channel-members-changed-hook nil)) + (dolist (item names) + (cond ((string-match "^@\\(.*\\)$" item) + (setq name (match-string 1 item) + op 'on + voice 'off)) + ((string-match "^+\\(.*\\)$" item) + (setq name (match-string 1 item) + op 'off + voice 'on)) + (t (setq name item + op 'off + voice 'off))) + (puthash (erc-downcase name) t + erc-channel-new-member-names) + (erc-update-current-channel-member + name name t op voice))) + (run-hooks 'erc-channel-members-changed-hook))) + +(defcustom erc-channel-members-changed-hook nil + "*This hook is called every time the variable `channel-members' changes. +The buffer where the change happened is current while this hook is called." + :group 'erc-hooks + :type 'hook) + +(defun erc-update-user-nick (nick &optional new-nick + host login full-name info) + "Updates the stored user information for the user with nickname +NICK. + +See also: `erc-update-user'" + (erc-update-user (erc-get-server-user nick) new-nick + host login full-name info)) + +(defun erc-update-user (user &optional new-nick + host login full-name info) + "Update user info for USER. USER must be an erc-server-user +struct. Any of NEW-NICK, HOST, LOGIN, FULL-NAME, INFO which are +non-nil and not equal to the existing values for USER are used to +replace the stored values in USER. + +If, any only if a change is made, +`erc-channel-members-changed-hook' is run for each channel for +which USER is a member, and `t' is returned." + (let (changed) + (when user + (when (and new-nick + (not (equal (erc-server-user-nickname user) + new-nick))) + (setq changed t) + (erc-change-user-nickname user new-nick)) + (when (and host + (not (equal (erc-server-user-host user) host))) + (setq changed t) + (setf (erc-server-user-host user) host)) + (when (and login + (not (equal (erc-server-user-login user) login))) + (setq changed t) + (setf (erc-server-user-login user) login)) + (when (and full-name + (not (equal (erc-server-user-full-name user) + full-name))) + (setq changed t) + (setf (erc-server-user-full-name user) full-name)) + (when (and info + (not (equal (erc-server-user-info user) info))) + (setq changed t) + (setf (erc-server-user-info user) info)) + (if changed + (dolist (buf (erc-server-user-buffers user)) + (if (buffer-live-p buf) + (with-current-buffer buf + (run-hooks 'erc-channel-members-changed-hook)))))) + changed)) + +(defun erc-update-current-channel-member + (nick new-nick &optional add op voice host login full-name info + update-message-time) + "Updates the stored user information for the user with nickname +NICK. `erc-update-user' is called to handle changes to nickname, +host, login, full-name, and info. If `op' or `voice' are +non-nil, they must be equal to either `on' or `off', in which +case the operator or voice status of USER in the current channel +is changed accordingly. If `update-message-time' is non-nil, the +last-message-time of the user in the current channel is set +to (current-time). + +If ADD is non-nil, the user will be added with the specified +information if it is not already present in the user or channel +lists. + +If, and only if, changes are made, or the user is added, +`erc-channel-members-updated-hook' is run, and `t' is returned. + +See also: `erc-update-user' and `erc-update-channel-member'." + (let* (changed user-changed + (channel-data (erc-get-channel-user nick)) + (cuser (if channel-data (cdr channel-data))) + (user (if channel-data (car channel-data) + (erc-get-server-user nick)))) + (if cuser + (progn + (erc-log (format "update-member: user = %S, cuser = %S" user cuser)) + (when (and op + (not (eq (erc-channel-user-op cuser) op))) + (setq changed t) + (setf (erc-channel-user-op cuser) + (cond ((eq op 'on) t) + ((eq op 'off) nil) + (t op)))) + (when (and voice + (not (eq (erc-channel-user-voice cuser) voice))) + (setq changed t) + (setf (erc-channel-user-voice cuser) + (cond ((eq voice 'on) t) + ((eq voice 'off) nil) + (t voice)))) + (when update-message-time + (setf (erc-channel-user-last-message-time cuser) (current-time))) + (setq user-changed + (erc-update-user user new-nick + host login full-name info))) + (when add + (if (null user) + (progn + (setq user (make-erc-server-user + :nickname nick + :host host + :full-name full-name + :login login + :info info + :buffers (list (current-buffer)))) + (erc-add-server-user nick user)) + (setf (erc-server-user-buffers user) + (cons (current-buffer) + (erc-server-user-buffers user)))) + (setq cuser (make-erc-channel-user + :op (cond ((eq op 'on) t) + ((eq op 'off) nil) + (t op)) + :voice (cond ((eq voice 'on) t) + ((eq voice 'off) nil) + (t voice)) + :last-message-time + (if update-message-time (current-time)))) + (puthash (erc-downcase nick) (cons user cuser) + erc-channel-users) + (setq changed t))) + (when (and changed (null user-changed)) + (run-hooks 'erc-channel-members-changed-hook)) + (or changed user-changed add))) + +(defun erc-update-channel-member (channel nick new-nick + &optional add op voice host login + full-name info update-message-time) + "Updates user and channel information for the user with +nickname NICK in channel CHANNEL. + +See also: `erc-update-current-channel-member'" + (erc-with-buffer + (channel) + (erc-update-current-channel-member nick new-nick add op voice host + login full-name info + update-message-time))) + +(defun erc-remove-current-channel-member (nick) + "Remove NICK from current channel membership list. Runs +`erc-channel-members-changed-hook'." + (let ((channel-data (erc-get-channel-user nick))) + (when channel-data + (erc-remove-channel-user nick) + (run-hooks 'erc-channel-members-changed-hook)))) + +(defun erc-remove-channel-member (channel nick) + "Remove NICK from CHANNEL's membership list. + +See also `erc-remove-current-channel-member'." + (erc-with-buffer + (channel) + (erc-remove-current-channel-member nick))) + +(defun erc-update-channel-topic (channel topic &optional modify) + "Find a buffer for CHANNEL and set the TOPIC for it. + +If optional MODIFY is 'append or 'prepend, then append or prepend the +TOPIC string to the current topic." + (erc-with-buffer (channel) + (cond ((eq modify 'append) + (setq erc-channel-topic (concat erc-channel-topic topic))) + ((eq modify 'prepend) + (setq erc-channel-topic (concat topic erc-channel-topic))) + (t (setq erc-channel-topic topic))) + (erc-update-mode-line-buffer (current-buffer)))) + +(defun erc-set-modes (tgt mode-string) + "Set the modes for the TGT provided as MODE-STRING." + (let* ((modes (erc-parse-modes mode-string)) + (add-modes (nth 0 modes)) + (remove-modes (nth 1 modes)) + ;; list of triples: (mode-char 'on/'off argument) + (arg-modes (nth 2 modes))) + (cond ((erc-channel-p tgt); channel modes + (let ((buf (and (boundp 'erc-server-process) erc-server-process + (erc-get-buffer tgt erc-server-process)))) + (when buf + (with-current-buffer buf + (setq erc-channel-modes add-modes) + (setq erc-channel-user-limit nil) + (setq erc-channel-key nil) + (while arg-modes + (let ((mode (nth 0 (car arg-modes))) + (onoff (nth 1 (car arg-modes))) + (arg (nth 2 (car arg-modes)))) + (cond ((string-match "^[Ll]" mode) + (erc-update-channel-limit tgt onoff arg)) + ((string-match "^[Kk]" mode) + (erc-update-channel-key tgt onoff arg)) + (t nil))) + (setq arg-modes (cdr arg-modes))) + (erc-update-mode-line-buffer buf))))) + ;; we do not keep our nick's modes yet + ;;(t (setq erc-user-modes add-modes)) + ) + )) + +(defun erc-sort-strings (list-of-strings) + "Sort LIST-OF-STRINGS in lexicographic order. + +Side-effect free." + (sort (copy-sequence list-of-strings) 'string<)) + +(defun erc-parse-modes (mode-string) + "Parse MODE-STRING into a list. + +Returns a list of three elements: + + (ADD-MODES REMOVE-MODES ARG-MODES). + +The add-modes and remove-modes are lists of single-character strings +for modes without parameters to add and remove respectively. The +arg-modes is a list of triples of the form: + + (MODE-CHAR ON/OFF ARGUMENT)." + (if (string-match "^\\s-*\\(\\S-+\\)\\(\\s-.*$\\|$\\)" mode-string) + (let ((chars (mapcar 'char-to-string (match-string 1 mode-string))) + ;; arguments in channel modes + (args-str (match-string 2 mode-string)) + (args nil) + (add-modes nil) + (remove-modes nil) + (arg-modes nil); list of triples: (mode-char 'on/'off argument) + (add-p t)) + ;; make the argument list + (while (string-match "^\\s-*\\(\\S-+\\)\\(\\s-+.*$\\|$\\)" args-str) + (setq args (cons (match-string 1 args-str) args)) + (setq args-str (match-string 2 args-str))) + (setq args (nreverse args)) + ;; collect what modes changed, and match them with arguments + (while chars + (cond ((string= (car chars) "+") (setq add-p t)) + ((string= (car chars) "-") (setq add-p nil)) + ((string-match "^[ovbOVB]" (car chars)) + (setq arg-modes (cons (list (car chars) + (if add-p 'on 'off) + (if args (car args) nil)) + arg-modes)) + (if args (setq args (cdr args)))) + ((string-match "^[LlKk]" (car chars)) + (setq arg-modes (cons (list (car chars) + (if add-p 'on 'off) + (if (and add-p args) + (car args) nil)) + arg-modes)) + (if (and add-p args) (setq args (cdr args)))) + (add-p (setq add-modes (cons (car chars) add-modes))) + (t (setq remove-modes (cons (car chars) remove-modes)))) + (setq chars (cdr chars))) + (setq add-modes (nreverse add-modes)) + (setq remove-modes (nreverse remove-modes)) + (setq arg-modes (nreverse arg-modes)) + (list add-modes remove-modes arg-modes)) + nil)) + +(defun erc-update-modes (tgt mode-string &optional nick host login) + "Update the mode information for TGT, provided as MODE-STRING. +Optional arguments: NICK, HOST and LOGIN - the attributes of the +person who changed the modes." + (let* ((modes (erc-parse-modes mode-string)) + (add-modes (nth 0 modes)) + (remove-modes (nth 1 modes)) + ;; list of triples: (mode-char 'on/'off argument) + (arg-modes (nth 2 modes))) + ;; now parse the modes changes and do the updates + (cond ((erc-channel-p tgt); channel modes + (let ((buf (and (boundp 'erc-server-process) erc-server-process + (erc-get-buffer tgt erc-server-process)))) + (when buf + ;; FIXME! This used to have an original buffer + ;; variable, but it never switched back to the original + ;; buffer. Is this wanted behavior? + (set-buffer buf) + (if (not (boundp 'erc-channel-modes)) + (setq erc-channel-modes nil)) + (while remove-modes + (setq erc-channel-modes (delete (car remove-modes) + erc-channel-modes) + remove-modes (cdr remove-modes))) + (while add-modes + (setq erc-channel-modes (cons (car add-modes) + erc-channel-modes) + add-modes (cdr add-modes))) + (setq erc-channel-modes (erc-sort-strings erc-channel-modes)) + (while arg-modes + (let ((mode (nth 0 (car arg-modes))) + (onoff (nth 1 (car arg-modes))) + (arg (nth 2 (car arg-modes)))) + (cond ((string-match "^[oO]" mode) + (erc-update-channel-member tgt arg arg nil onoff)) + ((string-match "^[Vv]" mode) + (erc-update-channel-member tgt arg arg nil nil + onoff)) + ((string-match "^[Ll]" mode) + (erc-update-channel-limit tgt onoff arg)) + ((string-match "^[Kk]" mode) + (erc-update-channel-key tgt onoff arg)) + (t nil)); only ops are tracked now + (setq arg-modes (cdr arg-modes)))) + (erc-update-mode-line buf)))) + ;; nick modes - ignored at this point + (t nil)))) + +(defun erc-update-channel-limit (channel onoff n) + ;; FIXME: what does ONOFF actually do? -- Lawrence 2004-01-08 + "Update CHANNEL's user limit to N." + (if (or (not (eq onoff 'on)) + (and (stringp n) (string-match "^[0-9]+$" n))) + (erc-with-buffer + (channel) + (cond ((eq onoff 'on) (setq erc-channel-user-limit (string-to-number n))) + (t (setq erc-channel-user-limit nil)))))) + +(defun erc-update-channel-key (channel onoff key) + "Update CHANNEL's key to KEY if ONOFF is 'on or to nil if it's 'off." + (erc-with-buffer + (channel) + (cond ((eq onoff 'on) (setq erc-channel-key key)) + (t (setq erc-channel-key nil))))) + +(defun erc-handle-user-status-change (type nlh &optional l) + "Handle changes in any user's status. + +So far, only nick change is handled. + +Generally, the TYPE argument is a symbol describing the change type, NLH is +a list containing the original nickname, login name and hostname for the user, +and L is a list containing additional TYPE-specific arguments. + +So far the following TYPE/L pairs are supported: + + Event TYPE L + + nickname change 'nick (NEW-NICK)" + (erc-log (format "user-change: type: %S nlh: %S l: %S" type nlh l)) + (cond + ;; nickname change + ((equal type 'nick) + t) + (t + nil))) + +(defun erc-highlight-notice (s) + "Highlight notice message S and return it. +See also variable `erc-notice-highlight-type'" + (cond + ((eq erc-notice-highlight-type 'prefix) + (erc-put-text-property 0 (length erc-notice-prefix) + 'face 'erc-notice-face s) + s) + ((eq erc-notice-highlight-type 'all) + (erc-put-text-property 0 (length s) 'face 'erc-notice-face s) + s) + (t s))) + +(defun erc-make-notice (message) + "Notify the user of MESSAGE." + (when erc-minibuffer-notice + (message "%s" message)) + (erc-highlight-notice (concat erc-notice-prefix message))) + +(defun erc-highlight-error (s) + "Highlight error message S and return it." + (erc-put-text-property 0 (length s) 'face 'erc-error-face s) + s) + +(defun erc-put-text-property (start end property value &optional object) + "Set text-property for an object (usually a string). +START and END define the characters covered. +PROPERTY is the text-property set, usually the symbol `face'. +VALUE is the value for the text-property, usually a face symbol such as +the face `bold' or `erc-pal-face'. +OBJECT is a string which will be modified and returned. +OBJECT is modified without being copied first. + +You can redefine or `defadvice' this function in order to add +EmacsSpeak support." + (put-text-property start end property value object)) + +(defun erc-list (thing) + "Return THING if THING is a list, or a list with THING as its element." + (if (listp thing) + thing + (list thing))) + +(defun erc-parse-user (string) + "Parse STRING as a user specification (nick!login@host). + +Return a list of the three separate tokens." + (cond + ((string-match "^\\([^!]*\\)!\\([^@]*\\)@\\(.*\\)$" string) + (list (match-string 1 string) + (match-string 2 string) + (match-string 3 string))) + ;; Some bogus bouncers send Nick!(null), try to live with that. + ((string-match "^\\([^!]*\\)!\\(.*\\)$" string) + (list (match-string 1 string) + "" + (match-string 2 string))) + (t + (list string "" "")))) + +(defun erc-extract-nick (string) + "Return the nick corresponding to a user specification STRING. + +See also `erc-parse-user'." + (car (erc-parse-user string))) + +(defun erc-put-text-properties (start end properties + &optional object value-list) + "Set text-properties for OBJECT. + +START and END describe positions in OBJECT. +If VALUE-LIST is nil, set each property in PROPERTIES to t, else set +each property to the corresponding value in VALUE-LIST." + (unless value-list + (setq value-list (mapcar (lambda (x) + t) + properties))) + (mapcar* (lambda (prop value) + (erc-put-text-property start end prop value object)) + properties value-list)) + +;;; Input area handling: + +(defun erc-beg-of-input-line () + "Return the value of `point' at the beginning of the input line. + +Specifically, return the position of `erc-insert-marker'." + (or (and (boundp 'erc-insert-marker) + (markerp erc-insert-marker)) + (error "erc-insert-marker has no value, please report a bug")) + (marker-position erc-insert-marker)) + +(defun erc-end-of-input-line () + "Return the value of `point' at the end of the input line." + (point-max)) + +(defun erc-send-current-line () + "Parse current line and send it to IRC." + (interactive) + (save-restriction + (widen) + (cond + ((< (point) (erc-beg-of-input-line)) + (message "Point is not in the input area") + (beep)) + ((not (erc-server-buffer-live-p)) + (message "ERC: No process running") + (beep)) + (t + (erc-set-active-buffer (current-buffer)) + (let ((inhibit-read-only t) + (str (erc-user-input)) + (old-buf (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) + ;; Fix the buffer if the command didn't kill it + (when (buffer-live-p old-buf) + (with-current-buffer old-buf + (save-restriction + (widen) + (goto-char (point-max)) + (set-marker (process-mark erc-server-process) (point)) + (set-marker erc-insert-marker (point)) + (let ((buffer-modified (buffer-modified-p))) + (erc-display-prompt) + (set-buffer-modified-p buffer-modified)))))) + + ;; Only when last hook has been run... + (run-hook-with-args 'erc-send-completed-hook str)))))) + +(defun erc-user-input () + "Return the input of the user in the current buffer." + (buffer-substring + erc-input-marker + (erc-end-of-input-line))) + +(defun erc-send-input (input) + "Treat INPUT as typed in by the user. It is assumed that the input +and the prompt is already deleted. +This returns non-nil only iff 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 + (let ((str input) + (erc-insert-this t)) + (setq erc-send-this t) + (run-hook-with-args 'erc-send-pre-hook input) + (when erc-send-this + (if (or (string-match "\n" str) + (not (char-equal (aref str 0) ?/))) + (mapc + (lambda (line) + (mapc + (lambda (line) + ;; Insert what has to be inserted for this. + (erc-display-msg line) + (erc-process-input-line (concat line "\n") + (null erc-flood-protect) t)) + (erc-split-line line))) + (split-string str "\n")) + ;; Insert the prompt along with the command. + (erc-display-command str) + (erc-process-input-line (concat str "\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) + 'face 'erc-command-indicator-face) + (insert "\n")) + (set-marker (process-mark erc-server-process) (point)) + (set-marker erc-insert-marker (point)) + (save-excursion + (save-restriction + (narrow-to-region insert-position (point)) + (run-hooks 'erc-send-modify-hook) + (run-hooks 'erc-send-post-hook)))))) + +(defun erc-display-msg (line) + "Display LINE as a message of the user to the current target at the +current position." + (when erc-insert-this + (let ((insert-position (point))) + (insert (erc-format-my-nick)) + (let ((beg (point))) + (insert line) + (erc-put-text-property beg (point) + 'face 'erc-input-face)) + (insert "\n") + (set-marker (process-mark erc-server-process) (point)) + (set-marker erc-insert-marker (point)) + (save-excursion + (save-restriction + (narrow-to-region insert-position (point)) + (run-hooks 'erc-send-modify-hook) + (run-hooks 'erc-send-post-hook)))))) + +(defun erc-command-symbol (command) + "Return the erc command symbol for COMMAND if it exists and is bound." + (let ((cmd (intern-soft (format "erc-cmd-%s" (upcase command))))) + (when (fboundp cmd) cmd))) + +(defun erc-extract-command-from-line (line) + "Extract command and args from the input LINE. +If no command was given, return nil. If command matches, return a +list of the form: (command args) where both elements are strings." + (when (string-match "^/\\([A-Za-z]+\\)\\(\\s-+.*\\|\\s-*\\)$" line) + (let* ((cmd (erc-command-symbol (match-string 1 line))) + ;; note: return is nil, we apply this simply for side effects + (canon-defun (while (and cmd (symbolp (symbol-function cmd))) + (setq cmd (symbol-function cmd)))) + (cmd-fun (or cmd #'erc-cmd-default)) + (arg (if cmd + (if (get cmd-fun 'do-not-parse-args) + (format "%s" (match-string 2 line)) + (delete "" (split-string (erc-trim-string + (match-string 2 line)) " "))) + line))) + (list cmd-fun arg)))) + +(defun erc-split-multiline-safe (string) + "Split STRING, containing multiple lines and return them in a list. +Do it only for STRING as the complete input, do not carry unfinished +strings over to the next call." + (let ((l ()) + (i0 0) + (doit t)) + (while doit + (let ((i (string-match "\r?\n" string i0)) + (s (substring string i0))) + (cond (i (setq l (cons (substring string i0 i) l)) + (setq i0 (match-end 0))) + ((> (length s) 0) + (setq l (cons s l))(setq doit nil)) + (t (setq doit nil))))) + (nreverse l))) + +;; nick handling + +(defun erc-set-current-nick (nick) + "Set the current nickname to NICK." + (with-current-buffer (or (erc-server-buffer) + (current-buffer)) + (setq erc-server-current-nick nick))) + +(defun erc-current-nick () + "Return the current nickname." + (with-current-buffer (if (buffer-live-p (erc-server-buffer)) + (erc-server-buffer) + (current-buffer)) + erc-server-current-nick)) + +(defun erc-current-nick-p (nick) + "Return non-nil if NICK is the current nickname." + (erc-nick-equal-p nick (erc-current-nick))) + +(defun erc-nick-equal-p (nick1 nick2) + "Return non-nil if NICK1 and NICK2 are the same. + +This matches strings according to the IRC protocol's case convention. + +See also `erc-downcase'." + (string= (erc-downcase nick1) + (erc-downcase nick2))) + +;; default target handling + +(defun erc-default-target () + "Return the current default target (as a character string) or nil if none." + (let ((tgt (car erc-default-recipients))) + (cond + ((not tgt) nil) + ((listp tgt) (cdr tgt)) + (t tgt)))) + +(defun erc-add-default-channel (channel) + "Add CHANNEL to the default channel list." + + (let ((d1 (car erc-default-recipients)) + (d2 (cdr erc-default-recipients)) + (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." + (let ((ob (current-buffer))) + (with-current-buffer (if (and buffer + (bufferp buffer)) + buffer + (current-buffer)) + (setq erc-default-recipients (delete (downcase channel) + erc-default-recipients))))) + +(defun erc-add-query (nickname) + "Add QUERY'd NICKNAME to the default channel list. + +The previous default target of QUERY type gets removed" + (let ((d1 (car erc-default-recipients)) + (d2 (cdr erc-default-recipients)) + (qt (cons 'QUERY (downcase nickname)))) + (if (and (listp d1) + (eq (car d1) 'QUERY)) + (setq erc-default-recipients (cons qt d2)) + (setq erc-default-recipients (cons qt erc-default-recipients))))) + +(defun erc-delete-query () + "Delete the topmost target if it is a QUERY." + + (let ((d1 (car erc-default-recipients)) + (d2 (cdr erc-default-recipients))) + (if (and (listp d1) + (eq (car d1) 'QUERY)) + (setq erc-default-recipients d2) + (error "Current target is not a QUERY")))) + +(defun erc-ignored-user-p (spec) + "Return non-nil if SPEC matches something in `erc-ignore-list'. + +Takes a full SPEC of a user in the form \"nick!login@host\", and +matches against all the regexp's in `erc-ignore-list'. If any +match, returns that regexp." + (dolist (ignored (with-current-buffer (erc-server-buffer) erc-ignore-list)) + (if (string-match ignored spec) + ;; We have `require'd cl, so we can return from the block named nil + (return ignored)))) + +(defun erc-ignored-reply-p (msg tgt proc) + ;; FIXME: this docstring needs fixing -- Lawrence 2004-01-08 + "Return non-nil if MSG matches something in `erc-ignore-reply-list'. + +Takes a message MSG to a channel and returns non-nil if the addressed +user matches any regexp in `erc-ignore-reply-list'." + (let ((target-nick (erc-message-target msg))) + (if (not target-nick) + nil + (erc-with-buffer (tgt proc) + (let ((user (erc-get-server-user target-nick))) + (when user + (erc-list-match erc-ignore-reply-list + (erc-user-spec user)))))))) + +(defun erc-message-target (msg) + "Return the addressed target in MSG. + +The addressed target is the string before the first colon in MSG." + (if (string-match "^\\([^: ]*\\):" msg) + (match-string 1 msg) + nil)) + +(defun erc-user-spec (user) + "Create a nick!user@host spec from a user struct." + (let ((nick (erc-server-user-nickname user)) + (host (erc-server-user-host user)) + (login (erc-server-user-login user))) + (concat (if nick + nick + "") + "!" + (if login + login + "") + "@" + (if host + host + "")))) + +(defun erc-list-match (lst str) + "Return non-nil if any regexp in LST matches STR." + (memq nil (mapcar (lambda (regexp) + (not (string-match regexp str))) + lst))) + +;; other "toggles" + +(defun erc-toggle-ctcp-autoresponse (&optional arg) + "Toggle automatic CTCP replies (like VERSION and PING). + +If ARG is positive, turns CTCP replies on. + +If ARG is non-nil and not positive, turns CTCP replies off." + (interactive "P") + (cond ((and (numberp arg) (> arg 0)) + (setq erc-disable-ctcp-replies t)) + (arg (setq erc-disable-ctcp-replies nil)) + (t (setq erc-disable-ctcp-replies (not erc-disable-ctcp-replies)))) + (message "ERC CTCP replies are %s" (if erc-disable-ctcp-replies "OFF" "ON"))) + +(defun erc-toggle-flood-control (&optional arg) + "Toggle use of flood control on sent messages. + +If ARG is non-nil, use flood control. +If ARG is nil, do not use flood control. + +See `erc-server-flood-margin' for an explanation of the available +flood control parameters." + (interactive "P") + (setq erc-flood-protect arg) + (message "ERC flood control is %s" + (cond (erc-flood-protect "ON") + (t "OFF")))) + +;; Some useful channel and nick commands for fast key bindings + +(defun erc-invite-only-mode (&optional arg) + "Turn on the invite only mode (+i) for the current channel. + +If ARG is non-nil, turn this mode off (-i). + +This command is sent even if excess flood is detected." + (interactive "P") + (erc-set-active-buffer (current-buffer)) + (let ((tgt (erc-default-target)) + (erc-force-send t)) + (cond ((or (not tgt) (not (erc-channel-p tgt))) + (erc-display-message nil 'error (current-buffer) 'no-target)) + (arg (erc-load-irc-script-lines (list (concat "/mode " tgt " -i")) + t)) + (t (erc-load-irc-script-lines (list (concat "/mode " tgt " +i")) + t))))) + +(defun erc-get-channel-mode-from-keypress (key) + "Read a key sequence and call the corresponding channel mode function. +After doing C-c C-o type in a channel mode letter. + +C-g means quit. +RET let's you type more than one mode at a time. +If \"l\" is pressed, `erc-set-channel-limit' gets called. +If \"k\" is pressed, `erc-set-channel-key' gets called. +Anything else will be sent to `erc-toggle-channel-mode'." + (interactive "kChannel mode (RET to set more than one): ") + (when (featurep 'xemacs) + (setq key (char-to-string (event-to-character (aref key 0))))) + (cond ((equal key "\C-g") + (keyboard-quit)) + ((equal key "\C-m") + (erc-insert-mode-command)) + ((equal key "l") + (call-interactively 'erc-set-channel-limit)) + ((equal key "k") + (call-interactively 'erc-set-channel-key)) + (t (erc-toggle-channel-mode key)))) + +(defun erc-toggle-channel-mode (mode &optional channel) + "Toggle channel MODE. + +If CHANNEL is non-nil, toggle MODE for that channel, otherwise use +`erc-default-target'." + (interactive "P") + (erc-set-active-buffer (current-buffer)) + (let ((tgt (or channel (erc-default-target))) + (erc-force-send t)) + (cond ((or (null tgt) (null (erc-channel-p tgt))) + (erc-display-message nil 'error 'active 'no-target)) + ((member mode erc-channel-modes) + (erc-log (format "%s: Toggle mode %s OFF" tgt mode)) + (message "Toggle channel mode %s OFF" mode) + (erc-server-send (format "MODE %s -%s" tgt mode))) + (t (erc-log (format "%s: Toggle channel mode %s ON" tgt mode)) + (message "Toggle channel mode %s ON" mode) + (erc-server-send (format "MODE %s +%s" tgt mode)))))) + +(defun erc-insert-mode-command () + "Insert the line \"/mode <current target> \" at `point'." + (interactive) + (let ((tgt (erc-default-target))) + (if tgt (insert (concat "/mode " tgt " ")) + (erc-display-message nil 'error (current-buffer) 'no-target)))) + +(defun erc-channel-names () + "Run \"/names #channel\" in the current channel." + (interactive) + (erc-set-active-buffer (current-buffer)) + (let ((tgt (erc-default-target))) + (if tgt (erc-load-irc-script-lines (list (concat "/names " tgt))) + (erc-display-message nil 'error (current-buffer) 'no-target)))) + +(defun erc-remove-text-properties-region (start end &optional object) + "Clears the region (START,END) in OBJECT from all colors, etc." + (interactive "r") + (save-excursion + (let ((inhibit-read-only t)) + (set-text-properties start end nil object)))) + +;; script execution and startup + +(defun erc-find-file (file &optional path) + "Search for a FILE in the filesystem. +First the `default-directory' is searched for FILE, then any directories +specified in the list PATH. + +If FILE is found, return the path to it." + (let ((filepath file)) + (if (file-readable-p filepath) filepath + (progn + (while (and path + (progn (setq filepath (expand-file-name file (car path))) + (not (file-readable-p filepath)))) + (setq path (cdr path))) + (if path filepath nil))))) + +(defun erc-select-startup-file () + "Select an ERC startup file. +See also `erc-startup-file-list'." + (let ((l erc-startup-file-list) + (f nil)) + (while (and (not f) l) + (if (file-readable-p (car l)) + (setq f (car l))) + (setq l (cdr l))) + f)) + +(defun erc-find-script-file (file) + "Search for FILE in `default-directory', and any in `erc-script-path'." + (erc-find-file file erc-script-path)) + +(defun erc-load-script (file) + "Load a script from FILE. + +FILE must be the full name, it is not searched in the +`erc-script-path'. If the filename ends with `.el', then load it +as a emacs-lisp program. Otherwise, treat it as a regular IRC +script" + (erc-log (concat "erc-load-script: " file)) + (cond + ((string-match "\\.el$" file) + (load file)) + (t + (erc-load-irc-script file)))) + +(defun erc-process-script-line (line &optional args) + "Process an IRC script LINE. + +Does script-specific substitutions (script arguments, current nick, +server, etc.) in LINE and returns it. + +Substitutions are: %C and %c = current target (channel or nick), +%S %s = current server, %N %n = my current nick, and %x is x verbatim, +where x is any other character; +$* = the entire argument string, $1 = the first argument, $2 = the second, +end so on." + (if (not args) (setq args "")) + (let* ((arg-esc-regexp "\\(\\$\\(\\*\\|[1-9][0-9]*\\)\\)\\([^0-9]\\|$\\)") + (percent-regexp "\\(%.\\)") + (esc-regexp (concat arg-esc-regexp "\\|" percent-regexp)) + (tgt (erc-default-target)) + (server (and (boundp 'erc-session-server) erc-session-server)) + (nick (erc-current-nick)) + (res "") + (tmp nil) + (arg-list nil) + (arg-num 0)) + (if (not tgt) (setq tgt "")) + (if (not server) (setq server "")) + (if (not nick) (setq nick "")) + ;; First, compute the argument list + (setq tmp args) + (while (string-match "^\\s-*\\(\\S-+\\)\\(\\s-+.*$\\|$\\)" tmp) + (setq arg-list (cons (match-string 1 tmp) arg-list)) + (setq tmp (match-string 2 tmp))) + (setq arg-list (nreverse arg-list)) + (setq arg-num (length arg-list)) + ;; now do the substitution + (setq tmp (string-match esc-regexp line)) + (while tmp + ;;(message "beginning of while: tmp=%S" tmp) + (let* ((hd (substring line 0 tmp)) + (esc "") + (subst "") + (tail (substring line tmp))) + (cond ((string-match (concat "^" arg-esc-regexp) tail) + (setq esc (match-string 1 tail)) + (setq tail (substring tail (match-end 1)))) + ((string-match (concat "^" percent-regexp) tail) + (setq esc (match-string 1 tail)) + (setq tail (substring tail (match-end 1))))) + ;;(message "hd=%S, esc=%S, tail=%S, arg-num=%S" hd esc tail arg-num) + (setq res (concat res hd)) + (setq subst + (cond ((string= esc "") "") + ((string-match "^\\$\\*$" esc) args) + ((string-match "^\\$\\([0-9]+\\)$" esc) + (let ((n (string-to-number (match-string 1 esc)))) + (message "n = %S, integerp(n)=%S" n (integerp n)) + (if (<= n arg-num) (nth (1- n) arg-list) ""))) + ((string-match "^%[Cc]$" esc) tgt) + ((string-match "^%[Ss]$" esc) server) + ((string-match "^%[Nn]$" esc) nick) + ((string-match "^%\\(.\\)$" esc) (match-string 1 esc)) + (t (erc-log (format "BUG in erc-process-script-line: bad escape sequence: %S\n" esc)) + (message "BUG IN ERC: esc=%S" esc) + ""))) + (setq line tail) + (setq tmp (string-match esc-regexp line)) + (setq res (concat res subst)) + ;;(message "end of while: line=%S, res=%S, tmp=%S" line res tmp) + )) + (setq res (concat res line)) + res)) + +(defun erc-load-irc-script (file &optional force) + "Load an IRC script from FILE." + (erc-log (concat "erc-load-script: " file)) + (let ((str (with-temp-buffer + (insert-file-contents file) + (buffer-string)))) + (erc-load-irc-script-lines (erc-split-multiline-safe str) force))) + +(defun erc-load-irc-script-lines (lines &optional force noexpand) + "Load IRC script LINES (a list of strings). + +If optional NOEXPAND is non-nil, do not expand script-specific +sequences, process the lines verbatim. Use this for multiline +user input." + (let* ((cb (current-buffer)) + (pnt (point)) + (s "") + (sp (or (erc-command-indicator) (erc-prompt))) + (args (and (boundp 'erc-script-args) erc-script-args))) + (if (and args (string-match "^ " args)) + (setq args (substring args 1))) + ;; prepare the prompt string for echo + (erc-put-text-property 0 (length sp) + 'face 'erc-command-indicator-face sp) + (while lines + (setq s (car lines)) + (erc-log (concat "erc-load-script: CMD: " s)) + (unless (string-match "^\\s-*$" s) + (let ((line (if noexpand s (erc-process-script-line s args)))) + (if (and (erc-process-input-line line force) + erc-script-echo) + (progn + (erc-put-text-property 0 (length line) + 'face 'erc-input-face line) + (erc-display-line (concat sp line) cb))))) + (setq lines (cdr lines))))) + +;; authentication + +(defun erc-login () + "Perform user authentication at the IRC server." + (erc-log (format "login: nick: %s, user: %s %s %s :%s" + (erc-current-nick) + (user-login-name) + (system-name) + erc-session-server + erc-session-user-full-name)) + (if erc-session-password + (erc-server-send (format "PASS %s" 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)) + "0" "*" + erc-session-user-full-name)) + (erc-update-mode-line)) + +;; connection properties' heuristics + +(defun erc-determine-parameters (&optional server port nick name) + "Determine the connection and authentication parameters. +Sets the buffer local variables: + +- erc-session-server +- erc-session-port +- erc-session-full-name +- erc-server-current-nick" + (setq 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-set-current-nick (erc-compute-nick nick))) + +(defun erc-compute-server (&optional server) + "Return an IRC server name. + +Tries a number of increasingly more default methods until a non-nil value is +found: + +- SERVER +- `erc-server' +- The value of the IRCSERVER environment variable +- `erc-default-server'." + (or server + erc-server + (getenv "IRCSERVER") + erc-default-server)) + +(defun erc-compute-nick (&optional nick) + "Return user's NICK. + +Tries a number of increasingly more default methods until a non-nil value is +found: + +- NICK +- `erc-nick' +- The value of the IRCNICK environment variable +- via the function `user-login-name'." + (or nick + (if (consp erc-nick) (car erc-nick) erc-nick) + (getenv "IRCNICK") + (user-login-name))) + + +(defun erc-compute-full-name (&optional full-name) + "Return user's FULL-NAME. + +Tries a number of increasingly more default methods until a non-nil value is +found: + +- FULL-NAME +- `erc-user-full-name' +- The value of the IRCNAME environment variable +- via the function `user-full-name'." + (or full-name + erc-user-full-name + (getenv "IRCNAME") + (if erc-anonymous-login "unknown" nil) + (user-full-name))) + +(defun erc-compute-port (&optional port) + "Return a port for an IRC server. + +Tries a number of increasingly more default methods until a non-nil +value is found: + +- PORT +- \"ircd\"." + (or port erc-port "ircd")) + +;; time routines + +(defun erc-string-to-emacs-time (string) + "Convert the long number represented by STRING into an Emacs format. +Returns a list of the form (HIGH LOW), compatible with Emacs time format." + (let* ((n (string-to-number (concat string ".0")))) + (list (truncate (/ n 65536)) + (truncate (mod n 65536))))) + +(defun erc-emacs-time-to-erc-time (time) + "Convert Emacs TIME to a number of seconds since the epoch." + (when time + (+ (* (nth 0 time) 65536.0) (nth 1 time)))) +; (round (+ (* (nth 0 tm) 65536.0) (nth 1 tm)))) + +(defun erc-current-time () + "Return the `current-time' as a number of seconds since the epoch. + +See also `erc-emacs-time-to-erc-time'." + (erc-emacs-time-to-erc-time (current-time))) + +(defun erc-time-diff (t1 t2) + "Return the time difference in seconds between T1 and T2." + (abs (- t2 t1))) + +(defun erc-time-gt (t1 t2) + "Check whether T1 > T2." + (> t1 t2)) + +(defun erc-sec-to-time (ns) + "Convert NS to a time string HH:MM.SS." + (setq ns (truncate ns)) + (format "%02d:%02d.%02d" + (/ ns 3600) + (/ (% ns 3600) 60) + (% ns 60))) + +(defun erc-seconds-to-string (seconds) + "Convert a number of SECONDS into an English phrase." + (let (days hours minutes format-args output) + (setq days (/ seconds 86400) + seconds (% seconds 86400) + hours (/ seconds 3600) + seconds (% seconds 3600) + minutes (/ seconds 60) + seconds (% seconds 60) + format-args (if (> days 0) + `("%d days, %d hours, %d minutes, %d seconds" + ,days ,hours ,minutes ,seconds) + (if (> hours 0) + `("%d hours, %d minutes, %d seconds" + ,hours ,minutes ,seconds) + (if (> minutes 0) + `("%d minutes, %d seconds" ,minutes ,seconds) + `("%d seconds" ,seconds)))) + output (apply 'format format-args)) + ;; Change all "1 units" to "1 unit". + (while (string-match "\\([^0-9]\\|^\\)1 \\S-+\\(s\\)" output) + (setq output (erc-replace-match-subexpression-in-string + "" output (match-string 2 output) 2 (match-beginning 2)))) + output)) + + +;; info + +(defconst erc-clientinfo-alist + '(("ACTION" . "is used to inform about one's current activity") + ("CLIENTINFO" . "gives help on CTCP commands supported by client") + ("ECHO" . "echoes its arguments back") + ("FINGER" . "shows user's name, location, and idle time") + ("PING" . "measures delay between peers") + ("TIME" . "shows client-side time") + ("USERINFO" . "shows information provided by a user") + ("VERSION" . "shows client type and version")) + "Alist of CTCP CLIENTINFO for ERC commands.") + +(defun erc-client-info (s) + "Return CTCP CLIENTINFO on command S. +If S is NIL or an empty string then return general CLIENTINFO" + (if (or (not s) (string= s "")) + (concat + (apply #'concat + (mapcar (lambda (e) + (concat (car e) " ")) + erc-clientinfo-alist)) + ": use CLIENTINFO <COMMAND> to get more specific information") + (let ((h (assoc (upcase s) erc-clientinfo-alist))) + (if h + (concat s " " (cdr h)) + (concat s ": unknown command"))))) + +;; Hook functions + +(defun erc-directory-writable-p (dir) + "Determine whether DIR is a writable directory. +If it doesn't exist, create it." + (unless (file-attributes dir) (make-directory dir)) + (or (file-accessible-directory-p dir) (error "Cannot access %s" dir))) + +(defun erc-kill-query-buffers (process) + "Kill all buffers of PROCESS." + ;; here, we only want to match the channel buffers, to avoid + ;; "selecting killed buffers" b0rkage. + (erc-with-all-buffers-of-server process + (lambda () + (not (erc-server-buffer-p))) + (kill-buffer (current-buffer)))) + +(defun erc-nick-at-point () + "Give information about the nickname at `point'. + +If called interactively, give a human readable message in the +minibuffer. If called programatically, return the corresponding +entry of `channel-members'." + (interactive) + (require 'thingatpt) + (let* ((word (word-at-point)) + (channel-data (erc-get-channel-user word)) + (cuser (cdr channel-data)) + (user (if channel-data + (car channel-data) + (erc-get-server-user word))) + host login full-name info nick op voice) + (when user + (setq nick (erc-server-user-nickname user) + host (erc-server-user-host user) + login (erc-server-user-login user) + full-name (erc-server-user-full-name user) + info (erc-server-user-info user)) + (if cuser + (setq op (erc-channel-user-op cuser) + voice (erc-channel-user-voice cuser))) + (if (interactive-p) + (message "%s is %s@%s%s%s" + nick login host + (if full-name (format " (%s)" full-name) "") + (if (or op voice) + (format " and is +%s%s on %s" + (if op "o" "") + (if voice "v" "") + (erc-default-target)) + "")) + user)))) + +(defun erc-away-p () + "Return t if the current ERC process is set away." + (save-excursion + (and (erc-server-buffer-live-p) + (set-buffer (process-buffer erc-server-process)) + erc-away))) + +;; Mode line handling + +(defcustom erc-mode-line-format "%s %a" + "A string to be formatted and shown in the mode-line in `erc-mode'. + +The string is formatted using `format-spec' and the result is set as the value +of `mode-line-buffer-identification'. + +The following characters are replaced: +%a: String indicating away status or \"\" if you are not away +%m: The modes of the channel +%n: The current nick name +%o: The topic of the channel +%p: The session port +%t: The name of the target (channel, nickname, or servername:port) +%s: In the server-buffer, this gets filled with the value of + `erc-server-announced-name', in a channel, the value of + (erc-default-target) also get concatenated." + :group 'erc-mode-line-and-header + :type 'string) + +(defcustom erc-header-line-format "[IRC] %n on %t %m %o" + "A string to be formatted and shown in the header-line in `erc-mode'. +Only used in Emacs 21. + +See `erc-mode-line-format' for which characters are can be used." + :group 'erc-mode-line-and-header + :type 'string) + +(defcustom erc-header-line-uses-help-echo-p t + "Show the contents of the header line in the echo area or as a tooltip +when you move point into the header line." + :group 'erc-mode-line-and-header + :type 'boolean) + +(defcustom erc-show-channel-key-p t + "Show the the channel key in the header line." + :group 'erc-paranoia + :type 'boolean) + +(defcustom erc-common-server-suffixes + '(("openprojects.net$" . "OPN") + ("freenode.net$" . "OPN")) + "Alist of common server name suffixes. +This variable is used in mode-line display to save screen +real estate. Set it to nil if you want to avoid changing +displayed hostnames." + :group 'erc-mode-line-and-header + :type 'alist) + +(defcustom erc-mode-line-away-status-format + "(AWAY since %a %b %d %H:%M) " + "When you're away on a server, this is shown in the mode line. +This should be a string with substitution variables recognized by +format-time-message." + :group 'erc-mode-line-and-header + :type 'string) + +(defun erc-shorten-server-name (server-name) + "Shorten SERVER-NAME according to `erc-common-server-suffixes'." + (if (stringp server-name) + (with-temp-buffer + (insert server-name) + (let ((alist erc-common-server-suffixes)) + (while alist + (goto-char (point-min)) + (if (re-search-forward (caar alist) nil t) + (replace-match (cdar alist))) + (setq alist (cdr alist)))) + (buffer-string)))) + +(defun erc-format-target () + "Return the name of the target (channel or nickname or servername:port)." + (let ((target (erc-default-target))) + (or target + (concat (erc-shorten-server-name + (or erc-server-announced-name + erc-session-server)) + ":" (erc-port-to-string erc-session-port))))) + +(defun erc-format-target-and/or-server () + "Return the server name or the current target and server name combined." + (let ((server-name (erc-shorten-server-name + (or erc-server-announced-name + erc-session-server)))) + (cond ((erc-default-target) + (concat (erc-string-no-properties (erc-default-target)) + "@" server-name)) + (server-name server-name) + (t (buffer-name (current-buffer)))))) + +(defun erc-format-away-status () + "Return a formatted `erc-mode-line-away-status-format' +if `erc-away' is non-nil." + (let ((a (when (erc-server-buffer-live-p) + (with-current-buffer (process-buffer erc-server-process) + erc-away)))) + (if a + (format-time-string erc-mode-line-away-status-format a) + ""))) + +(defun erc-format-channel-modes () + "Return the current channel's modes and the estimated lag." + (let ((lag (when (erc-server-buffer-live-p) + (with-current-buffer (process-buffer erc-server-process) + erc-server-lag)))) + (concat (apply 'concat + "(+" erc-channel-modes) + (cond ((and erc-channel-user-limit erc-channel-key) + (if erc-show-channel-key-p + (format "lk %.0f %s" erc-channel-user-limit + erc-channel-key) + (format "kl %.0f" erc-channel-user-limit))) + (erc-channel-user-limit + ;; Emacs has no bignums + (format "l %.0f" erc-channel-user-limit)) + (erc-channel-key + (if erc-show-channel-key-p + (format "k %s" erc-channel-key) + "k")) + (t "")) + (if lag (format ",lag:%.0f" lag) "") + ")"))) + +(defun erc-update-mode-line-buffer (buffer) + "Update the mode line in a single ERC buffer BUFFER." + (with-current-buffer buffer + (let ((spec (format-spec-make + ?a (erc-format-away-status) + ?m (erc-format-channel-modes) + ?n (or (erc-current-nick) "") + ?o (erc-controls-strip erc-channel-topic) + ?p (erc-port-to-string erc-session-port) + ?s (erc-format-target-and/or-server) + ?t (erc-format-target))) + (process-status (cond ((and (erc-server-process-alive) + (not erc-server-connected)) + ":connecting") + ((erc-server-process-alive) + "") + (t + ": CLOSED")))) + (cond ((featurep 'xemacs) + (setq modeline-buffer-identification + (list (format-spec erc-mode-line-format spec))) + (setq modeline-process (list process-status))) + (t + (setq mode-line-buffer-identification + (list (format-spec erc-mode-line-format spec))) + (setq mode-line-process (list process-status)))) + (when (boundp 'header-line-format) + (let ((header (if erc-header-line-format + (format-spec erc-header-line-format spec) + nil))) + (cond ((null header) + (setq header-line-format nil)) + (erc-header-line-uses-help-echo-p + (let ((help-echo (with-temp-buffer + (insert header) + (fill-region (point-min) (point-max)) + (buffer-string)))) + (setq header-line-format + (erc-replace-regexp-in-string + "%" + "%%" + (erc-propertize header 'help-echo help-echo))))) + (t (setq header-line-format header)))))) + (if (featurep 'xemacs) + (redraw-modeline) + (force-mode-line-update)))) + +(defun erc-update-mode-line (&optional buffer) + "Update the mode line in BUFFER. + +If BUFFER is nil, update the mode line in all ERC buffers." + (if (and buffer (bufferp buffer)) + (erc-update-mode-line-buffer buffer) + (dolist (buf (erc-buffer-list)) + (when (buffer-live-p buf) + (erc-update-mode-line-buffer buf))))) + +;; Miscellaneous + +(defun erc-port-to-string (p) + "Convert port P to a string. +P may be an integer or a service name." + (if (integerp p) + (int-to-string p) + p)) + +(defun erc-string-to-port (s) + "Convert string S to either an integer port number or a service name." + (let ((n (string-to-number s))) + (if (= n 0) + s + n))) + +(defun erc-version (&optional here) + "Show the version number of ERC in the minibuffer. +If optional argument HERE is non-nil, insert version number at point." + (interactive "P") + (let ((version-string + (format "ERC %s" erc-version-string))) + (if here + (insert version-string) + (if (interactive-p) + (message "%s" version-string) + version-string)))) + +(defun erc-version-modules (&optional here) + "Show the version numbers of all loaded ERC modules in the minibuffer. +If optional argument HERE is non-nil, insert version number at point." + (interactive "P") + (let ((version-string + (mapconcat 'identity + (let (versions (case-fold-search nil)) + (dolist (var (apropos-internal "^erc-.*version$")) + (when (and (boundp var) + (stringp (symbol-value var))) + (setq versions (cons (format "%S: %s" + var (symbol-value var)) + versions)))) + versions) ", "))) + (if here + (insert version-string) + (if (interactive-p) + (message "%s" version-string) + version-string)))) + +(defun erc-modes (&optional here) + "Show the active ERC modes in the minibuffer. +If optional argument HERE is non-nil, insert version number at point." + (interactive "P") + (let ((string + (mapconcat 'identity + (let (modes (case-fold-search nil)) + (dolist (var (apropos-internal "^erc-.*mode$")) + (when (and (boundp var) + (symbol-value var)) + (setq modes (cons (symbol-name var) + modes)))) + modes) + ", "))) + (if here + (insert string) + (if (interactive-p) + (message "%s" string) + string)))) + +(defun erc-latest-version () + "Retrieve the latest erc.el version from CVS." + (interactive) + (if (ignore-errors (require 'url)) + (progn + (switch-to-buffer (get-buffer-create "*erc.el latest version*")) + (delete-region (point-min) (point-max)) + (kill-all-local-variables) + (url-insert-file-contents (concat + "http://cvs.sourceforge.net/viewcvs.py/" + "*checkout*/erc/erc/erc.el?content-type" + "=text%2Fplain&rev=HEAD")) + (emacs-lisp-mode) + (current-buffer)) + (error "URL needs to be installed"))) + +(defun erc-ediff-latest-version () + "Ediff your installed erc.el with the latest CVS version. +See also `erc-latest-version'." + (interactive) + (let ((current (locate-library "erc.el"))) + (if current + (ediff-buffers (find-file current) + (erc-latest-version)) + (error "You do not appear to have the uncompiled erc.el file")))) + +(defun erc-trim-string (s) + "Trim leading and trailing spaces off S." + (cond + ((not (stringp s)) nil) + ((string-match "^\\s-*$" s) + "") + ((string-match "^\\s-*\\(.*\\S-\\)\\s-*$" s) + (match-string 1 s)) + (t + s))) + +(defun erc-arrange-session-in-multiple-windows () + "Open a window for every non-server buffer related to `erc-session-server'. + +All windows are opened in the current frame." + (interactive) + (unless (boundp 'erc-server-process) + (error "No erc-process found in current buffer")) + (let ((bufs (erc-buffer-list nil erc-server-process))) + (when bufs + (delete-other-windows) + (switch-to-buffer (car bufs)) + (setq bufs (cdr bufs)) + (while bufs + (split-window) + (switch-to-buffer-other-window (car bufs)) + (setq bufs (cdr bufs)) + (balance-windows))))) + +(defun erc-popup-input-buffer () + "Provide a input buffer." + (interactive) + (let ((buffer-name (generate-new-buffer-name "*ERC input*")) + (mode (intern + (completing-read + "Mode: " + (mapcar (lambda (e) + (list (symbol-name e))) + (apropos-internal "-mode$" 'commandp)) + nil t)))) + (pop-to-buffer (make-indirect-buffer (current-buffer) buffer-name)) + (funcall mode) + (narrow-to-region (point) (point)) + (shrink-window-if-larger-than-buffer))) + +;;; Message catalog + +(defun erc-make-message-variable-name (catalog entry) + "Create a variable name corresponding to CATALOG's ENTRY." + (intern (concat "erc-message-" + (symbol-name catalog) "-" (symbol-name entry)))) + +(defun erc-define-catalog-entry (catalog entry format-spec) + "Set CATALOG's ENTRY to FORMAT-SPEC." + (set (erc-make-message-variable-name catalog entry) + format-spec)) + +(defun erc-define-catalog (catalog entries) + "Define a CATALOG according to ENTRIES." + (dolist (entry entries) + (erc-define-catalog-entry catalog (car entry) (cdr entry)))) + +(erc-define-catalog + 'english + '((bad-ping-response . "Unexpected PING response from %n (time %t)") + (bad-syntax . "Error occurred - incorrect usage?\n%c %u\n%d") + (incorrect-args . "Incorrect arguments. Usage:\n%c %u\n%d") + (cannot-find-file . "Cannot find file %f") + (cannot-read-file . "Cannot read file %f") + (connect . "Connecting to %S:%p... ") + (country . "%c") + (country-unknown . "%d: No such domain") + (ctcp-empty . "Illegal empty CTCP query received from %n. Ignoring.") + (ctcp-request . "==> CTCP request from %n (%u@%h): %r") + (ctcp-request-to . "==> CTCP request from %n (%u@%h) to %t: %r") + (ctcp-too-many . "Too many CTCP queries in single message. Ignoring") + (flood-ctcp-off . "FLOOD PROTECTION: Automatic CTCP responses turned off.") + (flood-strict-mode . "FLOOD PROTECTION: Switched to Strict Flood Control mode.") + (disconnected . "Connection failed! Re-establishing connection...") + (disconnected-noreconnect . "Connection failed! Not re-establishing connection.") + (login . "Logging in as \'%n\'...") + (nick-in-use . "%n is in use. Choose new nickname: ") + (nick-too-long . "WARNING: Nick length (%i) exceeds max NICKLEN(%l) defined by server") + (no-default-channel . "No default channel") + (no-invitation . "You've got no invitation") + (no-target . "No target") + (ops . "%i operator%s: %o") + (ops-none . "No operators in this channel.") + (undefined-ctcp . "Undefined CTCP query received. Silently ignored") + (variable-not-bound . "Variable not bound!") + (ACTION . "* %n %a") + (CTCP-CLIENTINFO . "Client info for %n: %m") + (CTCP-ECHO . "Echo %n: %m") + (CTCP-FINGER . "Finger info for %n: %m") + (CTCP-PING . "Ping time to %n is %t") + (CTCP-TIME . "Time by %n is %m") + (CTCP-UNKNOWN . "Unknown CTCP message from %n (%u@%h): %m") + (CTCP-VERSION . "Version for %n is %m") + (ERROR . "==> ERROR from %s: %c\n") + (INVITE . "%n (%u@%h) invites you to channel %c") + (JOIN . "%n (%u@%h) has joined channel %c") + (JOIN-you . "You have joined channel %c") + (KICK . "%n (%u@%h) has kicked %k off channel %c: %r") + (KICK-you . "You have been kicked off channel %c by %n (%u@%h): %r") + (KICK-by-you . "You have kicked %k off channel %c: %r") + (MODE . "%n (%u@%h) has changed mode for %t to %m") + (MODE-nick . "%n has changed mode for %t to %m") + (NICK . "%n (%u@%h) is now known as %N") + (NICK-you . "Your new nickname is %N") + (PART . erc-message-english-PART) + (PING . "PING from server (last: %s sec. ago)") + (PONG . "PONG from %h (%i second%s)") + (QUIT . "%n (%u@%h) has quit: %r") + (TOPIC . "%n (%u@%h) has set the topic for %c: \"%T\"") + (WALLOPS . "Wallops from %n: %m") + (s004 . "%s %v %U %C") + (s221 . "User modes for %n: %m") + (s252 . "%i operator(s) online") + (s253 . "%i unknown connection(s)") + (s254 . "%i channels formed") + (s301 . "%n is AWAY: %r") + (s303 . "Is online: %n") + (s305 . "%m") + (s306 . "%m") + (s311 . "%n is %f (%u@%h)") + (s312 . "%n is/was on server %s (%c)") + (s313 . "%n is an IRC operator") + (s314 . "%n was %f (%u@%h)") + (s317 . "%n has been idle for %i") + (s317-on-since . "%n has been idle for %i, on since %t") + (s319 . "%n is on channel(s): %c") + (s320 . "%n is an identified user") + (s321 . "Channel Users Topic") + (s322 . "%c [%u] %t") + (s324 . "%c modes: %m") + (s329 . "%c was created on %t") + (s330 . "%n %a %i") + (s331 . "No topic is set for %c") + (s332 . "Topic for %c: %T") + (s333 . "%c: topic set by %n, %t") + (s341 . "Inviting %n to channel %c") + (s352 . "%-11c %-10n %-4a %u@%h (%f)") + (s353 . "Users on %c: %u") + (s367 . "Ban on %b on %c set by %s on %t (Use /banlist!)") + (s368 . "Banlist of %c ends.") + (s379 . "%c: Forwarded to %f") + (s391 . "The time at %s is %t") + (s401 . "%n: No such nick/channel") + (s403 . "%c: No such channel") + (s404 . "%c: Cannot send to channel") + (s405 . "%c: You have joined too many channels") + (s406 . "%n: There was no such nickname") + (s412 . "No text to send") + (s421 . "%c: Unknown command") + (s431 . "No nickname given") + (s432 . "%n is an erroneous nickname") + (s442 . "%c: You're not on that channel") + (s445 . "SUMMON has been disabled") + (s446 . "USERS has been disabled") + (s451 . "You have not registered") + (s461 . "%c: not enough parameters") + (s462 . "Unauthorized command (already registered)") + (s463 . "Your host isn't among the privileged") + (s464 . "Password incorrect") + (s465 . "You are banned from this server") + (s474 . "You can't join %c because you're banned (+b)") + (s475 . "You must specify the correct channel key (+k) to join %c") + (s481 . "Permission Denied - You're not an IRC operator") + (s482 . "You need to be a channel operator of %c to do that") + (s483 . "You can't kill a server!") + (s484 . "Your connection is restricted!") + (s485 . "You're not the original channel operator") + (s491 . "No O-lines for your host") + (s501 . "Unknown MODE flag") + (s502 . "You can't change modes for other users"))) + +(defun erc-message-english-PART (&rest args) + "Format a proper PART message. + +This function is an example on what could be done with formatting +functions." + (let ((nick (cadr (memq ?n args))) + (user (cadr (memq ?u args))) + (host (cadr (memq ?h args))) + (channel (cadr (memq ?c args))) + (reason (cadr (memq ?r args)))) + (if (string= nick (erc-current-nick)) + (format "You have left channel %s" channel) + (format "%s (%s@%s) has left channel %s%s" + nick user host channel + (if (not (string= reason "")) + (format ": %s" reason) + ""))))) + + +(defvar erc-current-message-catalog 'english) +(make-variable-buffer-local 'erc-current-message-catalog) + +(defun erc-retrieve-catalog-entry (entry &optional catalog) + "Retrieve ENTRY from CATALOG. + +If CATALOG is nil, `erc-current-message-catalog' is used. + +If ENTRY is nil in CATALOG, it is retrieved from the fallback, +english, catalog." + (unless catalog (setq catalog erc-current-message-catalog)) + (let ((var (erc-make-message-variable-name catalog entry))) + (if (boundp var) + (symbol-value var) + (when (boundp (erc-make-message-variable-name 'english entry)) + (symbol-value (erc-make-message-variable-name 'english entry)))))) + +(defun erc-format-message (msg &rest args) + "Format MSG according to ARGS. + +See also `format-spec'." + (when (eq (logand (length args) 1) 1) ; oddp + (error "Obscure usage of this function appeared")) + (let ((entry (erc-retrieve-catalog-entry msg))) + (when (not entry) + (error "No format spec for message %s" msg)) + (when (functionp entry) + (setq entry (apply entry args))) + (format-spec entry (apply 'format-spec-make args)))) + +;;; Various hook functions + +(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'." + :group 'erc-hooks + :type 'hook) + +(defcustom erc-kill-channel-hook '(erc-kill-channel) + "*Invoked whenever a channel-buffer is killed via `kill-buffer'." + :group 'erc-hooks + :type 'hook) + +(defcustom erc-kill-buffer-hook nil + "*Hook run whenever a non-server or channel buffer is killed. + +See also `kill-buffer'." + :group 'erc-hooks + :type 'hook) + +(defun erc-kill-buffer-function () + "Function to call when an ERC buffer is killed. +This function should be on `kill-buffer-hook'. +When the current buffer is in `erc-mode', this function will run +one of the following hooks: +`erc-kill-server-hook' if the server buffer was killed, +`erc-kill-channel-hook' if a channel buffer was killed, +or `erc-kill-buffer-hook' if any other buffer." + (when (eq major-mode 'erc-mode) + (erc-remove-channel-users) + (cond + ((eq (erc-server-buffer) (current-buffer)) + (run-hooks 'erc-kill-server-hook)) + ((erc-channel-p (erc-default-target)) + (run-hooks 'erc-kill-channel-hook)) + (t + (run-hooks 'erc-kill-buffer-hook))))) + +(defun erc-kill-server () + "Sends a QUIT command to the server when the server buffer is killed. +This function should be on `erc-kill-server-hook'." + (when (erc-server-process-alive) + (setq erc-server-quitting t) + (erc-server-send (format "QUIT :%s" (funcall erc-quit-reason nil))))) + +(defun erc-kill-channel () + "Sends a PART command to the server when the channel buffer is killed. +This function should be on `erc-kill-channel-hook'." + (when (erc-server-process-alive) + (let ((tgt (erc-default-target))) + (erc-server-send (format "PART %s :%s" tgt + (funcall erc-part-reason nil)) + nil tgt)))) + +(provide 'erc) + +;;; 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) + +;;; erc.el ends here +;; +;; Local Variables: +;; outline-regexp: ";;+" +;; indent-tabs-mode: t +;; tab-width: 8 +;; End: + +;; arch-tag: d19587f6-627e-48c1-8d86-58595fa3eca3