source: tags/0.8.0/protocol.lisp

Last change on this file was 170, checked in by Erik Huelsmann, 17 years ago

Refactor START-BACKGROUND-MESSAGE-HANDLER. Deprecate threading functions, because

  • It makes cl-irc less implementation dependent.
  • Users might want to use portability layers such as CLIM-SYS or BORDEAUX-THREADS
  • Our implementation doesn't implement error handling
  • Our implementation will never correctly handle errors from user-code (ie errors raised in event-handlers)
  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision URL
File size: 35.3 KB
Line 
1;;;; $Id: protocol.lisp 170 2007-01-06 13:31:05Z ehuelsmann $
2;;;; $URL: tags/0.8.0/protocol.lisp $
3
4;;;; See LICENSE for licensing information.
5
6(in-package :irc)
7
8;;
9;; Condition
10;;
11
12(define-condition no-such-reply ()
13  ((reply-number
14    :reader reply-number
15    :initarg :reply-number))
16  (:report (lambda (condition stream)
17             (format stream "No such reply ~A." (reply-number condition)))))
18
19
20
21;;
22;; Modes
23;;
24
25;; generic abstract mode class
26
27(defclass irc-mode ()
28  ((value
29    :initarg :value
30    :accessor value
31    :initform nil)
32   (value-type
33    :initarg :value-type
34    :accessor value-type
35    :documentation "The framework sets this to `:user' or `:non-user'.
36Essentially, if it's `:user', the value(s) held must be derived from the
37user class.")))
38
39(defgeneric set-mode-value (mode-object value))
40(defgeneric unset-mode-value (mode-object value))
41(defgeneric reset-mode-value (mode-object))
42(defgeneric has-value-p (mode-object value &key key test))
43
44(defmethod reset-mode-value ((mode irc-mode))
45  (setf (value mode) nil))
46
47
48;; mode class for holding boolean values
49
50(defclass boolean-value-mode (irc-mode) ())
51
52(defmethod set-mode-value ((mode boolean-value-mode) value)
53  (declare (ignore value))
54  (setf (value mode) t))
55
56(defmethod unset-mode-value ((mode boolean-value-mode) value)
57  (declare (ignore value))
58  (setf (value mode) nil))
59
60(defmethod has-value-p ((mode boolean-value-mode) value
61                        &key key test)
62  (declare (ignore value key test))
63  (value mode))
64
65;; mode class for holding single values
66
67(defclass single-value-mode (irc-mode) ())
68
69(defmethod set-mode-value ((mode single-value-mode) value)
70  (setf (value mode) value))
71
72(defmethod unset-mode-value ((mode single-value-mode) value)
73  (when (or (null value)
74            (equal value (value mode)))
75    (setf (value mode) nil)))
76
77(defmethod has-value-p ((mode single-value-mode) value
78                        &key (key #'identity) (test #'equal))
79  (funcall test
80           value
81           (funcall key (value mode))))
82
83
84;; mode class for holding lists of values
85
86(defclass list-value-mode (irc-mode) ())
87
88(defmethod set-mode-value ((mode list-value-mode) value)
89  (push value (value mode)))
90
91(defmethod unset-mode-value ((mode list-value-mode) value)
92  (setf (value mode)
93        (remove value (value mode))))
94
95(defmethod has-value-p ((mode list-value-mode) value
96                        &key (key #'identity) (test #'equal))
97  (let ((key-value (funcall key value)))
98    (some #'(lambda (x)
99              (funcall test
100                       key-value
101                       (funcall key x)))
102          (value mode))))
103
104;;
105;; Connection
106;;
107
108
109(defclass connection ()
110  ((user
111    :initarg :user
112    :accessor user)
113   (password
114    :initarg :password
115    :accessor password
116    :initform nil)
117   (server-name
118    :initarg :server-name
119    :accessor server-name
120    :initform "Unknown server")
121   (server-port
122    :initarg :server-port
123    :accessor server-port
124    :initform *default-irc-server-port*)
125   (socket
126    :initarg :socket
127    :documentation "Slot to store socket (for internal use only).")
128   (network-stream
129    :initarg :network-stream
130    :accessor network-stream
131    :documentation "Stream used to talk binary to the IRC server.")
132   (output-stream
133    :initarg :output-stream
134    :accessor output-stream
135    :documentation "Stream used to send messages to the IRC server")
136   (server-capabilities
137    :initform *default-isupport-values*
138    :accessor server-capabilities
139    :documentation "Assoc array for rpl_isupport message;
140see http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt")
141   (client-stream
142    :initarg :client-stream
143    :accessor client-stream
144    :initform t
145    :documentation "Messages coming back from the server is sent to
146this stream.")
147   (channels
148    :initarg :channels
149    :accessor channels
150    :initform (make-hash-table :test #'equal))
151   (hooks
152    :initarg :hooks
153    :accessor hooks
154    :initform (make-hash-table :test #'equal))
155   (channel-mode-descriptions
156    :initarg :channel-mode-descriptions
157    :accessor channel-mode-descriptions
158    :initform (chanmode-descs-from-isupport *default-isupport-values*)
159    :documentation
160    "Describes the modes an application intends to register with channels.")
161   (nick-prefixes
162    :initarg :nick-prefixes
163    :accessor nick-prefixes
164    :initform (nick-prefixes-from-isupport *default-isupport-values*))
165   (user-mode-destriptions
166    :initarg :user-mode-descriptions
167    :accessor user-mode-descriptions
168    :initform (mapcar #'(lambda (x)
169                          (make-mode-description :char (car x)
170                                                 :symbol (cdr x)))
171                      *char-to-user-modes-map*)
172    :documentation
173    "Describes the modes an application intends to register with channels.")
174   (users
175    :initarg :users
176    :accessor users
177    :initform (make-hash-table :test #'equal))))
178
179(defmethod print-object ((object connection) stream)
180  "Print the object for the Lisp reader."
181  (print-unreadable-object (object stream :type t :identity t)
182    (princ (server-name object) stream)))
183
184(defgeneric add-default-hooks (connection))
185(defgeneric client-raw-log (connection message))
186(defgeneric connectedp (connection))
187(defgeneric read-message (connection))
188(defgeneric read-irc-message (connection))
189(defgeneric send-irc-message (connection command &rest arguments))
190(defgeneric get-hooks (connection class))
191(defgeneric add-hook (connection class hook))
192(defgeneric remove-hook (connection class hook))
193(defgeneric remove-hooks (connection class))
194(defgeneric remove-all-hooks (connection))
195
196(defgeneric case-map-name (connection))
197(defgeneric re-apply-case-mapping (connection))
198
199(defun make-connection (&key (connection-type 'connection)
200                             (user nil)
201                             (password nil)
202                             (server-name "")
203                             (server-port nil)
204                             (socket nil)
205                             (network-stream nil)
206                             (outgoing-external-format *default-outgoing-external-format*)
207                             (client-stream t)
208                             (hooks nil))
209  (let* ((output-stream (flexi-streams:make-flexi-stream
210                         network-stream
211                         :element-type 'character
212                         :external-format (external-format-fixup outgoing-external-format)))
213         (connection (make-instance connection-type
214                                   :user user
215                                   :password password
216                                   :server-name server-name
217                                   :server-port server-port
218                                   :socket socket
219                                   :network-stream network-stream
220                                   :output-stream output-stream
221                                   :client-stream client-stream)))
222    (dolist (hook hooks)
223      (add-hook connection (car hook) (cadr hook)))
224    connection))
225
226(defmethod add-default-hooks ((connection connection))
227  (dolist (message '(irc-rpl_isupport-message
228                     irc-rpl_whoisuser-message
229                     irc-rpl_banlist-message
230                     irc-rpl_endofbanlist-message
231                     irc-rpl_exceptlist-message
232                     irc-rpl_endofexceptlist-message
233                     irc-rpl_invitelist-message
234                     irc-rpl_endofinvitelist-message
235                     irc-rpl_list-message
236                     irc-rpl_topic-message
237                     irc-rpl_namreply-message
238                     irc-rpl_endofnames-message
239                     irc-ping-message
240                     irc-join-message
241                     irc-topic-message
242                     irc-part-message
243                     irc-quit-message
244                     irc-kick-message
245                     irc-nick-message
246                     irc-mode-message
247                     irc-rpl_channelmodeis-message
248                     ctcp-time-message
249                     ctcp-source-message
250                     ctcp-finger-message
251                     ctcp-version-message
252                     ctcp-ping-message))
253      (add-hook connection message #'default-hook)))
254
255(defmethod client-raw-log ((connection connection) message)
256  (let ((stream (client-stream connection)))
257    (format stream (format nil "RAW LOG: ~A~%" message))
258    (force-output stream)))
259
260(defmethod connectedp ((connection connection))
261  "Returns t if `connection' is connected to a server and is ready for
262input."
263  (let ((stream (network-stream connection)))
264    (and (streamp stream)
265         (open-stream-p stream))))
266
267(defmethod read-message ((connection connection))
268  (when (connectedp connection)
269    (let ((message (read-irc-message connection)))
270      (when *debug-p*
271        (format *debug-stream* "~A" (describe message)))
272      (when message
273        (irc-message-event connection message))
274      message))) ; needed because of the "loop while" in read-message-loop
275
276(defvar *process-count* 0)
277
278(defun start-process (function name)
279  "Internal helper for the DEPRECATED function
280START-BACKGROUND-MESSAGE-HANDLER and therefore DEPRECATED itself."
281  (declare (ignorable name))
282  #+allegro (mp:process-run-function name function)
283  #+cmu (mp:make-process function :name name)
284  #+lispworks (mp:process-run-function name nil function)
285  #+sb-thread (sb-thread:make-thread function)
286  #+openmcl (ccl:process-run-function name function)
287  #+armedbear (ext:make-thread function))
288
289(defun start-background-message-handler (connection)
290  "Read messages from the `connection', parse them and dispatch
291irc-message-event on them. Returns background process ID if available.
292
293This function has been DEPRECATED.  The function body is meant as an
294example for library users on handling connection input.  Users
295are strongly encouraged to implement error handling (which is lacking
296from the prototype given here."
297  (warn "START-BACKGROUND-MESSAGE-HANDLER has been deprecated and
298is up for removal in a next release.")
299
300  #+(and sbcl (not sb-thread))
301  (flet ((select-handler (fd)
302            (declare (ignore fd))
303            (if (listen (network-stream connection))
304                (read-message connection)
305              ;; select() returns with no
306              ;; available data if the stream
307              ;; has been closed on the other
308              ;; end (EPIPE)
309              (sb-sys:invalidate-descriptor
310               (sb-sys:fd-stream-fd
311                (network-stream connection))))))
312    (sb-sys:add-fd-handler (sb-sys:fd-stream-fd
313                            (network-stream connection))
314                           :input #'select-handler))
315
316  #-(and sbcl (not sb-thread))
317  (flet ((do-loop () (read-message-loop connection)))
318    (let ((name (format nil "irc-hander-~D" (incf *process-count*))))
319      (start-process #'do-loop name))))
320
321(defun stop-background-message-handler (process)
322  "Stops a background message handler process returned by the start function.
323
324Just as its cousin START-BACKGROUND-MESSAGE-HANDLER,
325this function is DEPRECATED."
326  (declare (ignorable process))
327    #+cmu (mp:destroy-process process)
328    #+allegro (mp:process-kill process)
329    #+sb-thread (sb-thread:destroy-thread process)
330    #+lispworks (mp:process-kill process)
331    #+openmcl (ccl:process-kill process)
332    #+armedbear (ext:destroy-thread process))
333
334(defun read-message-loop (connection)
335  (loop while (read-message connection)))
336
337(defun try-decode-line (line external-formats)
338  (loop for external-format in external-formats
339        for decoded = nil
340        for error = nil
341        do (multiple-value-setq (decoded error)
342             (handler-case
343              (flexi-streams:with-input-from-sequence (in line)
344                (let ((flexi (flexi-streams:make-flexi-stream in
345;;                                                              :element-type 'character
346                                                              :external-format
347                                                              (external-format-fixup external-format))))
348                  (read-line flexi nil nil)))
349              (flexi-streams:flexi-stream-encoding-error ()
350                  nil)))
351        if decoded
352        do (return decoded)))
353
354(defmethod read-irc-message ((connection connection))
355  "Read and parse an IRC-message from the `connection'."
356  (handler-case
357   (multiple-value-bind
358       (buf buf-len)
359       ;; Note: we cannot use read-line here (or any other
360       ;; character based functions), since they may cause conversion
361       (read-sequence-until (network-stream connection)
362                            (make-array 1024
363                                        :element-type '(unsigned-byte 8)
364                                        :fill-pointer t)
365                            '(13 10))
366     (setf (fill-pointer buf) buf-len)
367     (let* ((message (create-irc-message (try-decode-line buf *default-incoming-external-formats*))))
368       (setf (connection message) connection)
369       message))
370    (end-of-file ())))
371       ;; satisfy read-message-loop assumption of nil when no more messages
372
373(defmethod send-irc-message ((connection connection) command
374                             &rest arguments)
375  "Turn the arguments into a valid IRC message and send it to the
376server, via the `connection'."
377  (let ((raw-message (apply #'make-irc-message command arguments)))
378    (write-sequence raw-message (output-stream connection))
379    (force-output (output-stream connection))
380    raw-message))
381
382(defmethod get-hooks ((connection connection) (class symbol))
383  "Return a list of all hooks for `class'."
384  (gethash class (hooks connection)))
385
386(defmethod add-hook ((connection connection) class hook)
387  "Add `hook' to `class'."
388  (setf (gethash class (hooks connection))
389        (pushnew hook (gethash class (hooks connection)))))
390
391(defmethod remove-hook ((connection connection) class hook)
392  "Remove `hook' from `class'."
393  (setf (gethash class (hooks connection))
394        (delete hook (gethash class (hooks connection)))))
395
396(defmethod remove-hooks ((connection connection) class)
397  "Remove all hooks for `class'."
398  (setf (gethash class (hooks connection)) nil))
399
400(defmethod remove-all-hooks ((connection connection))
401  (clrhash (hooks connection)))
402
403(defmethod case-map-name ((connection connection))
404  (let ((case-mapping (assoc "CASEMAPPING" (server-capabilities connection)
405                             :test #'equal)))
406    (intern (string-upcase (second case-mapping)) (find-package "KEYWORD"))))
407
408(defmethod re-apply-case-mapping ((connection connection))
409  (setf (normalized-nickname (user connection))
410        (normalize-nickname connection (nickname (user connection))))
411  (flet ((set-new-users-hash (object)
412           (let ((new-users (make-hash-table :test #'equal)))
413             (maphash
414              #'(lambda (norm-nick user)
415                  (declare (ignore norm-nick))
416                  (setf (gethash
417                         (setf (normalized-nickname user)
418                               (normalize-nickname connection
419                                                   (nickname user)))
420                         new-users) user))
421              (users object))
422             (setf (users object) new-users))))
423
424    (set-new-users-hash connection)
425    (let ((new-channels (make-hash-table :test #'equal)))
426      (maphash #'(lambda (norm-name channel)
427                   (declare (ignore norm-name))
428                   (setf (gethash
429                          (setf (normalized-name channel)
430                                (normalize-channel-name connection
431                                                        (name channel)))
432                          new-channels) channel)
433                   (set-new-users-hash channel))
434               (channels connection))
435      (setf (channels connection) new-channels))))
436
437
438;;
439;; DCC Connection
440;;
441
442(defclass dcc-connection ()
443  ((user
444    :initarg :user
445    :accessor user
446    :documentation "The user at the other end of this connection.  The
447user at this end can be reached via your normal connection object.")
448   (network-stream
449    :initarg :network-stream
450    :accessor network-stream)
451   (output-stream
452    :initarg :output-stream
453    :accessor output-stream
454    :initform t)))
455
456(defmethod print-object ((object dcc-connection) stream)
457  "Print the object for the Lisp reader."
458  (print-unreadable-object (object stream :type t :identity t)
459    (if (user object)
460        (format stream "with ~A@~A"
461                (nickname (user object))
462                (hostname (user object)))
463      "")))
464
465(defun make-dcc-connection (&key (user nil)
466                                 (remote-address nil)
467                                 (remote-port nil)
468                                 (output-stream t))
469  (make-instance 'dcc-connection
470                 :user user
471                 :network-stream (usocket:socket-connect remote-address
472                                                         remote-port)
473                 :output-stream output-stream))
474
475(defgeneric dcc-close (connection))
476(defgeneric send-dcc-message (connection message))
477
478(defmethod read-message ((connection dcc-connection))
479  (when (connectedp connection)
480    (let ((message (read-line (network-stream connection))))
481      (format (output-stream connection) "~A~%" message)
482      (force-output (output-stream connection))
483      (when *debug-p*
484        (format *debug-stream* "~A" (describe message)))
485      ;; (dcc-message-event message)
486      message))) ; needed because of the "loop while" in read-message-loop
487
488(defmethod send-dcc-message ((connection dcc-connection) message)
489  (format (network-stream connection) "~A~%" message)
490  (force-output (network-stream connection)))
491
492;; argh.  I want to name this quit but that gives me issues with
493;; generic functions.  need to resolve.
494(defmethod dcc-close ((connection dcc-connection))
495  #+(and sbcl (not sb-thread))
496  (sb-sys:invalidate-descriptor
497   (sb-sys:fd-stream-fd (network-stream connection)))
498  (close (network-stream connection))
499  (setf (user connection) nil)
500  (setf *dcc-connections* (remove connection *dcc-connections*))
501  )
502
503(defmethod connectedp ((connection dcc-connection))
504  (let ((stream (network-stream connection)))
505    (and (streamp stream)
506         (open-stream-p stream))))
507
508;;
509;; Channel
510;;
511
512(defclass channel ()
513  ((name
514    :initarg :name
515    :accessor name)
516   (normalized-name
517    :initarg :normalized-name
518    :accessor normalized-name)
519   (topic
520    :initarg :topic
521    :accessor topic)
522   (modes
523    :initarg :modes
524    :accessor modes
525    :initform '())
526   (visibility
527    :initarg :visibility
528    :accessor visibility
529    :initform nil
530    :type (member nil :public :private :secret :unknown))
531   (users
532    :initarg :users
533    :accessor users
534    :initform (make-hash-table :test #'equal))
535   (user-count
536    :initarg :user-count
537    :accessor user-count
538    :initform nil
539    :documentation "May not represent the real number of users in the
540channel.  Rather, the number returned from the LIST command gets stuck
541in there so the user of this library can use it for searching
542channels, for instance.  If the value is NIL then the slot has not
543been populated by a LIST command.")))
544
545(defmethod print-object ((object channel) stream)
546  "Print the object for the Lisp reader."
547  (print-unreadable-object (object stream :type t :identity t)
548    (princ (name object) stream)))
549
550(defun normalize-channel-name (connection string)
551  "Normalize `string' so that it represents an all-downcased channel
552name."
553  (irc-string-downcase (case-map-name connection) string))
554
555(defun make-channel (connection
556                     &key (name "")
557                          (topic "")
558                          (modes nil)
559                          (users nil)
560                          (user-count nil))
561  (let ((channel
562         (make-instance 'channel
563                        :name name
564                        :normalized-name
565                        (normalize-channel-name connection name)
566                        :topic topic
567                        :modes modes
568                        :user-count user-count)))
569    (dolist (user users)
570      (add-user channel user))
571    channel))
572
573(defgeneric find-channel (connection channel))
574(defgeneric remove-all-channels (connection))
575(defgeneric add-channel (connection channel))
576(defgeneric remove-channel (connection channel))
577(defgeneric remove-users (channel))
578
579(defgeneric mode-name-from-char (connection target mode-char)
580  (:documentation "Map the mode character used in the MODE message to a
581symbol used internally to describe the mode given a `target'."))
582
583(defgeneric mode-description (connection target mode-name)
584  (:documentation "Retrieve a `mode-description' structure for the given
585`mode-name' keyword."))
586
587(defgeneric get-mode (target mode)
588  (:documentation "Get the value associated with `mode' for `target'
589or `nil' if no mode available."))
590
591(defgeneric set-mode (target mode &optional parameter)
592  (:documentation "Set the mode designated by the `mode' keyword to a
593value passed in `parameter' or T if `parameter' is absent."))
594
595(defgeneric unset-mode (target mode &optional parameter)
596  (:documentation
597"Sets value of the mode designated by the `mode' keyword to nil.
598If the mode holds a list of values `parameter' is used to indicate which
599element to remove."))
600
601(defgeneric add-mode (target mode-name mode)
602  (:documentation "Add the mode-holding object `mode-value' to `target'
603under the access key `mode-name'.
604
605If mode-value is a subtype of irc-mode, it is added as-is.
606Otherwise, a mode-object will be generated from the "))
607(defgeneric remove-mode (target mode-name)
608  (:documentation "Remove the mode-holding object in the `mode-name' key
609from `target'."))
610
611(defgeneric has-mode-p (target mode)
612  (:documentation "Return a generalised boolean indicating if `target' has
613a mode `mode' associated with it."))
614
615(defgeneric has-mode-value-p (target mode value &key key test)
616  (:documentation "Return a generalised boolean indicating if `target' has
617a mode `mode' associated with the value `value' for given a `key' transform
618and `test' test."))
619
620(defmethod find-channel ((connection connection) (channel string))
621  "Return channel as designated by `channel'.  If no such channel can
622be found, return nil."
623  (let ((channel-name (normalize-channel-name connection channel)))
624    (gethash channel-name (channels connection))))
625
626(defmethod remove-all-channels ((connection connection))
627  "Remove all channels known to `connection'."
628  (clrhash (channels connection)))
629
630(defmethod add-channel ((connection connection) (channel channel))
631  "Add `channel' to `connection'."
632  (setf (gethash (normalized-name channel) (channels connection)) channel))
633
634(defmethod remove-channel ((connection connection) (channel channel))
635  "Remove `channel' from `connection'."
636  (remhash (normalized-name channel) (channels connection)))
637
638(defmethod remove-users ((channel channel))
639  "Remove all users on `channel'."
640  (clrhash (users channel))
641  (do-property-list (prop val (modes channel))
642     (when (and val (eq (value-type val) :user))
643       (remf (modes channel) prop))))
644
645(defmethod mode-name-from-char ((connection connection)
646                                (target channel) mode-char)
647  (declare (ignore target))
648  (let ((mode-desc (find mode-char (channel-mode-descriptions connection)
649                         :key #'mode-desc-char)))
650    (when mode-desc
651      (mode-desc-symbol (the mode-description mode-desc)))))
652
653(defmethod mode-description ((connection connection)
654                             (target channel) mode-name)
655  (declare (ignore target))
656  (find mode-name (channel-mode-descriptions connection)
657        :key #'mode-desc-symbol))
658
659(defgeneric make-mode (connection target mode-id))
660
661(defmethod make-mode (connection target (mode character))
662  (let ((mode-name (mode-name-from-char connection target mode)))
663    (make-mode connection target mode-name)))
664
665(defmethod make-mode (connection target (mode symbol))
666  (let ((mode-desc (mode-description connection target mode)))
667    (make-instance (mode-desc-class mode-desc)
668                   :value-type (if (mode-desc-nick-param-p mode-desc)
669                                   :user :non-user))))
670
671(defmethod add-mode (target mode-name mode)
672  (setf (getf (modes target) mode-name) mode))
673
674(defmethod remove-mode (target mode-name)
675  (remf (modes target) mode-name))
676
677(defmethod get-mode (target mode)
678  (let ((mode-object (has-mode-p target mode)))
679    (when mode-object
680      (value mode-object))))
681
682(defmethod set-mode (target mode &optional parameter)
683  (set-mode-value (getf (modes target) mode) parameter))
684
685(defmethod unset-mode (target mode &optional parameter)
686  (let ((mode (getf (modes target) mode)))
687    (when mode
688      (unset-mode-value mode parameter))))
689
690(defmethod has-mode-p (target mode)
691  (multiple-value-bind
692      (indicator value tail)
693      (get-properties (modes target) (list mode))
694    (when (or indicator value tail)
695      value)))
696
697(defmethod has-mode-value-p (target mode value
698                                    &key (key #'identity) (test #'equal))
699  (let ((mode (getf (modes target) mode)))
700    (when mode
701      (has-value-p mode value :key key :test test))))
702
703;;
704;; User
705;;
706
707(defclass user ()
708  ((nickname
709    :initarg :nickname
710    :accessor nickname
711    :initform "")
712   (normalized-nickname
713    :initarg :normalized-nickname
714    :accessor normalized-nickname
715    :initform "")
716   (username
717    :initarg :username
718    :accessor username
719    :initform "")
720   (hostname
721    :initarg :hostname
722    :accessor hostname
723    :initform "")
724   (realname
725    :initarg :realname
726    :accessor realname
727    :initform "")
728   (modes
729    :initarg :modes
730    :accessor modes
731    :initform '())
732   (channels
733    :initarg :channels
734    :accessor channels
735    :initform nil)))
736
737(defmethod print-object ((object user) stream)
738  "Print the object for the Lisp reader."
739  (print-unreadable-object (object stream :type t :identity t)
740    (format stream "~A!~A@~A \"~A\""
741            (nickname object)
742            (username object)
743            (hostname object)
744            (realname object))))
745
746(defun make-user (connection
747                  &key (nickname "")
748                       (username "")
749                       (hostname "")
750                       (realname ""))
751  (make-instance 'user
752                 :nickname nickname
753                 :normalized-nickname (normalize-nickname connection nickname)
754                 :username username
755                 :hostname hostname
756                 :realname realname))
757
758(defun canonicalize-nickname (connection nickname)
759  (if (find (char nickname 0)
760            (parse-isupport-prefix-argument
761             (second (assoc "PREFIX"
762                            (server-capabilities connection)
763                            :test #'string=))))
764      (substring nickname 1)
765      nickname))
766
767(defun normalize-nickname (connection string)
768  "Normalize `string' so that represents an all-downcased IRC
769nickname."
770  (irc-string-downcase (case-map-name connection) string))
771
772(defgeneric find-user (connection nickname))
773(defgeneric add-user (object user))
774(defgeneric remove-all-users (connection))
775(defgeneric remove-user (object user))
776(defgeneric remove-user-everywhere (connection user))
777(defgeneric find-or-make-user (connection nickname
778                                          &key username hostname realname))
779(defgeneric change-nickname (connection user new-nickname))
780
781(defmethod find-user ((connection connection) (nickname string))
782  "Return user as designated by `nickname' or nil if no such user is
783known."
784  (let ((nickname (normalize-nickname connection nickname)))
785    (or (gethash nickname (users connection))
786        (when (string= nickname (nickname (user connection)))
787          (user connection)))))
788
789; what if the user is not on any channels?
790(defmethod add-user ((connection connection) (user user))
791  "Add `user' to `connection'."
792  (setf (gethash (normalized-nickname user) (users connection)) user))
793
794(defmethod add-user ((channel channel) (user user))
795  (setf (gethash (normalized-nickname user) (users channel)) user)
796  (pushnew channel (channels user)))
797
798(defmethod remove-all-users ((connection connection))
799  "Remove all users known to `connection'."
800  (clrhash (users connection)))
801
802(defmethod remove-user ((channel channel) (user user))
803  "Remove `user' from `channel' and `channel' from `user'."
804  (remhash (normalized-nickname user) (users channel))
805  (setf (channels user) (remove channel (channels user)))
806  (do-property-list (prop val (modes channel))
807     (when (and val (eq (value-type val) :user))
808       (unset-mode channel prop user))))
809
810(defmethod remove-channel ((user user) (channel channel))
811  "Remove `channel' from `user'."
812  (setf (channels user) (remove channel (channels user))))
813
814(defmethod remove-user ((connection connection) (user user))
815  "Remove `user' from `connection' but leave user in any channels he
816may be already be on."
817  (remhash (normalized-nickname user) (users connection)))
818
819(defmethod remove-user-everywhere ((connection connection) (user user))
820  "Remove `user' anywhere present in the `connection'."
821  (dolist (channel (channels user))
822    (remove-user channel user))
823  (remove-user connection user))
824
825(defmethod mode-name-from-char ((connection connection)
826                                (target user) mode-char)
827  (declare (ignore target))
828  (let ((mode-desc (find mode-char (user-mode-descriptions connection)
829                         :key #'mode-desc-char)))
830    (when mode-desc
831      (mode-desc-symbol (the mode-description mode-desc)))))
832
833(defmethod mode-description ((connection connection)
834                             (target user) mode-name)
835  (declare (ignore target))
836  (find mode-name (user-mode-descriptions connection)
837        :key #'mode-desc-symbol))
838
839(defmethod find-or-make-user ((connection connection) nickname &key (username "")
840                              (hostname "") (realname ""))
841  (let ((user (find-user connection nickname)))
842    (unless user
843      (setf user
844            (make-user connection
845                       :nickname nickname
846                       :username username
847                       :hostname hostname
848                       :realname realname)))
849    (labels ((update-slot-if-known (slotname value)
850               (when (string= (slot-value user slotname) "")
851                 (setf (slot-value user slotname) value))))
852      (update-slot-if-known 'username username)
853      (update-slot-if-known 'hostname hostname)
854      (update-slot-if-known 'realname realname))
855    user))
856
857(defmethod change-nickname ((connection connection) (user user) new-nickname)
858  (let ((channels (channels user)))
859    (remove-user connection user)
860    (dolist (channel channels)
861      (remove-user channel user))
862    (setf (nickname user) new-nickname)
863    (setf (normalized-nickname user)
864          (normalize-nickname connection new-nickname))
865    (dolist (channel channels)
866      (add-user channel user))
867    (add-user connection user)
868    user))
869
870;; IRC Message
871;;
872
873(defclass irc-message ()
874  ((source
875    :accessor source
876    :initarg :source
877    :type string)
878   (user
879    :accessor user
880    :initarg :user)
881   (host
882    :accessor host
883    :initarg :host
884    :type string)
885   (command
886    :accessor command
887    :initarg :command
888    :type string)
889   (arguments
890    :accessor arguments
891    :initarg :arguments
892    :type list)
893   (connection
894    :accessor connection
895    :initarg :connection)
896   (received-time
897    :accessor received-time
898    :initarg :received-time)
899   (raw-message-string
900    :accessor raw-message-string
901    :initarg :raw-message-string
902    :type string)))
903
904(defmethod print-object ((object irc-message) stream)
905  "Print the object for the Lisp reader."
906  (print-unreadable-object (object stream :type t :identity t)
907    (format stream "~A ~A" (source object) (command object))))
908
909;;Compat code; remove after 2006-08-01
910
911(defgeneric trailing-argument (message))
912(defmethod trailing-argument ((message irc-message))
913  (warn "Use of deprecated function irc:trailing-argument")
914  (car (last (arguments message))))
915
916(defgeneric self-message-p (message))
917(defgeneric find-irc-message-class (type))
918(defgeneric client-log (connection message &optional prefix))
919(defgeneric apply-to-hooks (message))
920
921(defmethod self-message-p ((message irc-message))
922  "Did we send this message?"
923  (string-equal (source message)
924                (nickname (user (connection message)))))
925
926(defclass irc-error-reply (irc-message) ())
927
928(eval-when (:compile-toplevel :load-toplevel :execute)
929  (defun intern-message-symbol (prefix name)
930    "Intern based on symbol-name to support case-sensitive mlisp"
931    (intern
932     (concatenate 'string
933                  (symbol-name prefix)
934                  "-"
935                  (symbol-name name)
936                  "-"
937                  (symbol-name '#:message))))
938
939  (defun define-irc-message (command)
940    (let ((name (intern-message-symbol :irc command)))
941      `(progn
942        (defmethod find-irc-message-class ((type (eql ,command)))
943          (find-class ',name))
944        (export ',name)
945        (defclass ,name (irc-message) ())))))
946
947(defmacro create-irc-message-classes (class-list)
948  `(progn ,@(mapcar #'define-irc-message class-list)))
949
950;; should perhaps wrap this in an eval-when?
951(create-irc-message-classes #.(remove-duplicates (mapcar #'second *reply-names*)))
952(create-irc-message-classes (:privmsg :notice :kick :topic :error :mode :ping
953                             :nick :join :part :quit :kill :pong :invite))
954
955(defmethod find-irc-message-class (type)
956  (declare (ignore type))
957  (find-class 'irc-message))
958
959(defmethod client-log ((connection connection) (message irc-message) &optional (prefix ""))
960  (let ((stream (client-stream connection)))
961    (format stream "~A~A: ~A: ~A~{ ~A~} \"~A\"~%"
962            prefix
963            (received-time message)
964            (command message)
965            (source message)
966            (butlast (arguments message))
967            (car (last (arguments message))))
968    (force-output stream)))
969
970(defmethod apply-to-hooks ((message irc-message))
971  (let ((connection (connection message)))
972    (dolist (hook (get-hooks connection (class-name (class-of message))))
973      (funcall hook message))))
974
975;;
976;; CTCP Message
977;;
978
979(defclass ctcp-mixin ()
980  ((ctcp-command
981    :initarg :ctcp-command
982    :accessor ctcp-command)))
983
984(defclass standard-ctcp-message (ctcp-mixin irc-message) ())
985
986(defgeneric find-ctcp-message-class (type))
987(defgeneric ctcp-request-p (message))
988(defgeneric ctcp-reply-p (message))
989
990(eval-when (:compile-toplevel :load-toplevel :execute)
991  (defun define-ctcp-message (ctcp-command)
992    (let ((name (intern-message-symbol :ctcp ctcp-command)))
993      `(progn
994        (defmethod find-ctcp-message-class ((type (eql ,ctcp-command)))
995          (find-class ',name))
996        (export ',name)
997        (defclass ,name (ctcp-mixin irc-message) ())))))
998
999(defmacro create-ctcp-message-classes (class-list)
1000  `(progn ,@(mapcar #'define-ctcp-message class-list)))
1001
1002;; should perhaps wrap this in an eval-when?
1003(create-ctcp-message-classes (:action :source :finger :ping
1004                               :version :userinfo :time :dcc-chat-request
1005                               :dcc-send-request))
1006
1007(defmethod find-ctcp-message-class (type)
1008  (declare (ignore type))
1009  (find-class 'standard-ctcp-message))
1010
1011(defmethod ctcp-request-p ((message ctcp-mixin))
1012  (string= (command message) :privmsg))
1013
1014(defmethod ctcp-reply-p ((message ctcp-mixin))
1015  (string= (command message) :notice))
1016
1017(defmethod client-log ((connection connection) (message ctcp-mixin) &optional (prefix ""))
1018  (let ((stream (client-stream connection)))
1019    (format stream "~A~A: ~A (~A): ~A~{ ~A~} \"~A\"~%"
1020            prefix
1021            (received-time message)
1022            (command message)
1023            (ctcp-command message)
1024            (source message)
1025            (butlast (arguments message))
1026            (car (last (arguments message))))
1027    (force-output stream)))
1028
Note: See TracBrowser for help on using the repository browser.