source: tags/mode_tracking/protocol.lisp

Last change on this file was 88, checked in by Erik Huelsmann, 19 years ago

Implement MODE tracking.

  • TODO: Remove MODE updating item.
  • doc/user-guide.txt: Add section about modes.
  • package.lisp: Export new symbols.
  • variable.lisp (*default-isupport-CHANMODES*, *default-isupport-PREFIX*): New. (*default-isupport-values*): Add modes described by the RFC and declare them the default (minimal set). (mode-description): New. Structure to describe characteristics of mode arguments sent by the server. (*default-char-to-channel-modes-map*,

*char-to-user-modes-map*): New. Assoc lists to map characters to names.

  • utility.lisp (parse-isupport-prefix-argument): New. Returns the prefix and mode argument parts of the PREFIX RPL_ISUPPORT parameter. (nick-prefixes-from-isupport): New. Returns a plist associating mode prefixes with mode character designations. (chanmode-descs-from-isupport): New. Returns a list of mode-description structures for use with auto-creation of mode objects. (do-property-list): New. Macro to walk a property list like dolist. (parse-mode-arguments): New. Parses mode arguments given a connection object and target and translates those into a list of mode change instructions.
  • protocol.lisp (irc-mode): New. Abstract super class. Derivatives used to store mode values. (set-mode-value, unset-mode-value, reset-mode-value, has-value-p): New. Methods for irc-mode and its derivatives. (single-value-mode, list-value-mode): New. Classes implementing two types of value-holding mode-storage. (connection:channel-mode-descriptions): New. Slot in which the channel mode descriptions for the connection get stored. (connection:nick-prefixes): New. Slot which stores a plist associating RPL_NAMREPLY prefixes with mode characters. (connection:user-mode-descriptions): New. Slot which stores user modes like channel-mode-descriptions does for channels. (add-default-hooks): Add hook for irc-mode-message. (channel:modes): Change initialization to signal the value held will be of LIST type. (mode-name-from-char): New. Translates a mode character into an internal 'name': symbol. (mode-description): New. Retrieves a mode-description record from the given connection of a given mode name. (get-mode, set-mode, unset-mode, remove-mode): New. These provide operations on both channel and user modes. (has-mode-p, has-mode-value-p): New. This must be obvious. (remove-users): Also remove references to all users from any properties which carry the :user value-type. (make-mode): New. Automatically create mode object for the given mode to be added to the given target (user/channel). (user:modes): New. Slot to hold modes just like there is one on the channel class. (remove-user): Same as remove-user, but for the given user only.
  • event.lisp (default-hook [irc-rpl_isupport-message]): Set new channel-mode-descriptions and nick-prefixes slots. (default-hook [irc-rpl_namreply-message]): Set mode fields based on prefixes passed in the reply. (default-hook [irc-mode-message]): Set or unset channel and user modes upon reception of server notification.
  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 31.9 KB
