diff options
author | Miles Bader <miles@gnu.org> | 2006-01-29 13:08:58 +0000 |
---|---|---|
committer | Miles Bader <miles@gnu.org> | 2006-01-29 13:08:58 +0000 |
commit | 597993cf4433604ea65e40d33ad6cfe83dab2fb7 (patch) | |
tree | 9e9cc6dbc0968bc83d7657c17ecade6b56691f89 /lisp/erc/erc-dcc.el | |
parent | 33c7860d38eb0f5416630b54a7a1b878810a5d3b (diff) | |
download | emacs-597993cf4433604ea65e40d33ad6cfe83dab2fb7.tar.gz |
Revision: emacs@sv.gnu.org/emacs--devo--0--patch-22
Creator: Michael Olson <mwolson@gnu.org>
Install ERC.
Diffstat (limited to 'lisp/erc/erc-dcc.el')
-rw-r--r-- | lisp/erc/erc-dcc.el | 1135 |
1 files changed, 1135 insertions, 0 deletions
diff --git a/lisp/erc/erc-dcc.el b/lisp/erc/erc-dcc.el new file mode 100644 index 00000000000..d5789a51708 --- /dev/null +++ b/lisp/erc/erc-dcc.el @@ -0,0 +1,1135 @@ +;;; erc-dcc.el --- CTCP DCC module for ERC + +;; Copyright (C) 1993, 1994, 1995, 1998, 2002, 2003, 2004 +;; Free Software Foundation, Inc. + +;; Author: Ben A. Mesander <ben@gnu.ai.mit.edu> +;; Noah Friedman <friedman@prep.ai.mit.edu> +;; Per Persson <pp@sno.pp.se> +;; Maintainer: mlang@delysid.org +;; Keywords: comm, processes +;; Created: 1994-01-23 + +;; 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: + +;; This file provides Direct Client-to-Client support for the Emacs IRC Client. +;; +;; The original code was taken from zenirc-dcc.el, heavily mangled and +;; rewritten to support the way how ERC operates. Server socket support +;; was added for DCC CHAT and SEND afterwards. Thanks +;; to the original authors for their work. +;; +;; To use this file, put +;; (require 'erc-dcc) +;; in your .emacs. +;; +;; Provided commands +;; /dcc chat nick - Either accept pending chat offer from nick, or offer +;; DCC chat to nick +;; /dcc close type [nick] - Close DCC connection (SEND/GET/CHAT) with nick +;; /dcc get nick [file] - Accept DCC offer from nick +;; /dcc list - List all DCC offers/connections +;; /dcc send nick file - Offer DCC SEND to nick +;; +;; Please note that offering DCC connections (offering chats and sending +;; files) is only supported with Emacs 21.3.50 (CVS). + +;;; Code: + +(require 'erc) +(eval-when-compile + (require 'pcomplete)) + +(defgroup erc-dcc nil + "DCC stands for Direct Client Communication, where you and your +friend's client programs connect directly to each other, +bypassing IRC servers and their occasional \"lag\" or \"split\" +problems. Like /MSG, the DCC chat is completely private. + +Using DCC get and send, you can transfer files directly from and to other +IRC users." + :group 'erc) + +(defcustom erc-verbose-dcc t + "*If non-nil, be verbose about DCC activity reporting." + :group 'erc-dcc + :type 'boolean) + +(defvar erc-dcc-list nil + "List of DCC connections. Looks like: + ((:nick \"nick!user@host\" :type GET :peer proc :parent proc :size size :file file) + (:nick \"nick!user@host\" :type CHAT :peer proc :parent proc) + (:nick \"nick\" :type SEND :peer server-proc :parent parent-proc :file + file :sent <marker> :confirmed <marker>)) + + :nick - a user or userhost for the peer. combine with :parent to reach them + + :type - the type of DCC connection - SEND for outgoing files, GET for + incoming, and CHAT for both directions. To tell which end started + the DCC chat, look at :peer + + :peer - the other end of the DCC connection. In the case of outgoing DCCs, + this represents a server process until a connection is established + + :parent - the server process where the dcc connection was established. + Note that this can be nil or an invalid process since a DCC + connection is in general independent from a particular server + connection after it was established. + + :file - for outgoing sends, the full path to the file. for incoming sends, + the suggested filename or vetted filename + + :size - size of the file, may be nil on incoming DCCs") + +(defun erc-dcc-list-add (type nick peer parent &rest args) + "Add a new entry of type TYPE to `erc-dcc-list' and return it." + (car + (setq erc-dcc-list + (cons + (append (list :nick nick :type type :peer peer :parent parent) args) + erc-dcc-list)))) + +;; This function takes all the usual args as open-network-stream, plus one +;; more: the entry data from erc-dcc-list for this particular process. +(defvar erc-dcc-connect-function 'erc-dcc-open-network-stream) + +(defun erc-dcc-open-network-stream (procname buffer addr port entry) + (if nil; (fboundp 'open-network-stream-nowait) ;; this currently crashes + ;; cvs emacs + (open-network-stream-nowait procname buffer addr port) + (open-network-stream procname buffer addr port))) + +(erc-define-catalog + 'english + '((dcc-chat-discarded + . "DCC: previous chat request from %n (%u@%h) discarded") + (dcc-chat-ended . "DCC: chat with %n ended %t: %e") + (dcc-chat-no-request . "DCC: chat request from %n not found") + (dcc-chat-offered . "DCC: chat offered by %n (%u@%h:%p)") + (dcc-chat-offer . "DCC: offering chat to %n") + (dcc-chat-accept . "DCC: accepting chat from %n") + (dcc-chat-privmsg . "=%n= %m") + (dcc-closed . "DCC: Closed %T from %n") + (dcc-command-undefined + . "DCC: %c undefined subcommand. GET, CHAT and LIST are defined.") + (dcc-ctcp-errmsg . "DCC: `%s' is not a DCC subcommand known to this client") + (dcc-ctcp-unknown . "DCC: unknown dcc command `%q' from %n (%u@%h)") + (dcc-get-bytes-received . "DCC: %f: %b bytes received") + (dcc-get-complete + . "DCC: file %f transfer complete (%s bytes in %t seconds)") + (dcc-get-cmd-aborted . "DCC: Aborted getting %f from %n") + (dcc-get-file-too-long + . "DCC: %f: File longer than sender claimed; aborting transfer") + (dcc-get-notfound . "DCC: %n hasn't offered %f for DCC transfer") + (dcc-list-head . "DCC: From Type Active Size Filename") + (dcc-list-line . "DCC: -------- ---- ------ ------------ --------") + (dcc-list-item . "DCC: %-8n %-4t %-6a %-12s %f") + (dcc-list-end . "DCC: End of list.") + (dcc-malformed . "DCC: error: %n (%u@%h) sent malformed request: %q") + (dcc-privileged-port + . "DCC: possibly bogus request: %p is a privileged port.") + (dcc-request-bogus . "DCC: bogus dcc `%r' from %n (%u@%h)") + (dcc-send-finished . "DCC: SEND of %f to %n finished (size %s)") + (dcc-send-offered . "DCC: file %f offered by %n (%u@%h) (size %s)") + (dcc-send-offer . "DCC: offering %f to %n"))) + +;;; Misc macros and utility functions + +(defun erc-dcc-member (&rest args) + "Return the first matching entry in `erc-dcc-list' which satisfies the +constraints given as a plist in ARGS. Returns nil on no match. + +The property :nick is treated specially, if it contains a '!' character, +it is treated as a nick!user@host string, and compared with the :nick property +value of the individual elements using string-equal. Otherwise it is +compared with `erc-nick-equal-p' which is IRC case-insensitive." + (let ((list erc-dcc-list) + result test) + ;; for each element in erc-dcc-list + (while (and list (not result)) + (let ((elt (car list)) + (prem args) + (cont t)) + ;; loop through the constraints + (while (and prem cont) + (let ((prop (car prem)) + (val (cadr prem))) + (setq prem (cddr prem) + ;; plist-member is a predicate in xemacs + test (and (plist-member elt prop) + (plist-get elt prop))) + ;; if the property exists and is equal, we continue, else, try the + ;; next element of the list + (or (and (eq prop :nick) (string-match "!" val) + test (string-equal test val)) + (and (eq prop :nick) + test val + (erc-nick-equal-p + (erc-extract-nick test) + (erc-extract-nick val))) + ;; not a nick + (eq test val) + (setq cont nil)))) + (if cont + (setq result elt) + (setq list (cdr list))))) + result)) + +;; msa wrote this nifty little frob to convert an n-byte integer to a packed +;; string. +(defun erc-pack-int (value count) + (if (> count 0) + (concat (erc-pack-int (/ value 256) (1- count)) + (char-to-string (% value 256))) + "")) + +(defun erc-unpack-int (str) + "Unpack a 1-4 character packed string into an integer." + (let ((len (length str)) + (num 0) + (count 0)) + (erc-assert (<= len 4)) ;; this isn't going to fit in elisp bounds + (while (< count len) + (setq num (+ num (lsh (aref str (- len count 1)) (* 8 count)))) + (setq count (1+ count))) + num)) + +(defconst erc-dcc-ipv4-regexp + (concat "^" + (mapconcat #'identity (make-list 4 "\\([0-9]\\{1,3\\}\\)") "\\.") + "$")) + +(defun erc-ip-to-decimal (ip) + "Convert IP address to its decimal representation. +Argument IP is the address as a string. The result is also a string." + (interactive "sIP Address: ") + (if (not (string-match erc-dcc-ipv4-regexp ip)) + (error "Not an IP address") + (let* ((ips (mapcar + (lambda (str) + (let ((n (string-to-number str))) + (if (and (>= n 0) (< n 256)) + n + (error "%d out of range" n)))) + (split-string ip "\\."))) + (res (+ (* (car ips) 16777216.0) + (* (nth 1 ips) 65536.0) + (* (nth 2 ips) 256.0) + (nth 3 ips)))) + (if (interactive-p) + (message "%s is %.0f" ip res) + (format "%.0f" res))))) + +(defun erc-decimal-to-ip (dec) + "Convert a decimal representation DEC to an IP address. +The result is also a string." + (when (stringp dec) + (setq dec (string-to-number (concat dec ".0")))) + (let* ((first (floor (/ dec 16777216.0))) + (first-rest (- dec (* first 16777216.0))) + (second (floor (/ first-rest 65536.0))) + (second-rest (- first-rest (* second 65536.0))) + (third (floor (/ second-rest 256.0))) + (third-rest (- second-rest (* third 256.0))) + (fourth (floor third-rest))) + (format "%s.%s.%s.%s" first second third fourth))) + +;;; Server code + +(defcustom erc-dcc-host nil + "*IP address to use for outgoing DCC offers. +Should be set to a string or nil, if nil, automatic detection of the +host interface to use will be attempted." + :group 'erc-dcc + :type (list 'choice (list 'const :tag "Auto-detect" nil) + (list 'string :tag "IP-address" + :valid-regexp erc-dcc-ipv4-regexp))) + +(defcustom erc-dcc-send-request 'ask + "*How to treat incoming DCC Send requests. +'ask - Report the Send request, and wait for the user to manually accept it + You might want to set `erc-dcc-auto-masks' for this. +'auto - Automatically accept the request and begin downloading the file +'ignore - Ignore incoming DCC Send requests completely." + :group 'erc-dcc + :type '(choice (const ask) (const auto) (const ignore))) + +(defun erc-dcc-get-host (proc) + "Returns the local IP address used for an open PROCess." + (format-network-address (process-contact proc :local) t)) + +(defun erc-dcc-host () + "Determine the IP address we are using. +If variable `erc-dcc-host' is non-nil, use it. Otherwise call +`erc-dcc-get-host' on the erc-server-process." + (or erc-dcc-host (erc-dcc-get-host erc-server-process) + (error "Unable to determine local address"))) + +(defcustom erc-dcc-port-range nil + "If nil, any available user port is used for outgoing DCC connections. +If set to a cons, it specifies a range of ports to use in the form (min . max)" + :group 'erc-dcc + :type '(choice + (const :tag "Any port" nil) + (cons :tag "Port range" + (integer :tag "Lower port") + (integer :tag "Upper port")))) + +(defcustom erc-dcc-auto-masks nil + "List of regexps matching user identifiers whose DCC send offers should be +accepted automatically. A user identifier has the form \"nick!login@host\". +For instance, to accept all incoming DCC send offers automatically, add the +string \".*!.*@.*\" to this list." + :group 'erc-dcc + :type '(repeat regexp)) + +(defun erc-dcc-server (name filter sentinel) + "Start listening on a port for an incoming DCC connection. Returns the newly +created subprocess, or nil." + (let ((port (or (and erc-dcc-port-range (car erc-dcc-port-range)) t)) + (upper (and erc-dcc-port-range (cdr erc-dcc-port-range))) + process) + (while (not process) + (condition-case err + (setq process + (make-network-process :name name + :buffer nil + :host (erc-dcc-host) + :service port + :nowait t + :noquery nil + :filter filter + :sentinel sentinel + :log #'erc-dcc-server-accept + :server t)) + (file-error + (unless (and (string= "Cannot bind server socket" (cadr err)) + (string= "address already in use" (caddr err))) + (signal (car err) (cdr err))) + (setq port (1+ port)) + (unless (< port upper) + (error "No available ports in erc-dcc-port-range"))))) + process)) + +(defun erc-dcc-server-accept (server client message) + "Log an accepted DCC offer, then terminate the listening process and set up +the accepted connection." + (erc-log (format "(erc-dcc-server-accept): server %s client %s message %s" + server client message)) + (when (and (string-match "^accept from " message) + (processp server) (processp client)) + (let ((elt (erc-dcc-member :peer server))) + ;; change the entry in erc-dcc-list from the listening process to the + ;; accepted process + (setq elt (plist-put elt :peer client)) + ;; delete the listening process, as we've accepted the connection + (delete-process server)))) + +;;; Interactive command handling + +(defcustom erc-dcc-get-default-directory nil + "*Default directory for incoming DCC file transfers. +If this is nil, then the current value of `default-directory' is used." + :group 'erc-dcc + :type '(choice (const nil :tag "Default directory") directory)) + +;;;###autoload +(defun erc-cmd-DCC (cmd &rest args) + "Parser for /dcc command. +This figures out the dcc subcommand and calls the appropriate routine to +handle it. The function dispatched should be named \"erc-dcc-do-FOO-command\", +where FOO is one of CLOSE, GET, SEND, LIST, CHAT, etc." + (when cmd + (let ((fn (intern-soft (concat "erc-dcc-do-" (upcase cmd) "-command")))) + (if fn + (apply fn erc-server-process args) + (erc-display-message + nil 'notice 'active + 'dcc-command-undefined ?c cmd) + (apropos "erc-dcc-do-.*-command") + t)))) + +;;;###autoload +(defun pcomplete/erc-mode/DCC () + "Provides completion for the /DCC command." + (pcomplete-here (append '("chat" "close" "get" "list") + (when (fboundp 'make-network-process) '("send")))) + (pcomplete-here + (case (intern (downcase (pcomplete-arg 1))) + (chat (mapcar (lambda (elt) (plist-get elt :nick)) + (erc-remove-if-not + #'(lambda (elt) + (eq (plist-get elt :type) 'CHAT)) + erc-dcc-list))) + (close (remove-duplicates + (mapcar (lambda (elt) (symbol-name (plist-get elt :type))) + erc-dcc-list) :test 'string=)) + (get (mapcar #'erc-dcc-nick + (erc-remove-if-not + #'(lambda (elt) + (eq (plist-get elt :type) 'GET)) + erc-dcc-list))) + (send (pcomplete-erc-all-nicks)))) + (pcomplete-here + (case (intern (downcase (pcomplete-arg 2))) + (get (mapcar (lambda (elt) (plist-get elt :file)) + (erc-remove-if-not + #'(lambda (elt) + (and (eq (plist-get elt :type) 'GET) + (erc-nick-equal-p (erc-extract-nick + (plist-get elt :nick)) + (pcomplete-arg 1)))) + erc-dcc-list))) + (close (mapcar #'erc-dcc-nick + (erc-remove-if-not + #'(lambda (elt) + (eq (plist-get elt :type) + (intern (upcase (pcomplete-arg 1))))) + erc-dcc-list))) + (send (pcomplete-entries))))) + +(defun erc-dcc-do-CHAT-command (proc &optional nick) + (when nick + (let ((elt (erc-dcc-member :nick nick :type 'CHAT :parent proc))) + (if (and elt (not (processp (plist-get elt :peer)))) + ;; accept an existing chat offer + ;; FIXME: perhaps /dcc accept like other clients? + (progn (erc-dcc-chat-accept elt erc-server-process) + (erc-display-message + nil 'notice 'active + 'dcc-chat-accept ?n nick) + t) + (erc-dcc-chat nick erc-server-process) + (erc-display-message + nil 'notice 'active + 'dcc-chat-offer ?n nick) + t)))) + +(defun erc-dcc-do-CLOSE-command (proc &optional type nick) + "/dcc close type nick +type and nick are optional." + ;; FIXME, should also work if only nick is specified + (when (string-match (concat "^\\s-*\\(\\S-+\\)? *\\(" + erc-valid-nick-regexp "\\)?\\s-*$") line) + (let ((type (when (match-string 1 line) + (intern (upcase (match-string 1 line))))) + (nick (match-string 2 line)) + (ret t)) + (while ret + (if nick + (setq ret (erc-dcc-member :type type :nick nick)) + (setq ret (erc-dcc-member :type type))) + (when ret + ;; found a match - delete process if it exists. + (and (processp (plist-get ret :peer)) + (delete-process (plist-get ret :peer))) + (setq erc-dcc-list (delq ret erc-dcc-list)) + (erc-display-message + nil 'notice 'active + 'dcc-closed + ?T (plist-get ret :type) + ?n (erc-extract-nick (plist-get ret :nick)))))) + t)) + +(defun erc-dcc-do-GET-command (proc nick &optional file) + (let* ((elt (erc-dcc-member :nick nick :type 'GET)) + (filename (or file (plist-get elt :file) "unknown"))) + (if elt + (let* ((file (read-file-name + (format "Local filename (default %s): " + (file-name-nondirectory filename)) + (or erc-dcc-get-default-directory + default-directory) + (expand-file-name (file-name-nondirectory filename) + (or erc-dcc-get-default-directory + default-directory))))) + (cond ((file-exists-p file) + (if (yes-or-no-p (format "File %s exists. Overwrite? " + file)) + (erc-dcc-get-file elt file proc) + (erc-display-message + nil '(notice error) proc + 'dcc-get-cmd-aborted + ?n nick ?f filename))) + (t + (erc-dcc-get-file elt file proc)))) + (erc-display-message + nil '(notice error) 'active + 'dcc-get-notfound ?n nick ?f filename)))) + +(defun erc-dcc-do-LIST-command (proc) + "This is the handler for the /dcc list command. +It lists the current state of `erc-dcc-list' in an easy to read manner." + (let ((alist erc-dcc-list) + size elt) + (erc-display-message + nil 'notice 'active + 'dcc-list-head) + (erc-display-message + nil 'notice 'active + 'dcc-list-line) + (while alist + (setq elt (car alist) + alist (cdr alist)) + + (setq size (or (and (plist-member elt :size) + (plist-get elt :size)) + "")) + (setq size + (cond ((null size) "") + ((numberp size) (number-to-string size)) + ((string= size "") "unknown"))) + (erc-display-message + nil 'notice 'active + 'dcc-list-item + ?n (erc-dcc-nick elt) + ?t (plist-get elt :type) + ?a (if (processp (plist-get elt :peer)) + (process-status (plist-get elt :peer)) + "no") + ?s (concat size + (if (and (eq 'GET (plist-get elt :type)) + (plist-member elt :file) + (buffer-live-p (get-buffer (plist-get elt :file))) + (plist-member elt :size)) + (concat " (" (number-to-string + (* 100 + (/ (buffer-size + (get-buffer (plist-get elt :file))) + (plist-get elt :size)))) + "%)"))) + ?f (or (and (plist-member elt :file) (plist-get elt :file)) ""))) + (erc-display-message + nil 'notice 'active + 'dcc-list-end) + t)) + +(defun erc-dcc-do-SEND-command (proc nick file) + "Offer FILE to NICK by sending a ctcp dcc send message." + (if (file-exists-p file) + (progn + (erc-display-message + nil 'notice 'active + 'dcc-send-offer ?n nick ?f file) + (erc-dcc-send-file nick file) t) + (erc-display-message nil '(notice error) proc "File not found") t)) + +;;; Server message handling (i.e. messages from remote users) + +;;;###autoload +(defvar erc-ctcp-query-DCC-hook '(erc-ctcp-query-DCC) + "Hook variable for CTCP DCC queries") + +(defvar erc-dcc-query-handler-alist + '(("SEND" . erc-dcc-handle-ctcp-send) + ("CHAT" . erc-dcc-handle-ctcp-chat))) + +;;;###autoload +(defun erc-ctcp-query-DCC (proc nick login host to query) + "The function called when a CTCP DCC request is detected by the client. +It examines the DCC subcommand, and calls the appropriate routine for +that subcommand." + (let* ((cmd (cadr (split-string query " "))) + (handler (cdr (assoc cmd erc-dcc-query-handler-alist)))) + (if handler + (funcall handler proc query nick login host to) + ;; FIXME: Send a ctcp error notice to the remote end? + (erc-display-message + nil '(notice error) proc + 'dcc-ctcp-unknown + ?q query ?n nick ?u login ?h host)))) + +(defconst erc-dcc-ctcp-query-send-regexp + "^DCC SEND \\([^ ]+\\) \\([0-9]+\\) \\([0-9]+\\) *\\([0-9]*\\)") + +(defun erc-dcc-handle-ctcp-send (proc query nick login host to) + "This is called if a CTCP DCC SEND subcommand is sent to the client. +It extracts the information about the dcc request and adds it to +`erc-dcc-list'." + (unless (eq erc-dcc-send-request 'ignore) + (cond + ((not (erc-current-nick-p to)) + ;; DCC SEND requests must be sent to you, and you alone. + (erc-display-message + nil 'notice proc + 'dcc-request-bogus + ?r "SEND" ?n nick ?u login ?h host)) + ((string-match erc-dcc-ctcp-query-send-regexp query) + (let ((filename (match-string 1 query)) + (ip (erc-decimal-to-ip (match-string 2 query))) + (port (match-string 3 query)) + (size (match-string 4 query))) + ;; FIXME: a warning really should also be sent + ;; if the ip address != the host the dcc sender is on. + (erc-display-message + nil 'notice proc + 'dcc-send-offered + ?f filename ?n nick ?u login ?h host + ?s (if (string= size "") "unknown" size)) + (and (< (string-to-number port) 1025) + (erc-display-message + nil 'notice proc + 'dcc-privileged-port + ?p port)) + (erc-dcc-list-add + 'GET (format "%s!%s@%s" nick login host) + nil proc + :ip ip :port port :file filename + :size (string-to-number size)) + (if (and (eq erc-dcc-send-request 'auto) + (erc-dcc-auto-mask-p (format "\"%s!%s@%s\"" nick login host))) + (erc-dcc-get-file (car erc-dcc-list) filename proc)))) + (t + (erc-display-message + nil 'notice proc + 'dcc-malformed + ?n nick ?u login ?h host ?q query))))) + +(defun erc-dcc-auto-mask-p (spec) + "Takes a full SPEC of a user in the form \"nick!login@host\" and +matches against all the regexp's in `erc-dcc-auto-masks'. If any +match, returns that regexp and nil otherwise." + (let ((lst erc-dcc-auto-masks)) + (while (and lst + (not (string-match (car lst) spec))) + (setq lst (cdr lst))) + (and lst (car lst)))) + +(defconst erc-dcc-ctcp-query-chat-regexp + "^DCC CHAT +chat +\\([0-9]+\\) +\\([0-9]+\\)") + +(defcustom erc-dcc-chat-request 'ask + "*How to treat incoming DCC Chat requests. +'ask - Report the Chat request, and wait for the user to manually accept it +'auto - Automatically accept the request and open a new chat window +'ignore - Ignore incoming DCC chat requests completely." + :group 'erc-dcc + :type '(choice (const ask) (const auto) (const ignore))) + +(defun erc-dcc-handle-ctcp-chat (proc query nick login host to) + (unless (eq erc-dcc-chat-request 'ignore) + (cond + (;; DCC CHAT requests must be sent to you, and you alone. + (not (erc-current-nick-p to)) + (erc-display-message + nil '(notice error) proc + 'dcc-request-bogus ?r "CHAT" ?n nick ?u login ?h host)) + ((string-match erc-dcc-ctcp-query-chat-regexp query) + ;; We need to use let* here, since erc-dcc-member might clutter + ;; the match value. + (let* ((ip (erc-decimal-to-ip (match-string 1 query))) + (port (match-string 2 query)) + (elt (erc-dcc-member :nick nick :type 'CHAT))) + ;; FIXME: A warning really should also be sent if the ip + ;; address != the host the dcc sender is on. + (erc-display-message + nil 'notice proc + 'dcc-chat-offered + ?n nick ?u login ?h host ?p port) + (and (< (string-to-number port) 1025) + (erc-display-message + nil 'notice proc + 'dcc-privileged-port ?p port)) + (cond (elt + ;; XXX: why are we updating ip/port on the existing connection? + (setq elt (plist-put (plist-put elt :port port) :ip ip)) + (erc-display-message + nil 'notice proc + 'dcc-chat-discarded ?n nick ?u login ?h host)) + (t + (erc-dcc-list-add + 'CHAT (format "%s!%s@%s" nick login host) + nil proc + :ip ip :port port))) + (if (eq erc-dcc-chat-request 'auto) + (erc-dcc-chat-accept (erc-dcc-member :nick nick :type 'CHAT) + proc)))) + (t + (erc-display-message + nil '(notice error) proc + 'dcc-malformed ?n nick ?u login ?h host ?q query))))) + + +(defvar erc-dcc-entry-data nil + "Holds the `erc-dcc-list' entry for this DCC connection.") +(make-variable-buffer-local 'erc-dcc-entry-data) + +;;; SEND handling + +(defcustom erc-dcc-block-size 1024 + "*Block size to use for DCC SEND sessions." + :group 'erc-dcc + :type 'integer) + +(defcustom erc-dcc-pump-bytes nil + "*If set to an integer, keep sending until that number of bytes are +unconfirmed." + :group 'erc-dcc + :type '(choice (const nil) integer)) + +(defsubst erc-dcc-get-parent (proc) + (plist-get (erc-dcc-member :peer proc) :parent)) + +(defun erc-dcc-send-block (proc) + "Send one block of data. +PROC is the process-object of the DCC connection. Returns the number of +bytes sent." + (let* ((elt (erc-dcc-member :peer proc)) + (confirmed-marker (plist-get elt :sent)) + (sent-marker (plist-get elt :sent))) + (with-current-buffer (process-buffer proc) + (when erc-verbose-dcc + (erc-display-message + nil 'notice (erc-dcc-get-parent proc) + (format "DCC: Confirmed %d, sent %d, sending block now" + (- confirmed-marker (point-min)) + (- sent-marker (point-min))))) + (let* ((end (min (+ sent-marker erc-dcc-block-size) + (point-max))) + (string (buffer-substring-no-properties sent-marker end))) + (when (< sent-marker end) + (set-marker sent-marker end) + (process-send-string proc string)) + (length string))))) + +(defun erc-dcc-send-filter (proc string) + (erc-assert (= (% (length string) 4) 0)) + (let* ((size (erc-unpack-int (substring string (- (length string) 4)))) + (elt (erc-dcc-member :peer proc)) + (parent (plist-get elt :parent)) + (sent-marker (plist-get elt :sent)) + (confirmed-marker (plist-get elt :confirmed))) + (with-current-buffer (process-buffer proc) + (set-marker confirmed-marker (+ (point-min) size)) + (cond + ((and (= confirmed-marker sent-marker) + (= confirmed-marker (point-max))) + (erc-display-message + nil 'notice parent + 'dcc-send-finished + ?n (plist-get elt :nick) + ?f buffer-file-name + ?s (number-to-string (- sent-marker (point-min)))) + (setq erc-dcc-list (delete elt erc-dcc-list)) + (set-buffer-modified-p nil) + (kill-buffer (current-buffer)) + (delete-process proc)) + ((<= confirmed-marker sent-marker) + (while (and (< (- sent-marker confirmed-marker) + (or erc-dcc-pump-bytes + erc-dcc-block-size)) + (> (erc-dcc-send-block proc) 0)))) + ((> confirmed-marker sent-marker) + (erc-display-message + nil 'notice parent + (format "DCC: Client confirmed too much!")) + (delete-process proc)))))) + +(defcustom erc-dcc-send-connect-hook + '((lambda (proc) + (erc-display-message + nil 'notice (erc-dcc-get-parent proc) + (format "DCC: SEND connect from %s" + (format-network-address (process-contact proc :remote))))) + erc-dcc-send-block) + "*Hook run whenever the remote end of a DCC SEND offer connected to your +listening port." + :group 'erc-dcc + :type 'hook) + +(defun erc-dcc-nick (plist) + "Extract the nickname portion of the :nick property value in PLIST." + (erc-extract-nick (plist-get plist :nick))) + +(defun erc-dcc-send-sentinel (proc event) + (let* ((elt (erc-dcc-member :peer proc)) + (buf (marker-buffer (plist-get elt :sent)))) + (cond + ((string-match "^open from " event) + (when elt + (with-current-buffer buf + (set-process-buffer proc buf) + (setq erc-dcc-entry-data elt)) + (run-hook-with-args 'erc-dcc-send-connect-hook proc)))))) + +(defun erc-dcc-find-file (file) + (with-current-buffer (generate-new-buffer (file-name-nondirectory file)) + (insert-file-contents-literally file) + (setq buffer-file-name file) + (current-buffer))) + +(defun erc-dcc-file-to-name (file) + (with-temp-buffer + (insert (file-name-nondirectory file)) + (subst-char-in-region (point-min) (point-max) ? ?_ t) + (buffer-string))) + +(defun erc-dcc-send-file (nick file &optional pproc) + "Open a socket for incoming connections, and send a CTCP send request to the +other client." + (interactive "sNick: \nfFile: ") + (when (null pproc) (if (processp erc-server-process) + (setq pproc erc-server-process) + (error "Can not find parent process"))) + (if (featurep 'make-network-process) + (let* ((buffer (erc-dcc-find-file file)) + (size (buffer-size buffer)) + (start (with-current-buffer buffer + (set-marker (make-marker) (point-min)))) + (sproc (erc-dcc-server "dcc-send" + 'erc-dcc-send-filter + 'erc-dcc-send-sentinel)) + (contact (process-contact sproc))) + (erc-dcc-list-add + 'SEND nick sproc pproc + :file file :size size + :sent start :confirmed (copy-marker start)) + (process-send-string + pproc (format "PRIVMSG %s :\C-aDCC SEND %s %s %d %d\C-a\n" + nick (erc-dcc-file-to-name file) + (erc-ip-to-decimal (nth 0 contact)) + (nth 1 contact) + size))) + (error "`make-network-process' not supported by your emacs."))) + +;;; GET handling + +(defvar erc-dcc-byte-count nil) +(make-variable-buffer-local 'erc-dcc-byte-count) + +(defun erc-dcc-get-file (entry file parent-proc) + "This function does the work of setting up a transfer from the remote client +to the local one over a tcp connection. This involves setting up a process +filter and a process sentinel, and making the connection." + (let* ((buffer (generate-new-buffer (file-name-nondirectory file))) + proc) + (with-current-buffer buffer + (fundamental-mode) + ;; This is necessary to have the buffer saved as-is in GNU + ;; Emacs. + ;; XEmacs change: We don't have `set-buffer-multibyte', setting + ;; coding system to 'binary below takes care of us. + (when (fboundp 'set-buffer-multibyte) + (set-buffer-multibyte nil)) + + (setq mode-line-process '(":%s") + buffer-file-type t + buffer-read-only t) + (set-visited-file-name file) + + (setq erc-server-process parent-proc + erc-dcc-entry-data entry) + (setq erc-dcc-byte-count 0) + (setq proc + (funcall erc-dcc-connect-function + "dcc-get" buffer + (plist-get entry :ip) + (string-to-number (plist-get entry :port)) + entry)) + (set-process-buffer proc buffer) + ;; The following two lines make saving as-is work under Windows + (set-process-coding-system proc 'binary 'binary) + (set-buffer-file-coding-system 'binary t) + + (set-process-filter proc 'erc-dcc-get-filter) + (set-process-sentinel proc 'erc-dcc-get-sentinel) + (setq entry (plist-put entry :start-time (erc-current-time))) + (setq entry (plist-put entry :peer proc))))) + +(defun erc-dcc-get-filter (proc str) + "This is the process filter for transfers from other clients to this one. +It reads incoming bytes from the network and stores them in the DCC +buffer, and sends back the replies after each block of data per the DCC +protocol spec. Well not really. We write back a reply after each read, +rather than every 1024 byte block, but nobody seems to care." + (with-current-buffer (process-buffer proc) + (setq buffer-read-only nil) ;; FIXME + (goto-char (point-max)) + (insert (string-make-unibyte str)) + + (setq erc-dcc-byte-count (+ (length str) erc-dcc-byte-count)) + (erc-assert (= erc-dcc-byte-count (1- (point-max)))) + (and erc-verbose-dcc + (erc-display-message + nil 'notice erc-server-process + 'dcc-get-bytes-received + ?f (file-name-nondirectory buffer-file-name) + ?b (number-to-string erc-dcc-byte-count))) + (cond + ((and (> (plist-get erc-dcc-entry-data :size) 0) + (> erc-dcc-byte-count (plist-get erc-dcc-entry-data :size))) + (erc-display-message + nil '(error notice) 'active + 'dcc-get-file-too-long + ?f (file-name-nondirectory buffer-file-name)) + (delete-process proc)) + (t + (process-send-string + proc (erc-pack-int erc-dcc-byte-count 4)))))) + + +(defun erc-dcc-get-sentinel (proc event) + "This is the process sentinel for CTCP DCC SEND connections. +It shuts down the connection and notifies the user that the +transfer is complete." + ;; FIXME, we should look at EVENT, and also check size. + (with-current-buffer (process-buffer proc) + (delete-process proc) + (setq buffer-read-only nil) + (setq erc-dcc-list (delete erc-dcc-entry-data erc-dcc-list)) + (erc-display-message + nil 'notice erc-server-process + 'dcc-get-complete + ?f (file-name-nondirectory buffer-file-name) + ?s (number-to-string (buffer-size)) + ?t (format "%.0f" + (erc-time-diff (plist-get erc-dcc-entry-data :start-time) + (erc-current-time)))) + (save-buffer)) + (kill-buffer (process-buffer proc)) + (delete-process proc)) + +;;; CHAT handling + +(defcustom erc-dcc-chat-buffer-name-format "DCC-CHAT-%s" + "*Format to use for DCC Chat buffer names." + :group 'erc-dcc + :type 'string) + +(defcustom erc-dcc-chat-mode-hook nil + "*Hook calls when `erc-dcc-chat-mode' finished setting up the buffer." + :group 'erc-dcc + :type 'hook) + +(defcustom erc-dcc-chat-connect-hook nil + "" + :group 'erc-dcc + :type 'hook) + +(defcustom erc-dcc-chat-exit-hook nil + "" + :group 'erc-dcc + :type 'hook) + +(defun erc-cmd-CREQ (line &optional force) + "Set or get the DCC chat request flag. +Possible values are: ask, auto, ignore." + (when (string-match "^\\s-*\\(auto\\|ask\\|ignore\\)?$" line) + (let ((cmd (match-string 1 line))) + (if (stringp cmd) + (erc-display-message + nil 'notice 'active + (format "Set DCC Chat requests to %S" + (setq erc-dcc-chat-request (intern cmd)))) + (erc-display-message nil 'notice 'active + (format "DCC Chat requests are set to %S" + erc-dcc-chat-request))) + t))) + +(defun erc-cmd-SREQ (line &optional force) + "Set or get the DCC send request flag. +Possible values are: ask, auto, ignore." + (when (string-match "^\\s-*\\(auto\\|ask\\|ignore\\)?$" line) + (let ((cmd (match-string 1 line))) + (if (stringp cmd) + (erc-display-message + nil 'notice 'active + (format "Set DCC Send requests to %S" + (setq erc-dcc-send-request (intern cmd)))) + (erc-display-message nil 'notice 'active + (format "DCC Send requests are set to %S" + erc-dcc-send-request))) + t))) + +(defun pcomplete/erc-mode/CREQ () + (pcomplete-here '("auto" "ask" "ignore"))) +(defalias 'pcomplete/erc-mode/SREQ 'pcomplete/erc-mode/CREQ) + +(defvar erc-dcc-chat-filter-hook '(erc-dcc-chat-parse-output) + "*Hook to run after doing parsing (and possible insertion) of DCC messages.") + +(defvar erc-dcc-chat-mode-map + (let ((map (make-sparse-keymap))) + (define-key map (kbd "RET") 'erc-send-current-line) + (define-key map "\t" 'erc-complete-word) + map) + "Keymap for `erc-dcc-mode'.") + +(defun erc-dcc-chat-mode () + "Major mode for wasting time via DCC chat." + (interactive) + (kill-all-local-variables) + (setq mode-line-process '(":%s") + mode-name "DCC-Chat" + major-mode 'erc-dcc-chat-mode + erc-send-input-line-function 'erc-dcc-chat-send-input-line + erc-default-recipients '(dcc)) + (use-local-map erc-dcc-chat-mode-map) + (run-hooks 'erc-dcc-chat-mode-hook)) + +(defun erc-dcc-chat-send-input-line (recipient line &optional force) + "Send LINE to the remote end. +Argument RECIPIENT should always be the symbol dcc, and force +is ignored." + ;; FIXME: We need to get rid of all force arguments one day! + (if (eq recipient 'dcc) + (process-send-string + (get-buffer-process (current-buffer)) line) + (error "erc-dcc-chat-send-input-line in %s" (current-buffer)))) + +(defun erc-dcc-chat (nick &optional pproc) + "Open a socket for incoming connections, and send a chat request to the +other client." + (interactive "sNick: ") + (when (null pproc) (if (processp erc-server-process) + (setq pproc erc-server-process) + (error "Can not find parent process"))) + (let* ((sproc (erc-dcc-server "dcc-chat-out" + 'erc-dcc-chat-filter + 'erc-dcc-chat-sentinel)) + (contact (process-contact sproc))) + (erc-dcc-list-add 'OCHAT nick sproc pproc) + (process-send-string pproc + (format "PRIVMSG %s :\C-aDCC CHAT chat %s %d\C-a\n" + nick + (erc-ip-to-decimal (nth 0 contact)) (nth 1 contact))))) + +(defvar erc-dcc-from) +(make-variable-buffer-local 'erc-dcc-from) + +(defvar erc-dcc-unprocessed-output) +(make-variable-buffer-local 'erc-dcc-unprocessed-output) + +(defun erc-dcc-chat-setup (entry) + "Setup a DCC chat buffer, returning the buffer." + (let* ((nick (erc-extract-nick (plist-get entry :nick))) + (buffer (generate-new-buffer + (format erc-dcc-chat-buffer-name-format nick))) + (proc (plist-get entry :peer)) + (parent-proc (plist-get entry :parent))) + (erc-setup-buffer buffer) + ;; buffer is now the current buffer. + (erc-dcc-chat-mode) + (setq erc-server-process parent-proc) + (setq erc-dcc-from nick) + (setq erc-dcc-entry-data entry) + (setq erc-dcc-unprocessed-output "") + (setq erc-insert-marker (set-marker (make-marker) (point-max))) + (erc-display-prompt buffer (point-max)) + (set-process-buffer proc buffer) + (add-hook 'kill-buffer-hook 'erc-dcc-chat-buffer-killed nil t) + (run-hook-with-args 'erc-dcc-chat-connect-hook proc) + buffer)) + +(defun erc-dcc-chat-accept (entry parent-proc) + "Accept an incoming DCC connection and open a DCC window" + (let* ((nick (erc-extract-nick (plist-get entry :nick))) + buffer proc) + (setq proc + (funcall erc-dcc-connect-function + "dcc-chat" nil + (plist-get entry :ip) + (string-to-number (plist-get entry :port)) + entry)) + ;; XXX: connected, should we kill the ip/port properties? + (setq entry (plist-put entry :peer proc)) + (setq entry (plist-put entry :parent parent-proc)) + (set-process-filter proc 'erc-dcc-chat-filter) + (set-process-sentinel proc 'erc-dcc-chat-sentinel) + (setq buffer (erc-dcc-chat-setup entry)))) + +(defun erc-dcc-chat-filter (proc str) + (let ((orig-buffer (current-buffer))) + (unwind-protect + (progn + (set-buffer (process-buffer proc)) + (setq erc-dcc-unprocessed-output + (concat erc-dcc-unprocessed-output str)) + (run-hook-with-args 'erc-dcc-chat-filter-hook proc + erc-dcc-unprocessed-output)) + (set-buffer orig-buffer)))) + +(defun erc-dcc-chat-parse-output (proc str) + (save-match-data + (let ((posn 0) + line) + (while (string-match "\n" str posn) + (setq line (substring str posn (match-beginning 0))) + (setq posn (match-end 0)) + (erc-display-message + nil nil proc + 'dcc-chat-privmsg ?n (erc-propertize erc-dcc-from 'face + 'erc-nick-default-face) ?m line)) + (setq erc-dcc-unprocessed-output (substring str posn))))) + +(defun erc-dcc-chat-buffer-killed () + (erc-dcc-chat-close "killed buffer")) + +(defun erc-dcc-chat-close (&optional event) + "Close a DCC chat, removing any associated processes and tidying up +`erc-dcc-list'" + (let ((proc (plist-get erc-dcc-entry-data :peer)) + (evt (or event ""))) + (when proc + (setq erc-dcc-list (delq erc-dcc-entry-data erc-dcc-list)) + (run-hook-with-args 'erc-dcc-chat-exit-hook proc) + (delete-process proc) + (erc-display-message + nil 'notice erc-server-process + 'dcc-chat-ended ?n erc-dcc-from ?t (current-time-string) ?e evt) + (setq erc-dcc-entry-data (plist-put erc-dcc-entry-data :peer nil))))) + +(defun erc-dcc-chat-sentinel (proc event) + (let ((buf (current-buffer)) + (elt (erc-dcc-member :peer proc))) + ;; the sentinel is also notified when the connection is opened, so don't + ;; immediately kill it again + ;(message "buf %s elt %S evt %S" buf elt event) + (unwind-protect + (if (string-match "^open from" event) + (erc-dcc-chat-setup elt) + (erc-dcc-chat-close event)) + (set-buffer buf)))) + +(defun erc-dcc-no-such-nick (proc parsed) + "Detect and handle no-such-nick replies from the IRC server." + (let* ((elt (erc-dcc-member :nick (second (erc-response.command-args parsed)) + :parent proc)) + (peer (plist-get elt :peer))) + (when (or (and (processp peer) (not (eq (process-status peer) 'open))) + elt) + ;; Since we already created an entry before sending the CTCP + ;; message, we now remove it, if it doesn't point to a process + ;; which is already open. + (setq erc-dcc-list (delq elt erc-dcc-list)) + (if (processp peer) (delete-process peer))) + nil)) + +(add-hook 'erc-server-401-functions 'erc-dcc-no-such-nick) + +(provide 'erc-dcc) + +;;; erc-dcc.el ends here +;; +;; Local Variables: +;; indent-tabs-mode: nil +;; End: + +;; arch-tag: cda5a6b3-c510-4dbe-b699-84cccfa04edb |