about summary refs log tree commit diff
path: root/configs/shared/emacs/.emacs.d/elpa/circe-20180525.531/irc.el
diff options
context:
space:
mode:
Diffstat (limited to 'configs/shared/emacs/.emacs.d/elpa/circe-20180525.531/irc.el')
-rw-r--r--configs/shared/emacs/.emacs.d/elpa/circe-20180525.531/irc.el1413
1 files changed, 1413 insertions, 0 deletions
diff --git a/configs/shared/emacs/.emacs.d/elpa/circe-20180525.531/irc.el b/configs/shared/emacs/.emacs.d/elpa/circe-20180525.531/irc.el
new file mode 100644
index 0000000000..04b260c75a
--- /dev/null
+++ b/configs/shared/emacs/.emacs.d/elpa/circe-20180525.531/irc.el
@@ -0,0 +1,1413 @@
+;;; irc.el --- Library to handle IRC connections -*- lexical-binding: t -*-
+
+;; Copyright (C) 2015  Jorgen Schaefer <contact@jorgenschaefer.de>
+
+;; Author: Jorgen Schaefer <contact@jorgenschaefer.de>
+;; URL: https://github.com/jorgenschaefer/circe
+
+;; This program is free software; you can redistribute it and/or
+;; modify it under the terms of the GNU General Public License
+;; as published by the Free Software Foundation; either version 3
+;; of the License, or (at your option) any later version.
+
+;; This program is distributed in the hope that it will be useful,
+;; but WITHOUT ANY WARRANTY; without even the implied warranty of
+;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+;; GNU General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with this program. If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; The main entry function is `irc-connect'. This creates a new
+;; connection to an IRC server, and also takes an event handler table
+;; which is used to run various event handlers. Handlers receive a
+;; connection object which can be used for other API calls.
+
+;; IRC connection objects also accept connection options. These can be
+;; queried using `irc-connection-get', and are set by `irc-connect' or
+;; later using `irc-connection-put'.
+
+;; Event handler tables are simple maps of names to functions. See
+;; `irc-handler-table', `irc-handler-add' and `irc-handler-run' for
+;; the API.
+
+;; To send commands to the server, use `irc-send-raw' or
+;; `irc-send-command'.
+
+;; The rest of the library are handler packs that add support for
+;; various IRC features.
+
+;;; Code:
+
+(require 'cl-lib)
+(require 'make-tls-process)
+
+(defvar irc-debug-log nil
+  "Emit protocol debug info if this is non-nil.")
+
+;;;;;;;;;;;;;;;;;;;;;;;
+;;; Connection function
+
+(defun irc-connect (&rest keywords)
+  "Connect to an IRC server.
+
+Supported keyword arguments:
+
+:name NAME -- The name for the process
+:host HOST -- The host to connect to
+:service SERVICE -- The service or port to connect to
+:tls BOOL -- Whether to use TLS
+:family IP-FAMILY -- Force using of ipv4 or ipv6
+:handler-table HANDLER -- The event handler table to send events to.
+
+The following events are supported:
+
+conn.connected conn -- The connection was established
+conn.failed conn -- The connection could not be established
+conn.disconnected conn -- A previously established connection was lost
+
+NNN conn sender args... -- A numeric reply from IRC was received
+COMMAND conn sender args... -- An IRC command message was received"
+  (let ((proc (funcall (if (plist-get keywords :tls)
+                           #'make-tls-process
+                         #'make-network-process)
+                       :name (or (plist-get keywords :name)
+                                 (plist-get keywords :host))
+                       :host (or (plist-get keywords :host)
+                                 (error "Must specify a :host to connect to"))
+                       :service (or (plist-get keywords :service)
+                                    (error "Must specify a :service to connect to"))
+                       :family (plist-get keywords :family)
+                       :coding 'no-conversion
+                       :nowait (featurep 'make-network-process '(:nowait t))
+                       :noquery t
+                       :filter #'irc--filter
+                       :sentinel #'irc--sentinel
+                       :plist keywords
+                       :keepalive t)))
+    ;; When we used `make-network-process' without :nowait, the
+    ;; sentinel is not called with the open event, so we do this
+    ;; manually.
+    (when (eq (process-status proc) 'open)
+      (irc--sentinel proc "open manually"))
+    proc))
+
+(defun irc-connection-get (conn propname)
+  "Return the value of CONN's PROPNAME property."
+  (process-get conn propname))
+
+(defun irc-connection-put (conn propname value)
+  "Change CONN's PROPNAME property to VALUE."
+  (process-put conn propname value))
+
+(defun irc--sentinel (proc event)
+  (cond
+   ((string-match "\\`failed" event)
+    (irc-event-emit proc "conn.failed"))
+   ((string-match "\\`open" event)
+    (irc-event-emit proc "conn.connected"))
+   ((string-match "\\`\\(connection broken\\|finished\\|exited abnormally\\)"
+                  event)
+    (irc-event-emit proc "conn.disconnected"))
+   ((string-match "\\`\\(deleted\\|killed\\)" event)
+    nil)
+   (t
+    (error "Unknown event in IRC sentinel: %S" event))))
+
+(defvar irc--filter-running-p nil
+  "Non-nil when we're currently processing a message.
+
+Yep, this is a mutex. Why would one need a mutex in Emacs, a
+single-threaded application, you ask? Easy!
+
+When, during the execution of a process filter, any piece of code
+waits for process output - e.g. because they started a some
+external program - Emacs will process any input from external
+processes. Including the one for the filter that is currently
+running.
+
+If that process does emit output, the filter is run again, while
+it is already running. If the filter is not careful, this can
+cause data to arrive out of order, or get lost.")
+
+(defun irc--filter (proc data)
+  "Handle data from the process."
+  (irc-connection-put proc :conn-data
+                      (concat (or (irc-connection-get proc :conn-data)
+                                  "")
+                              data))
+  (when (not irc--filter-running-p)
+    (let ((irc--filter-running-p t)
+          (data (irc-connection-get proc :conn-data)))
+      (while (string-match "\r?\n" data)
+        (let ((line (substring data 0 (match-beginning 0))))
+          (setq data (substring data (match-end 0)))
+          (irc-connection-put proc :conn-data data)
+          (irc--handle-line proc line)
+          (setq data (irc-connection-get proc :conn-data)))))))
+
+(defun irc--handle-line (proc line)
+  "Handle a single line from the IRC server.
+
+The command is simply passed to the event handler of the IRC
+connection."
+  (irc-debug-out proc "S: %s" line)
+  (let* ((parsed (irc--parse line))
+         (sender (car parsed))
+         (command (cadr parsed))
+         (args (cddr parsed)))
+    (apply #'irc-event-emit proc command sender args)))
+
+(defun irc--parse (line)
+  "Parse a line from IRC.
+
+Returns a list: (sender command args...)
+
+A line from IRC is a space-separated list of arguments. If the
+first word starts with a colon, that's the sender. The first or
+second word is the command. All further words are arguments. The
+first word to start with a colon ends the argument list.
+
+Examples:
+
+COMMAND
+COMMAND arg
+COMMAND arg1 arg2
+COMMAND arg1 arg2 :arg3 still arg3
+:sender COMMAND arg1 arg2 :arg3 still arg3"
+  (with-temp-buffer
+    (insert line)
+    (goto-char (point-min))
+    (let ((sender nil)
+          (args nil))
+      ;; Optional sender.
+      (when (looking-at ":\\([^ ]+\\) ")
+        (setq sender (decode-coding-string
+                      (match-string 1)
+                      'undecided))
+        (goto-char (match-end 0)))
+
+      ;; COMMAND.
+      (unless (looking-at "\\([^ ]+\\)")
+        (error "Invalid message: %s" line))
+      (push (decode-coding-string (match-string 1) 'undecided)
+            args)
+      (goto-char (match-end 0))
+
+      ;; Arguments.
+      (while (re-search-forward " :\\(.*\\)\\| \\([^ ]*\\)" nil t)
+        (push (decode-coding-string
+               (or (match-string 1)
+                   (match-string 2))
+               'undecided)
+              args))
+
+      (cons sender (nreverse args)))))
+
+(defun irc-userstring-nick (userstring)
+  "Return the nick in a given USERSTRING.
+
+USERSTRING is a typical nick!user@host prefix as used by IRC."
+  (if (string-match "\\`\\([^!]+\\)!\\([^@]+\\)@\\(.*\\)\\'" userstring)
+      (match-string 1 userstring)
+    userstring))
+
+(defun irc-userstring-userhost (userstring)
+  "Return the nick in a given USERSTRING.
+
+USERSTRING is a typical nick!user@host prefix as used by IRC."
+  (if (string-match "\\`\\([^!]+\\)!\\([^@]+@.*\\)\\'" userstring)
+      (match-string 2 userstring)
+    nil))
+
+(defun irc-event-emit (conn event &rest args)
+  "Run the event handlers for EVENT in CONN with ARGS."
+  (irc-debug-out conn
+                 "E: %S %s"
+                 event
+                 (mapconcat (lambda (elt) (format "%S" elt))
+                            args
+                            " "))
+  (let ((handler-table (irc-connection-get conn :handler-table)))
+    (when handler-table
+      (apply #'irc-handler-run handler-table event conn event args)
+      (apply #'irc-handler-run handler-table nil conn event args))))
+
+;;;;;;;;;;;;;;;;;;;;;;;
+;;; Event handler table
+
+(defun irc-handler-table ()
+  "Return a new event handler table."
+  (make-hash-table :test 'equal))
+
+(defun irc-handler-add (table event handler)
+  "Add HANDLER for EVENT to the event handler table TABLE."
+  (puthash event
+           (append (gethash event table)
+                   (list handler))
+           table))
+
+(defun irc-handler-remove (table event handler)
+  "Remove HANDLER for EVENT to the event handler table TABLE."
+  (puthash event
+           (delete handler
+                   (gethash event table))
+           table))
+
+(defun irc-handler-run (table event &rest args)
+  "Run the handlers for EVENT in TABLE, passing ARGS to each."
+  (dolist (handler (gethash event table))
+    (if debug-on-error
+        (apply handler args)
+      (condition-case err
+          (apply handler args)
+        (error
+         (message "Error running event %S handler %S: %S (args were %S)"
+                  event handler err args))))))
+
+;;;;;;;;;;;
+;;; Sending
+
+(defun irc-send-raw (conn line &optional flood-handling)
+  "Send a line LINE to the IRC connection CONN.
+
+LINE should not include the trailing newline.
+
+FLOOD-HANDLING defines how to handle the situation when we are
+sending too  much data. It can have three values:
+
+nil -- Add the message to a queue and send it later
+:nowait -- Send the message immediately, circumventing flood protection
+:drop -- Send the message only if we are not flooding, and drop it if
+   we have queued up messages.
+
+The flood protection algorithm works like the one detailed in RFC
+2813, section 5.8 \"Flood control of clients\".
+
+  * If `flood-last-message' is less than the current
+    time, set it equal.
+  * While `flood-last-message' is less than `flood-margin'
+    seconds ahead of the current time, send a message, and
+    increase `flood-last-message' by `flood-penalty'."
+  (cond
+   ((null flood-handling)
+    (irc-connection-put conn
+                        :flood-queue
+                        (append (irc-connection-get conn :flood-queue)
+                                (list line)))
+    (irc-send--queue conn))
+   ((eq flood-handling :nowait)
+    (irc-send--internal conn line))
+   ((eq flood-handling :drop)
+    (let ((queue (irc-connection-get conn :flood-queue)))
+      (when (not queue)
+        (irc-connection-put conn :flood-queue (list line))
+        (irc-send--queue conn))))))
+
+(defun irc-send--queue (conn)
+  "Send messages from the flood queue in CONN.
+
+See `irc-send-raw' for the algorithm."
+  (let ((queue (irc-connection-get conn :flood-queue))
+        (last-message (or (irc-connection-get conn :flood-last-message)
+                          0))
+        (margin (or (irc-connection-get conn :flood-margin)
+                    10))
+        (penalty (or (irc-connection-get conn :flood-penalty)
+                     3))
+        (now (float-time)))
+    (when (< last-message now)
+      (setq last-message now))
+    (while (and queue
+                (< last-message (+ now margin)))
+      (irc-send--internal conn (car queue))
+      (setq queue (cdr queue)
+            last-message (+ last-message penalty)))
+    (irc-connection-put conn :flood-queue queue)
+    (irc-connection-put conn :flood-last-message last-message)
+    (let ((timer (irc-connection-get conn :flood-timer)))
+      (when timer
+        (cancel-timer timer)
+        (irc-connection-put conn :flood-timer nil))
+      (when queue
+        (irc-connection-put conn
+                            :flood-timer
+                            (run-at-time 1 nil #'irc-send--queue conn))))))
+
+(defun irc-send--internal (conn line)
+  "Send LINE to CONN."
+  (irc-debug-out conn "C: %s" line)
+  (process-send-string conn
+                       (concat (encode-coding-string line 'utf-8)
+                               "\r\n")))
+
+(defun irc-send-command (conn command &rest args)
+  "Send COMMAND with ARGS to IRC connection CONN."
+  (irc-send-raw conn (apply #'irc--format-command command args)))
+
+(defun irc--format-command (command &rest args)
+  "Format COMMAND and ARGS for IRC.
+
+The last value in ARGS will be escaped with a leading colon if it
+contains a space. All other arguments are checked to make sure
+they do not contain a space."
+  (dolist (arg (cons command args))
+    (when (not (stringp arg))
+      (error "Argument must be a string")))
+  (let* ((prefix (cons command (butlast args)))
+         (last (last args)))
+    (dolist (arg prefix)
+      (when (string-match " " arg)
+        (error "IRC protocol error: Argument %S must not contain space"
+               arg)))
+    (when (and last (or (string-match " " (car last))
+                        (string-match "^:" (car last))
+                        (equal "" (car last))))
+      (setcar last (concat ":" (car last))))
+    (mapconcat #'identity
+               (append prefix last)
+               " ")))
+
+(defun irc-send-AUTHENTICATE (conn arg)
+  "Send an AUTHENTICATE message with ARG.
+
+See https://github.com/atheme/charybdis/blob/master/doc/sasl.txt
+for details."
+  (irc-send-command conn "AUTHENTICATE" arg))
+
+(defun irc-send-AWAY (conn &optional reason)
+  "Mark yourself as AWAY with reason REASON, or back if reason is nil."
+  (if reason
+      (irc-send-command conn "AWAY" reason)
+    (irc-send-command conn "AWAY")))
+
+(defun irc-send-CAP (conn &rest args)
+  "Send a CAP message.
+
+See https://tools.ietf.org/html/draft-mitchell-irc-capabilities-01
+for details."
+  (apply #'irc-send-command conn "CAP" args))
+
+(defun irc-send-INVITE (conn nick channel)
+  "Invite NICK to CHANNEL."
+  (irc-send-command conn "INVITE" nick channel))
+
+(defun irc-send-JOIN (conn channel &optional key)
+  "Join CHANNEL.
+
+If KEY is given, use it to join the password-protected channel."
+  (if key
+      (irc-send-command conn "JOIN" channel key)
+    (irc-send-command conn "JOIN" channel)))
+
+(defun irc-send-NAMES (conn &optional channel)
+  "Retrieve user names from the server, optionally limited to CHANNEL."
+  (if channel
+      (irc-send-command conn "NAMES" channel)
+    (irc-send-command conn "NAMES")))
+
+(defun irc-send-NICK (conn nick)
+  "Change your own nick to NICK."
+  (irc-send-command conn "NICK" nick))
+
+(defun irc-send-NOTICE (conn msgtarget text-to-be-sent)
+  "Send a private notice containing TEXT-TO-BE-SENT to MSGTARGET.
+
+MSGTARGET can be either a nick or a channel."
+  (irc-send-command conn "NOTICE" msgtarget text-to-be-sent))
+
+(defun irc-send-PART (conn channel reason)
+  "Leave CHANNEL with reason REASON."
+  (irc-send-command conn "PART" channel reason))
+
+(defun irc-send-PASS (conn password)
+  "Authenticate to the server using PASSWORD."
+  (irc-send-command conn "PASS" password))
+
+(defun irc-send-PONG (conn server)
+  "Respond to a PING message."
+  (irc-send-raw conn
+                (irc--format-command "PONG" server)
+                :nowait))
+
+(defun irc-send-PRIVMSG (conn msgtarget text-to-be-sent)
+  "Send a private message containing TEXT-TO-BE-SENT to MSGTARGET.
+
+MSGTARGET can be either a nick or a channel."
+  (irc-send-command conn "PRIVMSG" msgtarget text-to-be-sent))
+
+(defun irc-send-QUIT (conn reason)
+  "Leave IRC with reason REASON."
+  (irc-send-command conn "QUIT" reason))
+
+(defun irc-send-TOPIC (conn channel &optional new-topic)
+  "Retrieve or set the topic of CHANNEL
+
+If NEW-TOPIC is given, set this as the new topic. If it is
+omitted, retrieve the current topic."
+  (if new-topic
+      (irc-send-command conn "TOPIC" channel new-topic)
+    (irc-send-command conn "TOPIC" channel)))
+
+(defun irc-send-USER (conn user mode realname)
+  "Send a USER message for registration.
+
+MODE should be an integer as per RFC 2812"
+  (irc-send-command conn "USER" user (format "%s" mode) "*" realname))
+
+(defun irc-send-WHOIS (conn target &optional server-or-name)
+  "Retrieve current whois information on TARGET."
+  (if server-or-name
+      (irc-send-command conn "WHOIS" target server-or-name)
+    (irc-send-command conn "WHOIS" target)))
+
+(defun irc-send-WHOWAS (conn target)
+  "Retrieve past whois information on TARGET."
+  (irc-send-command conn "WHOWAS" target))
+
+(defun irc-send-STATS (conn query &optional server)
+  "Return statistics on current server, or SERVER if it is specified."
+  (if server
+      (irc-send-command conn "STATS" query server)
+    (irc-send-command conn "STATS" query)))
+
+;;;;;;;;;;;;;;;
+;;; Debug stuff
+
+(defun irc-debug-out (conn fmt &rest args)
+  (when irc-debug-log
+    (let ((name (format "*IRC Protocol %s:%s*"
+                        (irc-connection-get conn :host)
+                        (irc-connection-get conn :service))))
+      (with-current-buffer (get-buffer-create name)
+        (save-excursion
+          (goto-char (point-max))
+          (insert (apply #'format fmt args) "\n"))))))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;
+;;; Handler: Registration
+
+(defun irc-handle-registration (table)
+  "Add command handlers to TABLE to handle registration.
+
+This will send the usual startup messages after we are connected.
+
+Events emitted:
+
+\"irc.registered\" current-nick -- We have successfully
+  registered with the IRC server. Most commands can be used now.
+  In particular, joining channels is only possible now.
+
+\"sasl.login\" nick!user@host account -- SASL log in was
+  successful.
+
+Connection options used:
+
+:nick -- The nick to use to register with the server
+:user -- The user name to use
+:mode -- The initial mode to use; an integer. See RFC 2812 for
+   the meaning.
+:realname -- The realname to use for the registration
+:pass -- The server password to send
+:cap-req -- CAP protocol capabilities to request, if available
+:sasl-username -- The SASL username to send, if sasl is available
+:sasl-password -- The SASL password to send, if sasl is available
+
+Connection options set:
+
+:connection-state -- One of nil, connected, registered, disconnected
+  See `irc-connection-state' for an interface to this.
+:cap-supported-p -- Non-nil if the server supports the CAP protocol
+:cap-ack -- The list of active capabilities negotiated with the server"
+  (irc-handler-add table "conn.connected"
+                   #'irc-handle-registration--connected)
+  (irc-handler-add table "conn.disconnected"
+                   #'irc-handle-registration--disconnected)
+  (irc-handler-add table "001" ;; RPL_WELCOME
+                   #'irc-handle-registration--rpl-welcome)
+  (irc-handler-add table "CAP"
+                   #'irc-handle-registration--cap)
+  (irc-handler-add table "AUTHENTICATE"
+                   #'irc-handle-registration--authenticate)
+  (irc-handler-add table "900" ;; RPL_LOGGEDIN
+                   #'irc-handle-registration--logged-in))
+
+(defun irc-handle-registration--connected (conn _event)
+  (irc-connection-put conn :connection-state 'connected)
+  (when (irc-connection-get conn :cap-req)
+    (irc-send-CAP conn "LS"))
+  (let ((password (irc-connection-get conn :pass)))
+    (when password
+      (irc-send-PASS conn password)))
+  (irc-send-NICK conn (irc-connection-get conn :nick))
+  (irc-send-USER conn
+                 (irc-connection-get conn :user)
+                 (irc-connection-get conn :mode)
+                 (irc-connection-get conn :realname)))
+
+(defun irc-handle-registration--disconnected (conn _event)
+  (irc-connection-put conn :connection-state 'disconnected))
+
+(defun irc-handle-registration--rpl-welcome (conn _event _sender target
+                                                  &rest ignored)
+  (irc-connection-put conn :connection-state 'registered)
+  (irc-event-emit conn "irc.registered" target))
+
+(defun irc-handle-registration--cap (conn _event _sender _target
+                                          subcommand arg)
+  (cond
+   ((equal subcommand "LS")
+    (let ((supported (split-string arg))
+          (wanted nil))
+      (dolist (cap (irc-connection-get conn :cap-req))
+        (when (member cap supported)
+          (setq wanted (append wanted (list cap)))))
+      (if wanted
+          (irc-send-CAP conn "REQ" (mapconcat #'identity wanted " "))
+        (irc-send-CAP conn "END"))))
+   ((equal subcommand "ACK")
+    (let ((acked (split-string arg)))
+      (irc-connection-put conn :cap-ack acked)
+      (if (and (member "sasl" acked)
+               (irc-connection-get conn :sasl-username)
+               (irc-connection-get conn :sasl-password))
+          (irc-send-AUTHENTICATE conn "PLAIN")
+        (irc-send-CAP conn "END"))))
+   (t
+    (message "Unknown CAP response from server: %s %s" subcommand arg))))
+
+(defun irc-handle-registration--authenticate (conn _event _sender arg)
+  (if (equal arg "+")
+      (let ((username (irc-connection-get conn :sasl-username))
+            (password (irc-connection-get conn :sasl-password)))
+        (irc-send-AUTHENTICATE conn (base64-encode-string
+                                     (format "%s\x00%s\x00%s"
+                                             username username password)))
+        (irc-send-CAP conn "END"))
+    (message "Unknown AUTHENTICATE response from server: %s" arg)))
+
+(defun irc-handle-registration--logged-in (conn _event _sender _target
+                                                userhost account _message)
+  (irc-event-emit conn "sasl.login" userhost account))
+
+(defun irc-connection-state (conn)
+  "connecting connected registered disconnected"
+  (let ((state (irc-connection-get conn :connection-state)))
+    (if (null state)
+        'connecting
+      state)))
+
+;;;;;;;;;;;;;;;;;;;;;;
+;;; Handler: Ping-Pong
+
+(defun irc-handle-ping-pong (table)
+  "Add command handlers to respond to PING requests."
+  (irc-handler-add table "PING" #'irc-handle-ping-pong--ping))
+
+(defun irc-handle-ping-pong--ping (conn _event _sender argument)
+  (irc-send-PONG conn argument))
+
+;;;;;;;;;;;;;;;;;;;;;
+;;; Handler: ISUPPORT
+
+(defun irc-handle-isupport (table)
+  "Add command handlers to track 005 RPL_ISUPPORT capabilities."
+  (irc-handler-add table "005" #'irc-handle-isupport--005))
+
+(defun irc-handle-isupport--005 (conn _event _sender _target &rest args)
+  (irc-connection-put
+   conn :isupport
+   (append (irc-connection-get conn :isupport)
+           (irc-handle-isupport--capabilities-to-alist args))))
+
+(defun irc-handle-isupport--capabilities-to-alist (capabilities)
+  (mapcar (lambda (cap)
+            (if (string-match "\\`\\([^=]+\\)=\\(.*\\)\\'" cap)
+                (cons (match-string 1 cap)
+                      (match-string 2 cap))
+              (cons cap t)))
+          capabilities))
+
+(defun irc-isupport (conn capability)
+  "Return the value of CAPABILITY of CONN.
+
+These capabilities are set when the server sends a 005
+RPL_ISUPPORT message. The return value is either the value of the
+capability, or t if it is a boolean capability that is present.
+If the capability is not present, the return value is nil."
+  (cdr (assoc capability
+              (irc-connection-get conn :isupport))))
+
+(defun irc-string-equal-p (conn s1 s2)
+  "Compare S1 to S2 case-insensitively.
+
+What case means is defined by the server of CONN."
+  (equal (irc-isupport--case-fold conn s1)
+         (irc-isupport--case-fold conn s2)))
+
+(defvar irc-isupport--ascii-table
+  (let ((table (make-string 128 0))
+        (char 0))
+    (while (<= char 127)
+      (if (and (<= ?A char)
+               (<= char ?Z))
+          (aset table char (+ char (- ?a ?A)))
+        (aset table char char))
+      (setq char (1+ char)))
+    table)
+  "A case mapping table for the ascii CASEMAPPING.")
+
+(defvar irc-isupport--rfc1459-table
+  (let ((table (concat irc-isupport--ascii-table)))  ; copy string
+    (aset table ?\[ ?\{)
+    (aset table ?\] ?\})
+    (aset table ?\\ ?\|)
+    (aset table ?^ ?\~)
+    table)
+  "A case mapping table for the rfc1459 CASEMAPPING.")
+
+(defvar irc-isupport--rfc1459-strict-table
+  (let ((table (concat irc-isupport--ascii-table)))  ; copy string
+    (aset table ?\[ ?\{)
+    (aset table ?\] ?\})
+    (aset table ?\\ ?\|)
+    table)
+  "A case mapping table for the rfc1459-strict CASEMAPPING.")
+
+(defun irc-isupport--case-fold (conn s)
+  "Translate S to be a lower-case.
+
+This uses the case mapping defined by the IRC server for CONN."
+  (with-temp-buffer
+    (insert s)
+    (let ((mapping (or (irc-isupport conn "CASEMAPPING")
+                       "rfc1459")))
+      (cond
+       ((equal mapping "rfc1459")
+        (translate-region (point-min)
+                          (point-max)
+                          irc-isupport--rfc1459-table))
+       ((equal mapping "ascii")
+        (translate-region (point-min)
+                          (point-max)
+                          irc-isupport--ascii-table))
+       ((equal mapping "rfc1459-strict")
+        (translate-region (point-min)
+                          (point-max)
+                          irc-isupport--rfc1459-strict-table))))
+    (buffer-string)))
+
+(defun irc-channel-name-p (conn string)
+  "True iff STRING is a valid channel name for CONN.
+
+This depends on the CHANTYPES setting set by the server of CONN."
+  (let ((chantypes (string-to-list
+                    (or (irc-isupport conn "CHANTYPES")
+                        "#"))))
+    (if (and (> (length string) 0)
+             (member (aref string 0) chantypes))
+        t
+      nil)))
+
+(defun irc-nick-without-prefix (conn nick)
+  "Return NICK without any mode prefixes.
+
+For example, a user with op status might be shown as @Nick. This
+function would return Nick without the prefix. This uses the 005
+RPL_ISUPPORT setting of PREFIX set by the IRC server for CONN."
+  (let ((prefixes (irc-connection-get conn :nick-prefixes)))
+    (when (not prefixes)
+      (let ((prefix-string (or (irc-isupport conn "PREFIX")
+                               "(qaohv)~&@%+")))
+        (setq prefixes (string-to-list
+                        (if (string-match "(.*)\\(.*\\)" prefix-string)
+                            (match-string 1 prefix-string)
+                          "~&@%+")))
+        (irc-connection-put conn :nick-prefixes prefixes)))
+    (while (and (> (length nick) 0)
+                (member (aref nick 0) prefixes))
+      (setq nick (substring nick 1)))
+    nick))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;; Handler: Initial nick acquisition
+
+(defun irc-handle-initial-nick-acquisition (table)
+  "Track the current nick of the user.
+
+Connection options used:
+
+:nick-alternatives -- A list of nicks to try if the first attempt
+  does not succeed."
+  (irc-handler-add table "432" ;; ERR_ERRONEUSNICKNAME
+                   #'irc-handle-initial-nick-acquisition--get-initial-nick)
+  (irc-handler-add table "433" ;; ERR_NICKNAMEINUSE
+                   #'irc-handle-initial-nick-acquisition--get-initial-nick)
+  (irc-handler-add table "437" ;; ERR_UNAVAILRESOURCE
+                   #'irc-handle-initial-nick-acquisition--get-initial-nick))
+
+(defun irc-handle-initial-nick-acquisition--get-initial-nick
+    (conn _event _sender current-nick _attempted-nick _reason)
+  (when (equal current-nick "*")
+    (let ((alternatives (irc-connection-get conn :nick-alternatives)))
+      (if (not alternatives)
+          (irc-send-NICK conn (irc-generate-nick))
+        (irc-connection-put conn :nick-alternatives (cdr alternatives))
+        (irc-send-NICK conn (car alternatives))))))
+
+(defun irc-generate-nick ()
+  "Return a random, valid IRC nick name.
+
+Valid nick names are at least (RFC 1459):
+
+<nick>       ::= <letter> { <letter> | <number> | <special> }
+<special>    ::= '-' | '[' | ']' | '\' | '`' | '^' | '{' | '}'"
+  (let ((chars "abcdefghijklmnopqrstuvwxyz"))
+    (mapconcat (lambda (_)
+                 (make-string 1 (aref chars (random (length chars)))))
+               (make-string 9 0)
+               "")))
+
+;;;;;;;;;;;;;;;;;
+;;; Handler: CTCP
+
+(defun irc-handle-ctcp (table)
+  "Add command handlers to TABLE to handle the CTCP protocol.
+
+Connection options used:
+
+:ctcp-version -- The response to a CTCP VERSION request.
+:ctcp-clientinfo -- The response to a CTCP CLIENTINFO request.
+:ctcp-source -- The response to a CTCP SOURCE request.
+
+Events emitted:
+
+\"irc.message\" sender target body -- A non-CTCP PRIVMSG
+\"irc.notice\" sender target body -- A non-CTCP NOTICE
+\"irc.ctcp\" sender target verb argument -- A CTCP request. ARGUMENT
+  can be nil if there was no argument, or the empty string if the
+  argument was empty.
+\"irc.ctcpreply\" sender target verb argument -- A CTCP reply.
+  ARGUMENT is similar to above.
+\"irc.ctcp.VERB\" sender target argument -- A CTCP request of
+  this specific type.
+\"irc.ctcpreply.VERB\" sender target argument -- A CTCP reply of
+  this specific type."
+  (irc-handler-add table "PRIVMSG"
+                   #'irc-handle-ctcp--privmsg)
+  (irc-handler-add table "irc.ctcp"
+                   #'irc-handle-ctcp--ctcp)
+  (irc-handler-add table "NOTICE"
+                   #'irc-handle-ctcp--notice)
+  (irc-handler-add table "irc.ctcpreply"
+                   #'irc-handle-ctcp--ctcpreply)
+  (irc-handler-add table "irc.ctcp.VERSION"
+                   #'irc-handle-ctcp--ctcp-version)
+  (irc-handler-add table "irc.ctcp.CLIENTINFO"
+                   #'irc-handle-ctcp--ctcp-clientinfo)
+  (irc-handler-add table "irc.ctcp.SOURCE"
+                   #'irc-handle-ctcp--ctcp-source)
+  (irc-handler-add table "irc.ctcp.PING"
+                   #'irc-handle-ctcp--ctcp-ping)
+  (irc-handler-add table "irc.ctcp.TIME"
+                   #'irc-handle-ctcp--ctcp-time)
+  )
+
+(defun irc-handle-ctcp--privmsg (conn _event sender target body)
+  (if (string-match "\\`\x01\\([^ ]+\\)\\(?: \\(.*\\)\\)?\x01\\'"
+                    body)
+      (irc-event-emit conn "irc.ctcp" sender target
+                      (match-string 1 body)
+                      (match-string 2 body))
+    (irc-event-emit conn "irc.message" sender target body)))
+
+(defun irc-handle-ctcp--ctcp (conn _event sender target verb argument)
+  (irc-event-emit conn
+                  (format "irc.ctcp.%s" (upcase verb))
+                  sender
+                  target
+                  argument))
+
+(defun irc-handle-ctcp--notice (conn _event sender target body)
+  (if (string-match "\\`\x01\\([^ ]+\\)\\(?: \\(.*\\)\\)?\x01\\'"
+                    body)
+      (irc-event-emit conn "irc.ctcpreply" sender target
+                      (match-string 1 body)
+                      (match-string 2 body))
+    (irc-event-emit conn "irc.notice" sender target body)))
+
+(defun irc-handle-ctcp--ctcpreply (conn _event sender target verb argument)
+  (irc-event-emit conn
+                  (format "irc.ctcpreply.%s" (upcase verb))
+                  sender
+                  target
+                  argument))
+
+(defun irc-handle-ctcp--ctcp-version (conn _event sender _target _argument)
+  (let ((version (irc-connection-get conn :ctcp-version)))
+    (when version
+      (irc-send-ctcpreply conn
+                          (irc-userstring-nick sender)
+                          "VERSION"
+                          version))))
+
+(defun irc-handle-ctcp--ctcp-clientinfo (conn _event sender _target _argument)
+  (let ((clientinfo (irc-connection-get conn :ctcp-clientinfo)))
+    (when clientinfo
+      (irc-send-ctcpreply conn
+                          (irc-userstring-nick sender)
+                          "CLIENTINFO"
+                          clientinfo))))
+
+(defun irc-handle-ctcp--ctcp-source (conn _event sender _target _argument)
+  (let ((source (irc-connection-get conn :ctcp-source)))
+    (when source
+      (irc-send-ctcpreply conn
+                          (irc-userstring-nick sender)
+                          "SOURCE"
+                          source))))
+
+(defun irc-handle-ctcp--ctcp-ping (conn _event sender _target argument)
+  (when argument
+    (irc-send-ctcpreply conn
+                        (irc-userstring-nick sender)
+                        "PING"
+                        argument)))
+
+(defun irc-handle-ctcp--ctcp-time (conn _event sender _target _argument)
+  (irc-send-ctcpreply conn
+                      (irc-userstring-nick sender)
+                      "TIME"
+                      (current-time-string)))
+
+(defun irc-send-ctcp (conn target verb &optional argument)
+  "Send a CTCP VERB request to TARGET, optionally with ARGUMENT."
+  (irc-send-PRIVMSG conn
+                    target
+                    (format "\x01%s%s\x01"
+                            verb
+                            (if argument
+                                (concat " " argument)
+                              ""))))
+
+(defun irc-send-ctcpreply (conn target verb &optional argument)
+  "Send a CTCP VERB reply to TARGET, optionally with ARGUMENT."
+  (irc-send-raw conn
+                (irc--format-command "NOTICE"
+                                     target
+                                     (format "\x01%s%s\x01"
+                                             verb
+                                             (if argument
+                                                 (concat " " argument)
+                                               "")))
+                :drop))
+
+;;;;;;;;;;;;;;;;;;;;;;;;;;;
+;;; Handler: State tracking
+
+(defun irc-handle-state-tracking (table)
+  "Add command handlers to TABLE to track the IRC state.
+
+Connection options used:
+
+:current-nick -- The current nick, or nil if not known/set yet.
+
+Use helper functions to access the information tracked by this
+handler:
+
+- `irc-current-nick'
+- `irc-current-nick-p'
+
+Events emitted:
+
+\"channel.quit\" sender channel reason -- A user quit IRC and
+    left this channel that way."
+  (irc-handler-add table "001" ;; RPL_WELCOME
+                   #'irc-handle-state-tracking--rpl-welcome)
+  (irc-handler-add table "JOIN"
+                   #'irc-handle-state-tracking--JOIN)
+  (irc-handler-add table "PART"
+                   #'irc-handle-state-tracking--PART)
+  (irc-handler-add table "KICK"
+                   #'irc-handle-state-tracking--KICK)
+  (irc-handler-add table "QUIT"
+                   #'irc-handle-state-tracking--QUIT)
+  (irc-handler-add table "NICK"
+                   #'irc-handle-state-tracking--NICK)
+  (irc-handler-add table "PRIVMSG"
+                   #'irc-handle-state-tracking--PRIVMSG)
+  (irc-handler-add table "353" ;; RPL_NAMREPLY
+                   #'irc-handle-state-tracking--rpl-namreply)
+  (irc-handler-add table "366" ;; RPL_ENDOFNAMES
+                   #'irc-handle-state-tracking--rpl-endofnames)
+
+  (irc-handler-add table "TOPIC"
+                   #'irc-handle-state-tracking--TOPIC)
+  (irc-handler-add table "331" ;; RPL_NOTOPIC
+                   #'irc-handle-state-tracking--rpl-notopic)
+  (irc-handler-add table "332" ;; RPL_TOPIC
+                   #'irc-handle-state-tracking--rpl-topic)
+  )
+
+(cl-defstruct irc-channel
+  name
+  topic
+  last-topic
+  folded-name
+  users
+  recent-users
+  receiving-names
+  connection)
+
+(defun irc-channel-from-name (conn name)
+  "Create a new IRC channel object on CONN, named NAME."
+  (make-irc-channel :name name
+                    :folded-name (irc-isupport--case-fold conn name)
+                    :users (make-hash-table :test 'equal)
+                    :recent-users (make-hash-table :test 'equal)
+                    :connection conn))
+
+(defun irc-connection-channel (conn channel-name)
+  "Return the channel object for CHANNEL-NAME on CONN."
+  (let ((channel-table (irc--connection-channel-table conn))
+        (folded-name (irc-isupport--case-fold conn channel-name)))
+    (gethash folded-name channel-table)))
+
+(defun irc-connection-channel-list (conn)
+  "Return the list of channel object on CONN."
+  (let ((channel-list nil))
+    (maphash (lambda (_folded-name channel)
+               (push channel channel-list))
+             (irc--connection-channel-table conn))
+    channel-list))
+
+(defun irc-connection-add-channel (conn channel-name)
+  "Add CHANNEL-NAME to the channel table of CONN."
+  (let* ((channel-table (irc--connection-channel-table conn))
+         (channel (irc-channel-from-name conn channel-name))
+         (folded-name (irc-channel-folded-name channel)))
+    (when (not (gethash folded-name channel-table))
+      (puthash folded-name channel channel-table))))
+
+(defun irc-connection-remove-channel (conn channel-name)
+  "Remove CHANNEL-NAME from the channel table of CONN."
+  (let* ((channel-table (irc--connection-channel-table conn))
+         (folded-name (irc-isupport--case-fold conn channel-name)))
+    (remhash folded-name channel-table)))
+
+(defun irc-current-nick (conn)
+  "Return the current nick on IRC connection CONN, or nil if not set yet."
+  (irc-connection-get conn :current-nick))
+
+(defun irc-current-nick-p (conn nick)
+  "Return t if NICK is our current nick on IRC connection CONN."
+  (let ((current-nick (irc-current-nick conn)))
+    (if (and (stringp nick)
+             (stringp current-nick))
+        (irc-string-equal-p conn current-nick nick)
+      nil)))
+
+(defun irc--connection-channel-table (conn)
+  (let ((table (irc-connection-get conn :channel-table)))
+    (when (not table)
+      (setq table (make-hash-table :test 'equal))
+      (irc-connection-put conn :channel-table table))
+    table))
+
+(cl-defstruct irc-user
+  nick
+  folded-nick
+  userhost
+  join-time
+  last-activity-time
+  part-time
+  connection)
+
+(defun irc-user-from-userstring (conn userstring)
+  "Create an irc-user struct on CONN from USERSTRING.
+
+USERSTRING should be a s tring of the form \"nick!user@host\"."
+  (let ((nick (irc-userstring-nick userstring)))
+    (make-irc-user :nick nick
+                   :folded-nick (irc-isupport--case-fold conn nick)
+                   :userhost (let ((nick-len (length nick)))
+                               (if (>= nick-len (length userstring))
+                                   nil
+                                 (substring userstring (1+ nick-len))))
+                   :connection conn)))
+
+(defun irc-channel-user (channel nick)
+  "Return a user named NICK on channel CHANNEL."
+  (let ((user-table (irc-channel-users channel))
+        (folded-nick (irc-isupport--case-fold (irc-channel-connection channel)
+                                              nick)))
+    (gethash folded-nick user-table)))
+
+(defun irc-channel-recent-user (channel nick)
+  "Return a recent user named NICK on channel CHANNEL."
+  (let ((user-table (irc-channel-recent-users channel))
+        (folded-nick (irc-isupport--case-fold (irc-channel-connection channel)
+                                              nick)))
+    (gethash folded-nick user-table)))
+
+(defun irc-channel-add-user (channel userstring)
+  "Add USER to CHANNEL."
+  (let* ((user-table (irc-channel-users channel))
+         (user (irc-user-from-userstring (irc-channel-connection channel)
+                                         userstring))
+         (folded-nick (irc-user-folded-nick user))
+         (recent-user (irc-channel-recent-user channel (irc-user-nick user))))
+    (when (not (gethash folded-nick user-table))
+      (when (and recent-user
+                 (equal (irc-user-userhost recent-user)
+                        (irc-user-userhost user)))
+        (setf (irc-user-last-activity-time user)
+              (irc-user-last-activity-time recent-user)))
+      (puthash folded-nick user user-table)
+      user)))
+
+(defun irc-channel-remove-user (channel nick)
+  "Remove NICK from CHANNEL."
+  (let* ((user-table (irc-channel-users channel))
+         (recent-user-table (irc-channel-recent-users channel))
+         (folded-nick (irc-isupport--case-fold (irc-channel-connection channel)
+                                               nick))
+         (user (gethash folded-nick user-table)))
+    (remhash folded-nick user-table)
+    (when user
+      (setf (irc-user-part-time user) (float-time))
+      (puthash folded-nick user recent-user-table)
+      (maphash (lambda (folded-nick user)
+                 (when (< (irc-user-part-time user)
+                          (- (float-time)
+                             (* 60 60)))
+                   (remhash folded-nick recent-user-table)))
+               recent-user-table))))
+
+(defun irc-channel-rename-user (channel oldnick newnick)
+  "Update CHANNEL so that the user with nick OLDNICK now has nick NEWNICK."
+  (let ((user-table (irc-channel-users channel))
+        (user (irc-channel-user channel oldnick))
+        (newnick-folded (irc-isupport--case-fold
+                         (irc-channel-connection channel)
+                         newnick))
+        (recent-user (irc-channel-recent-user channel newnick)))
+    (when user
+      (when (and recent-user
+                 (equal (irc-user-userhost recent-user)
+                        (irc-user-userhost user)))
+        (setf (irc-user-last-activity-time user)
+              (irc-user-last-activity-time recent-user)))
+      (remhash (irc-user-folded-nick user) user-table)
+      (setf (irc-user-nick user) newnick)
+      (setf (irc-user-folded-nick user) newnick-folded)
+      (puthash (irc-user-folded-nick user) user user-table))))
+
+(defun irc-handle-state-tracking--rpl-welcome (conn _event _sender target
+                                                    &rest ignored)
+  (irc-connection-put conn :current-nick target))
+
+(defun irc-handle-state-tracking--JOIN (conn _event sender target
+                                             &optional _account _realname)
+  (let ((nick (irc-userstring-nick sender)))
+    (cond
+     ((irc-current-nick-p conn nick)
+      (irc-connection-add-channel conn target))
+     (t
+      (let ((channel (irc-connection-channel conn target)))
+        (when channel
+          (let ((user (irc-channel-add-user channel sender)))
+            (when user
+              (setf (irc-user-join-time user) (float-time))))))))))
+
+(defun irc-handle-state-tracking--PART (conn _event sender target
+                                             &optional _reason)
+  (let ((nick (irc-userstring-nick sender)))
+    (cond
+     ((irc-current-nick-p conn nick)
+      (irc-connection-remove-channel conn target))
+     (t
+      (let ((channel (irc-connection-channel conn target)))
+        (when channel
+          (irc-channel-remove-user channel nick)))))))
+
+(defun irc-handle-state-tracking--KICK (conn _event _sender target nick
+                                             &optional _reason)
+  (cond
+   ((irc-current-nick-p conn nick)
+    (irc-connection-remove-channel conn target))
+   (t
+    (let ((channel (irc-connection-channel conn target)))
+      (when channel
+        (irc-channel-remove-user channel nick))))))
+
+(defun irc-handle-state-tracking--QUIT (conn _event sender
+                                             &optional reason)
+  (let ((nick (irc-userstring-nick sender)))
+    (if (irc-current-nick-p conn nick)
+        (dolist (channel (irc-connection-channel-list conn))
+          (irc-connection-remove-channel conn
+                                         (irc-channel-folded-name channel)))
+      (dolist (channel (irc-connection-channel-list conn))
+        (when (irc-channel-user channel nick)
+          (irc-event-emit conn "channel.quit"
+                          sender
+                          (irc-channel-name channel)
+                          reason))
+        (irc-channel-remove-user channel nick)))))
+
+(defun irc-handle-state-tracking--NICK (conn _event sender new-nick)
+  ;; Update channels
+  (let ((nick (irc-userstring-nick sender)))
+    (dolist (channel (irc-connection-channel-list conn))
+      (irc-channel-rename-user channel nick new-nick)))
+  ;; Update our own nick
+  (when (irc-current-nick-p conn (irc-userstring-nick sender))
+    (irc-connection-put conn :current-nick new-nick)))
+
+(defun irc-handle-state-tracking--PRIVMSG (conn _event sender target _message)
+  (let ((channel (irc-connection-channel conn target))
+        (nick (irc-userstring-nick sender)))
+    (when channel
+      (let ((user (irc-channel-user channel nick)))
+        (when user
+          (setf (irc-user-last-activity-time user) (float-time)))))))
+
+(defun irc-handle-state-tracking--rpl-namreply
+    (conn _event _sender _current-nick _channel-type channel-name nicks)
+  (let ((channel (irc-connection-channel conn channel-name)))
+    (when channel
+      (setf (irc-channel-receiving-names channel)
+            (append (irc-channel-receiving-names channel)
+                    (mapcar (lambda (nick)
+                              (irc-nick-without-prefix
+                               conn
+                               (string-trim nick)))
+                            (split-string nicks)))))))
+
+(defun irc-handle-state-tracking--rpl-endofnames
+    (conn _event _sender _current-nick channel-name _description)
+  (let ((channel (irc-connection-channel conn channel-name)))
+    (when channel
+      (irc-channel--synchronize-nicks channel
+                                      (irc-channel-receiving-names channel))
+      (setf (irc-channel-receiving-names channel) nil))))
+
+(defun irc-channel--synchronize-nicks (channel nicks)
+  "Update the user list of CHANNEL to match NICKS."
+  (let ((have (irc-channel-users channel))
+        (want (make-hash-table :test 'equal)))
+    (dolist (nick nicks)
+      (puthash (irc-isupport--case-fold (irc-channel-connection channel)
+                                        nick)
+               nick
+               want))
+    (maphash (lambda (nick-folded user)
+               (when (not (gethash nick-folded want))
+                 (irc-channel-remove-user channel
+                                          (irc-user-nick user))))
+             have)
+    (maphash (lambda (_nick-folded nick)
+               (irc-channel-add-user channel nick))
+             want)))
+
+(defun irc-handle-state-tracking--TOPIC (conn _event _sender channel new-topic)
+  (let ((channel (irc-connection-channel conn channel)))
+    (when channel
+      (setf (irc-channel-last-topic channel)
+            (irc-channel-topic channel))
+      (setf (irc-channel-topic channel) new-topic))))
+
+(defun irc-handle-state-tracking--rpl-notopic (conn _event _sender
+                                                    _current-nick channel
+                                                    _no-topic-desc)
+  (let ((channel (irc-connection-channel conn channel)))
+    (when channel
+      (setf (irc-channel-topic channel) nil))))
+
+(defun irc-handle-state-tracking--rpl-topic (conn _event _sender _current-nick
+                                                  channel topic)
+  (let ((channel (irc-connection-channel conn channel)))
+    (when channel
+      (setf (irc-channel-topic channel) topic))))
+
+;;;;;;;;;;;;;;,;;;;;;
+;;; Handler: NickServ
+
+(defun irc-handle-nickserv (table)
+  "Add command handlers to TABLE to deal with NickServ.
+
+Connection options used:
+
+:nickserv-nick -- The nick to register as
+
+:nickserv-password -- The password for nickserv; can be a function and
+  is then called with the IRC connection as its sole argument
+
+:nickserv-mask -- A regular expression matching the correct NickServ's
+  nick!user@host string to avoid fakes
+
+:nickserv-identify-challenge -- A regular expression matching the
+  challenge sent by NickServ to request identification
+
+:nickserv-identify-command -- The raw IRC command to send to identify;
+  expands {nick} and {password} when present
+
+:nickserv-identify-confirmation -- A regular expression matching the
+  confirmation message from NickServ after successful identification
+
+:nickserv-ghost-command -- The raw IRC comment to ghost your
+  original nick; expands {nick} and {password}. Set this to nil
+  to disable ghosting and nick regaining.
+
+:nickserv-ghost-confirmation -- A regular expression matching the
+  confirmation message that the nick was ghosted
+
+Events emitted:
+
+\"nickserv.identified\" -- We have successfully identified with nickserv.
+
+\"nickserv.ghosted\" -- We have ghosted a nick."
+  (irc-handler-add table "irc.registered" #'irc-handle-nickserv--registered)
+  (irc-handler-add table "NOTICE" #'irc-handle-nickserv--NOTICE)
+  (irc-handler-add table "PRIVMSG" #'irc-handle-nickserv--NOTICE)
+  (irc-handler-add table "NICK" #'irc-handle-nickserv--NICK))
+
+(defun irc-handle-nickserv--password (conn)
+  (let ((password (irc-connection-get conn :nickserv-password)))
+    (if (functionp password)
+        (funcall password conn)
+      password)))
+
+(defun irc-handle-nickserv--registered (conn _event current-nick)
+  (let ((ghost-command (irc-connection-get conn :nickserv-ghost-command))
+        (wanted-nick (irc-connection-get conn :nickserv-nick))
+        (password (irc-handle-nickserv--password conn)))
+    (when (and ghost-command
+               wanted-nick
+               password
+               (not (irc-string-equal-p conn current-nick wanted-nick)))
+      (irc-send-raw conn
+                    (irc-format ghost-command
+                                'nick wanted-nick
+                                'password password)))))
+
+(defun irc-handle-nickserv--NOTICE (conn _event sender _target message)
+  (let ((nickserv-mask (irc-connection-get conn :nickserv-mask))
+        identify-challenge identify-command identify-confirmation
+        ghost-confirmation
+        nickserv-nick nickserv-password)
+    (when (and nickserv-mask (string-match nickserv-mask sender))
+      (setq identify-challenge
+            (irc-connection-get conn :nickserv-identify-challenge))
+      (setq identify-command
+            (irc-connection-get conn :nickserv-identify-command))
+      (setq identify-confirmation
+            (irc-connection-get conn :nickserv-identify-confirmation))
+      (setq ghost-confirmation
+            (irc-connection-get conn :nickserv-ghost-confirmation))
+      (setq nickserv-nick (irc-connection-get conn :nickserv-nick))
+      (setq nickserv-password (irc-handle-nickserv--password conn))
+      (cond
+       ;; Identify
+       ((and identify-challenge
+             identify-command
+             nickserv-nick
+             nickserv-password
+             (string-match identify-challenge message))
+        (irc-send-raw conn
+                      (irc-format identify-command
+                                  'nick nickserv-nick
+                                  'password nickserv-password)))
+       ;; Identification confirmed
+       ((and identify-confirmation
+             (string-match identify-confirmation message))
+        (irc-event-emit conn "nickserv.identified"))
+       ;; Ghosting confirmed
+       ((and ghost-confirmation
+             (string-match ghost-confirmation message))
+        (irc-event-emit conn "nickserv.ghosted")
+        (irc-connection-put conn :nickserv-regaining-nick t)
+        (when nickserv-nick
+          (irc-send-NICK conn nickserv-nick)))))))
+
+(defun irc-handle-nickserv--NICK (conn _event _sender new-nick)
+  (when (and (irc-connection-get conn :nickserv-regaining-nick)
+             (irc-string-equal-p conn new-nick
+                                 (irc-connection-get conn :nickserv-nick)))
+    (irc-connection-put conn :nickserv-regaining-nick nil)
+    (irc-event-emit conn "nickserv.regained")))
+
+(defun irc-format (format &rest args)
+  "Return a formatted version of FORMAT, using substitutions from ARGS.
+
+The substitutions are identified by braces ('{' and '}')."
+  (with-temp-buffer
+    (insert format)
+    (goto-char (point-min))
+    (while (re-search-forward "{\\([^}]*\\)}" nil t)
+      (replace-match (format "%s" (plist-get args (intern (match-string 1))))
+                     t t))
+    (buffer-string)))
+
+;;;;;;;;;;;;;;;;;;;;;;
+;;; Handler: Auto-Join
+
+(defun irc-handle-auto-join (table)
+  "Add command handlers to TABLE to deal with NickServ.
+
+Connection options used:
+
+:auto-join-after-registration -- List of channels to join
+  immediately after registration with the server
+
+:auto-join-after-host-hiding -- List of channels to join
+  after our host was hidden
+
+:auto-join-after-nick-acquisition -- List of channels to join
+  after we gained our desired nick
+
+:auto-join-after-nickserv-identification -- List of channels
+  to join after we identified successfully with NickServ"
+  (irc-handler-add table "irc.registered" #'irc-handle-auto-join--registered)
+  (irc-handler-add table "396" ;; RPL_HOSTHIDDEN
+                   #'irc-handle-auto-join--rpl-hosthidden)
+  (irc-handler-add table "nickserv.regained"
+                   #'irc-handle-auto-join--nickserv-regained)
+  (irc-handler-add table "nickserv.identified"
+                   #'irc-handle-auto-join--nickserv-identified)
+  (irc-handler-add table "sasl.login"
+                   #'irc-handle-auto-join--sasl-login))
+
+(defun irc-handle-auto-join--registered (conn _event _current-nick)
+  (dolist (channel (irc-connection-get conn :auto-join-after-registration))
+    (irc-send-JOIN conn channel)))
+
+(defun irc-handle-auto-join--rpl-hosthidden (conn _event _sender _target _host
+                                                  _description)
+  (dolist (channel (irc-connection-get conn :auto-join-after-host-hiding))
+    (irc-send-JOIN conn channel)))
+
+(defun irc-handle-auto-join--nickserv-regained (conn _event)
+  (dolist (channel (irc-connection-get
+                    conn :auto-join-after-nick-acquisition))
+    (irc-send-JOIN conn channel)))
+
+(defun irc-handle-auto-join--nickserv-identified (conn event)
+  (dolist (channel (irc-connection-get
+                    conn :auto-join-after-nickserv-identification))
+    (irc-send-JOIN conn channel))
+  (if (irc-string-equal-p conn
+                          (irc-connection-get conn :nick)
+                          (irc-connection-get conn :nickserv-nick))
+      (irc-handle-auto-join--nickserv-regained conn event)))
+
+(defun irc-handle-auto-join--sasl-login (conn _event &rest ignored)
+  (dolist (channel (irc-connection-get
+                    conn :auto-join-after-sasl-login))
+    (irc-send-JOIN conn channel)))
+
+(provide 'irc)
+;;; irc.el ends here