Line 
1;;;; $Id: protocol.lisp 88 2005-03-20 16:55:43Z ehuelsmann $
2;;;; $Source$
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 single values
49
50(defclass single-value-mode (irc-mode) ())
51
52(defmethod set-mode-value ((mode single-value-mode) value)
53  (setf (value mode) (or value t)))
54
55(defmethod unset-mode-value ((mode single-value-mode) value)
56  (when (or (null value)
57            (equal value (value mode)))
58    (setf (value mode) nil)))
59
60(defmethod has-value-p ((mode single-value-mode) value
61                        &key (key #'identity) (test #'equal))
62  (funcall test
63           (funcall key (value mode))
64           (funcall key value)))
65
66
67;; mode class for holding lists of values
68
69(defclass list-value-mode (irc-mode) ())
70
71(defmethod set-mode-value ((mode list-value-mode) value)
72  (push value (value mode)))
73
74(defmethod unset-mode-value ((mode list-value-mode) value)
75  (setf (value mode)
76        (remove value (value mode))))
77
78(defmethod has-value-p ((mode list-value-mode) value
79                        &key (key #'identity) (test #'equal))
80  (let ((key-value (funcall key value)))
81    (some #'(lambda (x)
82              (funcall test
83                       key-value
84                       (funcall key x)))
85          (value mode))))
86
87;;
88;; Connection
89;;
90
91
92(defclass connection ()
93  ((user
94    :initarg :user
95    :accessor user)
96   (server-name
97    :initarg :server-name
98    :accessor server-name
99    :initform "Unknown server")
100   (server-stream
101    :initarg :server-stream
102    :accessor server-stream
103    :documentation "Stream used to talk to the IRC server.")
104   (server-capabilities
105    :initform *default-isupport-values*
106    :accessor server-capabilities
107    :documentation "Assoc array for rpl_isupport message;
108see http://www.irc.org/tech_docs/draft-brocklesby-irc-isupport-03.txt")
109   (client-stream
110    :initarg :client-stream
111    :accessor client-stream
112    :initform t
113    :documentation "Messages coming back from the server is sent to
114this stream.")
115   (channels
116    :initarg :channels
117    :accessor channels
118    :initform (make-hash-table :test #'equal))
119   (hooks
120    :initarg :hooks
121    :accessor hooks
122    :initform (make-hash-table :test #'equal))
123   (channel-mode-descriptions
124    :initarg :channel-mode-descriptions
125    :accessor channel-mode-descriptions
126    :initform (chanmode-descs-from-isupport *default-isupport-values*)
127    :documentation
128    "Describes the modes an application intends to register with channels.")
129   (nick-prefixes
130    :initarg :nick-prefixes
131    :accessor nick-prefixes
132    :initform (nick-prefixes-from-isupport *default-isupport-values*))
133   (user-mode-destriptions
134    :initarg :user-mode-descriptions
135    :accessor user-mode-descriptions
136    :initform (mapcar #'(lambda (x)
137                          (make-mode-description :char (car x)
138                                                 :symbol (cdr x)))
139                      *char-to-user-modes-map*)
140    :documentation
141    "Describes the modes an application intends to register with channels.")
142   (users
143    :initarg :users
144    :accessor users
145    :initform (make-hash-table :test #'equal))))
146
147(defmethod print-object ((object connection) stream)
148  "Print the object for the Lisp reader."
149  (print-unreadable-object (object stream :type t :identity t)
150    (princ (server-name object) stream)))
151
152(defgeneric add-default-hooks (connection))
153(defgeneric client-raw-log (connection message))
154(defgeneric connectedp (connection))
155(defgeneric read-message (connection))
156(defgeneric start-process (function name))
157(defgeneric start-background-message-handler (connection))
158(defgeneric read-message-loop (connection))
159(defgeneric read-irc-message (connection))
160(defgeneric send-irc-message (connection command
161                             &optional trailing-argument &rest arguments))
162(defgeneric get-hooks (connection class))
163(defgeneric add-hook (connection class hook))
164(defgeneric remove-hook (connection class hook))
165(defgeneric remove-hooks (connection class))
166(defgeneric remove-all-hooks (connection))
167
168(defgeneric case-map-name (connection))
169(defgeneric re-apply-case-mapping (connection))
170
171(defun make-connection (&key (user nil)
172                             (server-name "")
173                             (server-stream nil)
174                             (client-stream t)
175                             (hooks nil))
176  (let ((connection (make-instance 'connection
177                                   :user user
178                                   :server-name server-name
179                                   :server-stream server-stream
180                                   :client-stream client-stream)))
181    (dolist (hook hooks)
182      (add-hook connection (car hook) (cadr hook)))
183    connection))
184
185(defmethod add-default-hooks ((connection connection))
186  (dolist (message '(irc-rpl_isupport-message
187                     irc-rpl_whoisuser-message
188                     irc-rpl_list-message
189                     irc-rpl_topic-message
190                     irc-rpl_namreply-message
191                     irc-ping-message
192                     irc-join-message
193                     irc-topic-message
194                     irc-part-message
195                     irc-quit-message
196                     irc-kick-message
197                     irc-nick-message
198                     irc-mode-message
199                     ctcp-time-message
200                     ctcp-source-message
201                     ctcp-finger-message
202                     ctcp-version-message
203                     ctcp-ping-message))
204      (add-hook connection message #'default-hook)))
205
206(defmethod client-raw-log ((connection connection) message)
207  (let ((stream (client-stream connection)))
208    (format stream (format nil "RAW LOG: ~A~%" message))
209    (force-output stream)))
210
211(defmethod connectedp ((connection connection))
212  "Returns t if `connection' is connected to a server and is ready for
213input."
214  (let ((stream (server-stream connection)))
215    (and (streamp stream)
216         (open-stream-p stream))))
217
218(define-condition invalidate-me (condition)
219  ((stream :initarg :stream
220           :reader invalidate-me-stream)
221   (condition :initarg :condition
222              :reader invalidate-me-condition)))
223
224(defmethod read-message ((connection connection))
225  (let ((read-more-p t))
226    (handler-case
227        (progn
228          (when (and (connectedp connection) read-more-p)
229            (let ((message (read-irc-message connection)))
230              (when *debug-p*
231                (format *debug-stream* "~A" (describe message)))
232              (irc-message-event message)
233              message))) ; needed because of the "loop while" in read-message-loop
234        (stream-error (c) (setf read-more-p nil)
235                    (signal 'invalidate-me :stream
236                            (server-stream connection)
237                            :condition c)))))
238
239(defvar *process-count* 0)
240
241(defmethod start-process (function name)
242  #+allegro (mp:process-run-function name function)
243  #+cmu (mp:make-process function :name name)
244  #+lispworks (mp:process-run-function name nil function)
245  #+sb-thread (sb-thread:make-thread function)
246  #+openmcl (ccl:process-run-function name function)
247  #+armedbear (ext:make-thread function))
248
249(defmethod start-background-message-handler ((connection connection))
250  "Read messages from the `connection', parse them and dispatch
251irc-message-event on them. Returns background process ID if available."
252  (flet ((do-loop () (read-message-loop connection)))
253    (let ((name (format nil "irc-hander-~D" (incf *process-count*))))
254      #+(or allegro cmu lispworks sb-thread openmcl armedbear)
255      (start-process #'do-loop name)
256      #+(and sbcl (not sb-thread))
257      (sb-sys:add-fd-handler (sb-sys:fd-stream-fd
258                              (server-stream connection))
259                             :input (lambda (fd)
260                                      (declare (ignore fd))
261                                      (handler-case
262                                          (read-message connection)
263                                        (invalidate-me (c)
264                                          (sb-sys:invalidate-descriptor
265                                           (sb-sys:fd-stream-fd
266                                            (invalidate-me-stream c)))
267                                          (format t "Socket closed: ~A~%"
268                                                  (invalidate-me-condition c)))))))))
269
270(defun stop-background-message-handler (process)
271  "Stops a background message handler process returned by the start function."
272    #+cmu (mp:destroy-process process)
273    #+allegro (mp:process-kill process)
274    #+sb-thread (sb-thread:destroy-thread process)
275    #+lispworks (mp:process-kill process)
276    #+openmcl (ccl:process-kill process)
277    #+armedbear (ext:destroy-thread process))
278
279(defmethod read-message-loop ((connection connection))
280  (loop while (read-message connection)))
281
282(defmethod read-irc-message ((connection connection))
283  "Read and parse an IRC-message from the `connection'."
284  (let ((message (create-irc-message
285                  (read-line (server-stream connection) t))))
286    (setf (connection message) connection)
287    message))
288
289(defmethod send-irc-message ((connection connection) command
290                             &optional trailing-argument &rest arguments)
291  "Turn the arguments into a valid IRC message and send it to the
292server, via the `connection'."
293  (let ((raw-message (make-irc-message command
294                                       :arguments arguments
295                                       :trailing-argument trailing-argument)))
296    (write-sequence raw-message (server-stream connection))
297    (force-output (server-stream connection))
298    raw-message))
299
300(defmethod get-hooks ((connection connection) (class symbol))
301  "Return a list of all hooks for `class'."
302  (gethash class (hooks connection)))
303
304(defmethod add-hook ((connection connection) class hook)
305  "Add `hook' to `class'."
306  (setf (gethash class (hooks connection))
307        (pushnew hook (gethash class (hooks connection)))))
308
309(defmethod remove-hook ((connection connection) class hook)
310  "Remove `hook' from `class'."
311  (setf (gethash class (hooks connection))
312        (delete hook (gethash class (hooks connection)))))
313
314(defmethod remove-hooks ((connection connection) class)
315  "Remove all hooks for `class'."
316  (setf (gethash class (hooks connection)) nil))
317
318(defmethod remove-all-hooks ((connection connection))
319  (clrhash (hooks connection)))
320
321(defmethod case-map-name ((connection connection))
322  (let ((case-mapping (assoc "CASEMAPPING" (server-capabilities connection)
323                             :test #'equal)))
324    (intern (string-upcase (second case-mapping)) (find-package "KEYWORD"))))
325
326(defmethod re-apply-case-mapping ((connection connection))
327  (setf (normalized-nickname (user connection))
328        (normalize-nickname connection (nickname (user connection))))
329  (flet ((set-new-users-hash (object)
330           (let ((new-users (make-hash-table :test #'equal)))
331             (maphash
332              #'(lambda (norm-nick user)
333                  (declare (ignore norm-nick))
334                  (setf (gethash
335                         (setf (normalized-nickname user)
336                               (normalize-nickname connection
337                                                   (nickname user)))
338                         new-users) user))
339              (users object))
340             (setf (users object) new-users))))
341
342    (set-new-users-hash connection)
343    (let ((new-channels (make-hash-table :test #'equal)))
344      (maphash #'(lambda (norm-name channel)
345                   (declare (ignore norm-name))
346                   (setf (gethash
347                          (setf (normalized-name channel)
348                                (normalize-channel-name connection
349                                                        (name channel)))
350                          new-channels) channel)
351                   (set-new-users-hash channel))
352               (channels connection))
353      (setf (channels connection) new-channels))))
354
355
356;;
357;; DCC Connection
358;;
359
360(defclass dcc-connection ()
361  ((user
362    :initarg :user
363    :accessor user
364    :documentation "The user at the other end of this connection.  The
365user at this end can be reached via your normal connection object.")
366   (stream
367    :initarg :stream
368    :accessor dcc-stream)
369   (output-stream
370    :initarg :output-stream
371    :accessor output-stream
372    :initform t)
373   (socket
374    :initarg :socket
375    :accessor socket
376    :documentation "The actual socket object for the connection
377between the two users.")))
378
379(defmethod print-object ((object dcc-connection) stream)
380  "Print the object for the Lisp reader."
381  (print-unreadable-object (object stream :type t :identity t)
382    (if (user object)
383        (format stream "with ~A@~A"
384                (nickname (user object))
385                (hostname (user object)))
386      "")))
387
388(defun make-dcc-connection (&key (user nil)
389                                 (remote-address nil)
390                                 (remote-port nil)
391                                 (output-stream t))
392  #+sbcl
393  (let ((socket (sb-bsd-sockets:make-inet-socket :stream :tcp)))
394    (sb-bsd-sockets:socket-connect socket remote-address remote-port)
395    (make-instance 'dcc-connection
396                   :user user
397                   :stream (sb-bsd-sockets:socket-make-stream socket :input t :output t :buffering :none)
398                   :socket socket
399                   :output-stream output-stream))
400  #+openmcl
401  (let ((socket-stream (ccl:make-socket :remote-host remote-address
402                                        :remote-port remote-port)))
403    (make-instance 'dcc-connection
404                   :user user
405                   :stream socket-stream
406                   :output-stream output-stream))
407  #-(or openmcl sbcl)
408  (warn "make-dcc-connection not supported for this implementation."))
409
410(defgeneric dcc-close (connection))
411(defgeneric send-dcc-message (connection message))
412
413(defmethod read-message ((connection dcc-connection))
414  (let ((message (read-line (dcc-stream connection))))
415    (format (output-stream connection) "~A~%" message)
416    (force-output (output-stream connection))
417    message))
418
419(defmethod read-message-loop ((connection dcc-connection))
420  (loop while (read-message connection)))
421
422(defmethod send-dcc-message ((connection dcc-connection) message)
423  (format (dcc-stream connection) "~A~%" message))
424
425;; argh.  I want to name this quit but that gives me issues with
426;; generic functions.  need to resolve.
427(defmethod dcc-close ((connection dcc-connection))
428  (close (dcc-stream connection))
429  (setf (user connection) nil)
430  (setf *dcc-connections* (remove connection *dcc-connections*))
431  #+sbcl (sb-bsd-sockets:socket-close (socket connection))
432  )
433
434(defmethod connectedp ((connection dcc-connection))
435  (let ((stream (dcc-stream connection)))
436    (and (streamp stream)
437         (open-stream-p stream))))
438
439;;
440;; Channel
441;;
442
443(defclass channel ()
444  ((name
445    :initarg :name
446    :accessor name)
447   (normalized-name
448    :initarg :normalized-name
449    :accessor normalized-name)
450   (topic
451    :initarg :topic
452    :accessor topic)
453   (modes
454    :initarg :modes
455    :accessor modes
456    :initform '())
457   (users
458    :initarg :users
459    :accessor users
460    :initform (make-hash-table :test #'equal))
461   (user-count
462    :initarg :user-count
463    :accessor user-count
464    :initform nil
465    :documentation "May not represent the real number of users in the
466channel.  Rather, the number returned from the LIST command gets stuck
467in there so the user of this library can use it for searching
468channels, for instance.  If the value is NIL then the slot has not
469been populated by a LIST command.")))
470
471(defmethod print-object ((object channel) stream)
472  "Print the object for the Lisp reader."
473  (print-unreadable-object (object stream :type t :identity t)
474    (princ (name object) stream)))
475
476(defun normalize-channel-name (connection string)
477  "Normalize `string' so that it represents an all-downcased channel
478name."
479  (irc-string-downcase (case-map-name connection) string))
480
481(defun make-channel (connection
482                     &key (name "")
483                          (topic "")
484                          (modes nil)
485                          (users nil)
486                          (user-count nil))
487  (let ((channel
488         (make-instance 'channel
489                        :name name
490                        :normalized-name
491                        (normalize-channel-name connection name)
492                        :topic topic
493                        :modes modes
494                        :user-count user-count)))
495    (dolist (user users)
496      (add-user channel user))
497    channel))
498
499(defgeneric find-channel (connection channel))
500(defgeneric remove-all-channels (connection))
501(defgeneric add-channel (connection channel))
502(defgeneric remove-channel (connection channel))
503(defgeneric remove-users (channel))
504
505(defgeneric mode-name-from-char (connection target mode-char)
506  (:documentation "Map the mode character used in the MODE message to a
507symbol used internally to describe the mode given a `target'."))
508
509(defgeneric mode-description (connection target mode-name)
510  (:documentation "Retrieve a `mode-description' structure for the given
511`mode-name' keyword."))
512
513(defgeneric get-mode (target mode)
514  (:documentation "Get the value associated with `mode' for `target'
515or `nil' if no mode available."))
516
517(defgeneric set-mode (target mode &optional parameter)
518  (:documentation "Set the mode designated by the `mode' keyword to a
519value passed in `parameter' or T if `parameter' is absent."))
520
521(defgeneric unset-mode (target mode &optional parameter)
522  (:documentation
523"Sets value of the mode designated by the `mode' keyword to nil.
524If the mode holds a list of values `parameter' is used to indicate which
525element to remove."))
526
527(defgeneric add-mode (target mode-name mode)
528  (:documentation "Add the mode-holding object `mode-value' to `target'
529under the access key `mode-name'.
530
531If mode-value is a subtype of irc-mode, it is added as-is.
532Otherwise, a mode-object will be generated from the "))
533(defgeneric remove-mode (target mode-name)
534  (:documentation "Remove the mode-holding object in the `mode-name' key
535from `target'."))
536
537(defgeneric has-mode-p (target mode)
538  (:documentation "Return a generalised boolean indicating if `target' has
539a mode `mode' associated with it."))
540
541(defgeneric has-mode-value-p (target mode value &key key test)
542  (:documentation "Return a generalised boolean indicating if `target' has
543a mode `mode' associated with the value `value' for given a `key' transform
544and `test' test."))
545
546(defmethod find-channel ((connection connection) (channel string))
547  "Return channel as designated by `channel'.  If no such channel can
548be found, return nil."
549  (let ((channel-name (normalize-channel-name connection channel)))
550    (gethash channel-name (channels connection))))
551
552(defmethod remove-all-channels ((connection connection))
553  "Remove all channels known to `connection'."
554  (clrhash (channels connection)))
555
556(defmethod add-channel ((connection connection) (channel channel))
557  "Add `channel' to `connection'."
558  (setf (gethash (normalized-name channel) (channels connection)) channel))
559
560(defmethod remove-channel ((connection connection) (channel channel))
561  "Remove `channel' from `connection'."
562  (remhash (normalized-name channel) (channels connection)))
563
564(defmethod remove-users ((channel channel))
565  "Remove all users on `channel'."
566  (clrhash (users channel))
567  (do-property-list (prop val (modes channel))
568     (when (and val (eq (value-type val) :user))
569       (remf (modes channel) prop))))
570
571(defmethod mode-name-from-char ((connection connection)
572                                (target channel) mode-char)
573  (declare (ignore target))
574  (let ((mode-desc (find mode-char (channel-mode-descriptions connection)
575                         :key #'mode-desc-char)))
576    (when mode-desc
577      (mode-desc-symbol (the mode-description mode-desc)))))
578
579(defmethod mode-description ((connection connection)
580                             (target channel) mode-name)
581  (declare (ignore target))
582  (find mode-name (channel-mode-descriptions connection)
583        :key #'mode-desc-symbol))
584
585(defgeneric make-mode (connection target mode-id))
586
587(defmethod make-mode (connection target (mode character))
588  (let ((mode-name (mode-name-from-char connection target mode)))
589    (make-mode connection target mode-name)))
590
591(defmethod make-mode (connection target (mode symbol))
592  (let ((mode-desc (mode-description connection target mode)))
593    (make-instance (mode-desc-class mode-desc)
594                   :value-type (if (mode-desc-nick-param-p mode-desc)
595                                   :user :non-user))))
596
597(defmethod add-mode (target mode-name mode)
598  (setf (getf (modes target) mode-name) mode))
599
600(defmethod remove-mode (target mode-name)
601  (remf (modes target) mode-name))
602
603(defmethod get-mode (target mode)
604  (let ((mode-object (has-mode-p target mode)))
605    (when mode-object
606      (value mode-object))))
607
608(defmethod set-mode (target mode &optional parameter)
609  (set-mode-value (getf (modes target) mode) parameter))
610
611(defmethod unset-mode (target mode &optional parameter)
612  (let ((mode (getf (modes target) mode)))
613    (when mode
614      (unset-mode-value mode parameter))))
615
616(defmethod has-mode-p (target mode)
617  (multiple-value-bind
618      (indicator value tail)
619      (get-properties (modes target) (list mode))
620    (when (or indicator value tail)
621      value)))
622
623(defmethod has-mode-value-p (target mode value
624                                    &key (key #'identity) (test #'equal))
625  (let ((mode (getf (modes target) mode)))
626    (when mode
627      (has-value-p mode value :key key :test test))))
628
629;;
630;; User
631;;
632
633(defclass user ()
634  ((nickname
635    :initarg :nickname
636    :accessor nickname
637    :initform "")
638   (normalized-nickname
639    :initarg :normalized-nickname
640    :accessor normalized-nickname
641    :initform "")
642   (username
643    :initarg :username
644    :accessor username
645    :initform "")
646   (hostname
647    :initarg :hostname
648    :accessor hostname
649    :initform "")
650   (realname
651    :initarg :realname
652    :accessor realname
653    :initform "")
654   (modes
655    :initarg :modes
656    :accessor modes
657    :initform '())
658   (channels
659    :initarg :channels
660    :accessor channels
661    :initform nil)))
662
663(defmethod print-object ((object user) stream)
664  "Print the object for the Lisp reader."
665  (print-unreadable-object (object stream :type t :identity t)
666    (format stream "~A!~A@~A \"~A\""
667            (nickname object)
668            (username object)
669            (hostname object)
670            (realname object))))
671
672(defun make-user (connection
673                  &key (nickname "")
674                       (username "")
675                       (hostname "")
676                       (realname ""))
677  (make-instance 'user
678                 :nickname nickname
679                 :normalized-nickname (normalize-nickname connection nickname)
680                 :username username
681                 :hostname hostname
682                 :realname realname))
683
684(defun canonicalize-nickname (connection nickname)
685  (if (find (char nickname 0)
686            (parse-isupport-prefix-argument
687             (second (assoc "PREFIX"
688                            (server-capabilities connection)
689                            :test #'string=))))
690      (subseq nickname 1)
691      nickname))
692
693(defun normalize-nickname (connection string)
694  "Normalize `string' so that represents an all-downcased IRC
695nickname."
696  (irc-string-downcase (case-map-name connection) string))
697
698(defgeneric find-user (connection nickname))
699(defgeneric add-user (object user))
700(defgeneric remove-all-users (connection))
701(defgeneric remove-user (object user))
702(defgeneric remove-user-everywhere (connection user))
703(defgeneric find-or-make-user (connection nickname
704                                          &key username hostname realname))
705(defgeneric change-nickname (connection user new-nickname))
706
707(defmethod find-user ((connection connection) (nickname string))
708  "Return user as designated by `nickname' or nil if no such user is
709known."
710  (let ((nickname (normalize-nickname connection nickname)))
711    (or (gethash nickname (users connection))
712        (when (string= nickname (nickname (user connection)))
713          (user connection)))))
714
715; what if the user is not on any channels?
716(defmethod add-user ((connection connection) (user user))
717  "Add `user' to `connection'."
718  (setf (gethash (normalized-nickname user) (users connection)) user))
719
720(defmethod add-user ((channel channel) (user user))
721  (setf (gethash (normalized-nickname user) (users channel)) user)
722  (pushnew channel (channels user)))
723
724(defmethod remove-all-users ((connection connection))
725  "Remove all users known to `connection'."
726  (clrhash (users connection)))
727
728(defmethod remove-user ((channel channel) (user user))
729  "Remove `user' from `channel' and `channel' from `user'."
730  (remhash (normalized-nickname user) (users channel))
731  (setf (channels user) (remove channel (channels user)))
732  (do-property-list (prop val (modes channel))
733     (when (and val (eq (value-type val) :user))
734       (unset-mode channel prop user))))
735
736(defmethod remove-channel ((channel channel) (user user))
737  "Remove `channel' from `user'."
738  (warn
739   (concatenate 'string
740                "use of depricated API (remove-channel channel user): "
741                "(remove-channel user channel) is now preferred"))
742  (remove-channel user channel))
743
744(defmethod remove-channel ((user user) (channel channel))
745  "Remove `channel' from `user'."
746  (setf (channels user) (remove channel (channels user))))
747
748(defmethod remove-user ((connection connection) (user user))
749  "Remove `user' from `connection' but leave user in any channels he
750may be already be on."
751  (remhash (normalized-nickname user) (users connection)))
752
753(defmethod remove-user-everywhere ((connection connection) (user user))
754  "Remove `user' anywhere present in the `connection'."
755  (dolist (channel (channels user))
756    (remove-user channel user))
757  (remove-user connection user))
758
759(defmethod mode-name-from-char ((connection connection)
760                                (target user) mode-char)
761  (declare (ignore target))
762  (let ((mode-desc (find mode-char (user-mode-descriptions connection)
763                         :key #'mode-desc-char)))
764    (when mode-desc
765      (mode-desc-symbol (the mode-description mode-desc)))))
766
767(defmethod mode-description ((connection connection)
768                             (target user) mode-name)
769  (declare (ignore target))
770  (find mode-name (user-mode-descriptions connection)
771        :key #'mode-desc-symbol))
772
773(defmethod find-or-make-user ((connection connection) nickname &key (username "")
774                              (hostname "") (realname ""))
775  (or (find-user connection nickname)
776      (make-user connection
777                 :nickname nickname
778                 :username username
779                 :hostname hostname
780                 :realname realname)))
781
782(defmethod change-nickname ((connection connection) (user user) new-nickname)
783  (let ((new-user user)
784        (channels (channels user)))
785    (remove-user connection user)
786    (setf (nickname new-user) new-nickname)
787    (setf (normalized-nickname new-user)
788          (normalize-nickname connection new-nickname))
789    (dolist (channel channels)
790      (remove-user channel user)
791      (add-user channel new-user))
792    (add-user connection user)
793    new-user))
794
795;; IRC Message
796;;
797
798(defclass irc-message ()
799  ((source
800    :accessor source
801    :initarg :source
802    :type string)
803   (user
804    :accessor user
805    :initarg :user)
806   (host
807    :accessor host
808    :initarg :host
809    :type string)
810   (command
811    :accessor command
812    :initarg :command
813    :type string)
814   (arguments
815    :accessor arguments
816    :initarg :arguments
817    :type list)
818   (trailing-argument
819    :accessor trailing-argument
820    :initarg :trailing-argument
821    :type string)
822   (connection
823    :accessor connection
824    :initarg :connection)
825   (received-time
826    :accessor received-time
827    :initarg :received-time)
828   (raw-message-string
829    :accessor raw-message-string
830    :initarg :raw-message-string
831    :type string)))
832
833(defmethod print-object ((object irc-message) stream)
834  "Print the object for the Lisp reader."
835  (print-unreadable-object (object stream :type t :identity t)
836    (format stream "~A ~A" (source object) (command object))))
837
838(defgeneric self-message-p (message))
839(defgeneric find-irc-message-class (type))
840(defgeneric client-log (connection message &optional prefix))
841(defgeneric apply-to-hooks (message))
842
843(defmethod self-message-p ((message irc-message))
844  "Did we send this message?"
845  (string-equal (source message)
846                (nickname (user (connection message)))))
847
848(defclass irc-error-reply (irc-message) ())
849
850(eval-when (:compile-toplevel :load-toplevel :execute)
851  (defun intern-message-symbol (prefix name)
852    "Intern based on symbol-name to support case-sensitive mlisp"
853    (intern
854     (concatenate 'string
855                  (symbol-name prefix)
856                  "-"
857                  (symbol-name name)
858                  "-"
859                  (symbol-name '#:message))))
860
861  (defun define-irc-message (command)
862    (let ((name (intern-message-symbol :irc command)))
863      `(progn
864        (defmethod find-irc-message-class ((type (eql ,command)))
865          (find-class ',name))
866        (export ',name)
867        (defclass ,name (irc-message) ())))))
868
869(defmacro create-irc-message-classes (class-list)
870  `(progn ,@(mapcar #'define-irc-message class-list)))
871
872;; should perhaps wrap this in an eval-when?
873(create-irc-message-classes #.(remove-duplicates (mapcar #'second *reply-names*)))
874(create-irc-message-classes (:privmsg :notice :kick :topic :error :mode :ping
875                             :nick :join :part :quit :kill :pong :invite))
876
877(defmethod find-irc-message-class (type)
878  (declare (ignore type))
879  (find-class 'irc-message))
880
881(defmethod client-log ((connection connection) (message irc-message) &optional (prefix ""))
882  (let ((stream (client-stream connection)))
883    (format stream "~A~A: ~A: ~A~{ ~A~} \"~A\"~%"
884            prefix
885            (received-time message)
886            (command message)
887            (source message)
888            (arguments message)
889            (trailing-argument message))
890    (force-output stream)))
891
892(defmethod apply-to-hooks ((message irc-message))
893  (let ((connection (connection message)))
894    (dolist (hook (get-hooks connection (class-name (class-of message))))
895      (funcall hook message))))
896
897;;
898;; CTCP Message
899;;
900
901(defclass ctcp-mixin ()
902  ((ctcp-command
903    :initarg :ctcp-command
904    :accessor ctcp-command)))
905
906(defclass standard-ctcp-message (ctcp-mixin irc-message) ())
907
908(defgeneric find-ctcp-message-class (type))
909
910(eval-when (:compile-toplevel :load-toplevel :execute)
911  (defun define-ctcp-message (ctcp-command)
912    (let ((name (intern-message-symbol :ctcp ctcp-command)))
913      `(progn
914        (defmethod find-ctcp-message-class ((type (eql ,ctcp-command)))
915          (find-class ',name))
916        (export ',name)
917        (defclass ,name (ctcp-mixin irc-message) ())))))
918
919(defmacro create-ctcp-message-classes (class-list)
920  `(progn ,@(mapcar #'define-ctcp-message class-list)))
921
922;; should perhaps wrap this in an eval-when?
923(create-ctcp-message-classes (:action :source :finger :ping
924                               :version :userinfo :time :dcc-chat-request
925                               :dcc-send-request))
926
927(defmethod find-ctcp-message-class (type)
928  (declare (ignore type))
929  (find-class 'standard-ctcp-message))
930
931(defmethod client-log ((connection connection) (message ctcp-mixin) &optional (prefix ""))
932  (let ((stream (client-stream connection)))
933    (format stream "~A~A: ~A (~A): ~A~{ ~A~} \"~A\"~%"
934            prefix
935            (received-time message)
936            (command message)
937            (ctcp-command message)
938            (source message)
939            (arguments message)
940            (trailing-argument message))
941    (force-output stream)))
942
Note: See TracBrowser for help on using the repository browser.