source: tags/init/protocol.lisp

Last change on this file was 2, checked in by Erik Enge, 20 years ago

Initial revision

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 19.5 KB
Line 
1;;;; $Id: protocol.lisp 2 2004-01-05 14:13:03Z eenge $
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;; Connection
21;;
22
23
24(defclass connection ()
25  ((user
26    :initarg :user
27    :accessor user)
28   (server-name
29    :initarg :server-name
30    :accessor server-name
31    :initform "Unknown server")
32   (server-socket
33    :initarg :server-socket
34    :accessor server-socket
35    :documentation "Socket used to talk to the IRC server.")
36   (server-stream
37    :initarg :server-stream
38    :accessor server-stream
39    :documentation "Stream used to talk to the IRC server.")
40   (client-stream
41    :initarg :client-stream
42    :accessor client-stream
43    :initform t
44    :documentation "Messages coming back from the server is sent to
45this stream.")
46   (channels
47    :initarg :channels
48    :accessor channels
49    :initform (make-hash-table :test #'equal))
50   (hooks
51    :initarg :hooks
52    :accessor hooks
53    :initform (make-hash-table :test #'equal))
54   (users
55    :initarg :users
56    :accessor users
57    :initform (make-hash-table :test #'equal))))
58
59(defmethod print-object ((object connection) stream)
60  "Print the object for the Lisp reader."
61  (print-unreadable-object (object stream :type t :identity t)
62    (princ (server-name object) stream)))
63
64(defun make-connection (&key (user nil)
65                             (server-name "")
66                             (server-socket nil)
67                             (server-stream nil)
68                             (client-stream t)
69                             (hooks nil))
70  (let ((connection (make-instance 'connection
71                                   :user user
72                                   :server-name server-name
73                                   :server-socket server-socket
74                                   :server-stream server-stream
75                                   :client-stream client-stream)))
76    (dolist (hook hooks)
77      (add-hook connection (car hook) (cadr hook)))
78    connection))
79
80(defmethod add-default-hooks ((connection connection))
81  (dolist (message '(irc-rpl_whoisuser-message
82                     irc-rpl_list-message
83                     irc-rpl_topic-message
84                     irc-rpl_namreply-message
85                     irc-ping-message
86                     irc-join-message
87                     irc-topic-message
88                     irc-part-message
89                     irc-quit-message
90                     irc-kick-message
91                     irc-nick-message
92                     ctcp-time-message
93                     ctcp-source-message
94                     ctcp-finger-message
95                     ctcp-version-message
96                     ctcp-ping-message))
97      (add-hook connection message #'default-hook)))
98
99(defmethod client-raw-log ((connection connection) message)
100  (let ((stream (client-stream connection)))
101    (format stream (format nil "RAW LOG: ~A~%" message))
102    (force-output stream)))
103
104(defmethod connectedp ((connection connection))
105  "Returns t if `connection' is connected to a server and is ready for
106input."
107  (let ((stream (server-stream connection)))
108    (and (streamp stream)
109         (open-stream-p stream))))
110 
111(defmethod read-message ((connection connection))
112  (let ((read-more-p t))
113    (handler-case
114        (progn
115          (when (and (connectedp connection) read-more-p)
116            (let ((message (read-irc-message connection)))
117              (when *debug-p*
118                (format *debug-stream* "~A" (describe message)))
119              (irc-message-event message)
120              message))) ; needed because of the "loop while" in read-message-loop
121      (stream-error () (setf read-more-p nil)))))
122
123(defvar *process-count* 0)
124
125(defmethod start-process (function name)
126  #+allegro (mp:process-run-function name function)
127  #+cmu (mp:make-process function :name name)
128  #+lispworks (mp:process-run-function name nil function)
129  #+sb-thread (sb-thread:make-thread function))
130 
131(defmethod start-background-message-handler ((connection connection))
132  "Read messages from the `connection', parse them and dispatch
133irc-message-event on them. Returns background process ID if available."
134  (flet ((do-loop () (read-message-loop connection)))
135    (let ((name (format nil "irc-hander-~D" (incf *process-count*))))
136      #+(or allegro cmu lispworks sb-thread)
137      (start-process #'do-loop name)
138      #+(and sbcl (not sb-thread))
139      (sb-sys:add-fd-handler (sb-bsd-sockets:socket-file-descriptor
140                              (server-socket connection))
141                             :input (lambda (fd)
142                                      (declare (ignore fd))
143                                      (read-message connection))))))
144
145(defun stop-background-message-handler (process)
146  "Stops a background message handler process returned by the start function."
147    #+cmu (mp:destroy-process process)
148    #+allegro (mp:process-kill process)
149    #+sb-thread (sb-thread:destroy-thread process)
150    #+lispworks (mp:process-kill process))
151
152(defmethod read-message-loop ((connection connection))
153  (loop while (read-message connection)))
154
155(defmethod read-irc-message ((connection connection))
156  "Read and parse an IRC-message from the `connection'."
157  (let ((message (create-irc-message
158                  (read-line (server-stream connection) t))))
159    (setf (connection message) connection)
160    message))
161
162(defmethod send-irc-message ((connection connection) command
163                             &optional trailing-argument &rest arguments)
164  "Turn the arguments into a valid IRC message and send it to the
165server, via the `connection'."
166  (let ((raw-message (make-irc-message command
167                                       :arguments arguments
168                                       :trailing-argument trailing-argument)))
169    (write-sequence raw-message (server-stream connection))
170    (force-output (server-stream connection))
171    raw-message))
172
173(defmethod get-hooks ((connection connection) (class symbol))
174  "Return a list of all hooks for `class'."
175  (gethash class (hooks connection)))
176       
177(defmethod add-hook ((connection connection) class hook)
178  "Add `hook' to `class'."
179  (setf (gethash class (hooks connection))
180        (pushnew hook (gethash class (hooks connection)))))
181
182(defmethod remove-hook ((connection connection) class hook)
183  "Remove `hook' from `class'."
184  (setf (gethash class (hooks connection))
185        (delete hook (gethash class (hooks connection)))))
186
187(defmethod remove-hooks ((connection connection) class)
188  "Remove all hooks for `class'."
189  (setf (gethash class (hooks connection)) nil))
190
191(defmethod remove-all-hooks ((connection connection))
192  (clrhash (hooks connection)))
193
194;;
195;; DCC Connection
196;;
197
198(defclass dcc-connection ()
199  ((user
200    :initarg :user
201    :accessor user
202    :documentation "The user at the other end of this connection.  The
203user at this end can be reached via your normal connection object.")
204   (stream
205    :initarg :stream
206    :accessor dcc-stream)
207   (output-stream
208    :initarg :output-stream
209    :accessor output-stream
210    :initform t)
211   (socket
212    :initarg :socket
213    :accessor socket
214    :documentation "The actual socket object for the connection
215between the two users.")))
216
217(defmethod print-object ((object dcc-connection) stream)
218  "Print the object for the Lisp reader."
219  (print-unreadable-object (object stream :type t :identity t)
220    (if (user object)
221        (format stream "with ~A@~A"
222                (nickname (user object))
223                (hostname (user object)))
224               
225        "")))
226
227(defun make-dcc-connection (&key (user nil)
228                                 (remote-address nil)
229                                 (remote-port nil)
230                                 (output-stream t))
231  #+sbcl
232  (let ((socket (sb-bsd-sockets:make-inet-socket :stream :tcp)))
233    (sb-bsd-sockets:socket-connect socket remote-address remote-port)
234    (make-instance 'dcc-connection
235                   :user user
236                   :stream (sb-bsd-sockets:socket-make-stream socket :input t :output t :buffering :none)
237                   :socket socket
238                   :output-stream t))
239  #-sbcl
240  (warn "make-dcc-connection not supported for this implementation."))
241
242(defmethod read-message ((connection dcc-connection))
243  (let ((message (read-line (dcc-stream connection))))
244    (format (output-stream connection) "~A~%" message)
245    (force-output (output-stream connection))
246    message))
247
248(defmethod read-message-loop ((connection dcc-connection))
249  (loop while (read-message connection)))
250
251(defmethod send-dcc-message ((connection dcc-connection) message)
252  (format (dcc-stream connection) "~A~%" message))
253
254;; argh.  I want to name this quit but that gives me issues with
255;; generic functions.  need to resolve.
256(defmethod dcc-close ((connection dcc-connection))
257  (close (dcc-stream connection))
258  (setf (user connection) nil)
259  (setf *dcc-connections* (remove connection *dcc-connections*))
260  #+sbcl (sb-bsd-sockets:socket-close (socket connection))
261  )
262
263(defmethod connectedp ((connection dcc-connection))
264  (let ((stream (dcc-stream connection)))
265    (and (streamp stream)
266         (open-stream-p stream))))
267
268;;
269;; Channel
270;;
271
272(defclass channel ()
273  ((name
274    :initarg :name
275    :accessor name)
276   (normalized-name
277    :initarg :normalized-name
278    :accessor normalized-name)
279   (topic
280    :initarg :topic
281    :accessor topic)
282   (modes
283    :initarg :modes
284    :accessor modes
285    :initform nil)
286   (users
287    :initarg :users
288    :accessor users
289    :initform (make-hash-table :test #'equal))
290   (user-count
291    :initarg :user-count
292    :accessor user-count
293    :initform nil
294    :documentation "May not represent the real number of users in the
295channel.  Rather, the number returned from the LIST command gets stuck
296in there so the user of this library can use it for searching
297channels, for instance.  If the value is NIL then the slot has not
298been populated by a LIST command.")))
299   
300(defmethod print-object ((object channel) stream)
301  "Print the object for the Lisp reader."
302  (print-unreadable-object (object stream :type t :identity t)
303    (princ (name object) stream)))
304
305(defun normalize-channel-name (string)
306  "Normalize `string' so that it represents an all-downcased channel
307name."
308  (string-downcase string))
309 
310(defun make-channel (&key (name "")
311                          (topic "")
312                          (modes nil)
313                          (users nil)
314                          (user-count nil))
315  (let ((channel
316         (make-instance 'channel
317                        :name name
318                        :normalized-name (normalize-channel-name name)
319                        :topic topic
320                        :modes modes
321                        :user-count user-count)))
322    (dolist (user users)
323      (add-user channel user))
324    channel))
325
326(defmethod find-channel ((connection connection) (channel string))
327  "Return channel as designated by `channel'.  If no such channel can
328be found, return nil."
329  (let ((channel-name (normalize-channel-name channel)))
330    (gethash channel-name (channels connection))))
331
332(defmethod remove-all-channels ((connection connection))
333  "Remove all channels known to `connection'."
334  (clrhash (channels connection)))
335
336(defmethod add-channel ((connection connection) (channel channel))
337  "Add `channel' to `connection'."
338  (setf (gethash (normalized-name channel) (channels connection)) channel))
339
340(defmethod remove-channel ((connection connection) (channel channel))
341  "Remove `channel' from `connection'."
342  (remhash (normalized-name channel) (channels connection)))
343
344(defmethod remove-users ((channel channel))
345  "Remove all users on `channel'."
346  (clrhash (users channel)))
347
348;;
349;; User
350;;
351
352(defclass user ()
353  ((nickname
354    :initarg :nickname
355    :accessor nickname
356    :initform "")
357   (normalized-nickname
358    :initarg :normalized-nickname
359    :accessor normalized-nickname
360    :initform "")
361   (username
362    :initarg :username
363    :accessor username
364    :initform "")
365   (hostname
366    :initarg :hostname
367    :accessor hostname
368    :initform "")
369   (realname
370    :initarg :realname
371    :accessor realname
372    :initform "")
373   (channels
374    :initarg :channels
375    :accessor channels
376    :initform nil)))
377
378(defmethod print-object ((object user) stream)
379  "Print the object for the Lisp reader."
380  (print-unreadable-object (object stream :type t :identity t)
381    (format stream "~A!~A@~A \"~A\""
382            (nickname object)
383            (username object)
384            (hostname object)
385            (realname object))))
386
387(defun make-user (&key (nickname "")
388                       (username "")
389                       (hostname "")
390                       (realname ""))
391  (make-instance 'user
392                 :nickname nickname
393                 :normalized-nickname (normalize-nickname nickname)
394                 :username username
395                 :hostname hostname
396                 :realname realname))
397
398(defun canonicalize-nickname (nickname)
399  (if (find (char nickname 0) "@+*")
400      (subseq nickname 1)
401      nickname))
402
403(defun normalize-nickname (string)
404  "Normalize `string' so that represents an all-downcased IRC
405nickname."
406  (let* ((new-string (substitute #\[ #\{ string))
407         (new-string (substitute #\] #\} new-string))
408         (new-string (substitute #\\ #\| new-string))
409         (new-string (substitute #\~ #\^ new-string)))
410    (string-downcase new-string)))
411
412(defmethod find-user ((connection connection) (nickname string))
413  "Return user as designated by `nickname' or nil if no such user is
414known."
415  (let ((nickname (normalize-nickname nickname)))
416    (or (gethash nickname (users connection))
417        (when (string= nickname (nickname (user connection)))
418          (user connection)))))
419
420; what if the user is not on any channels?
421(defmethod add-user ((connection connection) (user user))
422  "Add `user' to `connection'."
423  (setf (gethash (normalized-nickname user) (users connection)) user))
424
425(defmethod add-user ((channel channel) (user user))
426  (setf (gethash (normalized-nickname user) (users channel)) user)
427  (pushnew channel (channels user)))
428
429(defmethod remove-all-users ((connection connection))
430  "Remove all users known to `connection'."
431  (clrhash (users connection)))
432
433(defmethod remove-user ((channel channel) (user user))
434  "Remove `user' from `channel' and `channel' from `user'."
435  (remhash (normalized-nickname user) (users channel))
436  (setf (channels user) (remove channel (channels user))))
437
438(defmethod remove-channel ((channel channel) (user user))
439  "Remove `channel' from `user'."
440  (setf (channels user) (remove channel (channels user))))
441
442(defmethod remove-user ((connection connection) (user user))
443  "Remove `user' from `connection' but leave user in any channels he
444may be already be on."
445  (remhash (normalized-nickname user) (users connection)))
446
447(defmethod remove-user-everywhere ((connection connection) (user user))
448  "Remove `user' anywhere present in the `connection'."
449  (dolist (channel (channels user))
450    (remove-user channel user))
451  (remove-user connection user))
452
453(defmethod find-or-make-user ((connection connection) nickname &key (username "")
454                              (hostname "") (realname ""))
455  (or (find-user connection nickname)
456      (make-user :nickname nickname
457                 :username username
458                 :hostname hostname
459                 :realname realname)))
460
461(defmethod change-nickname ((connection connection) (user user) new-nickname)
462  (let ((new-user user)
463        (channels (channels user)))
464    (remove-user connection user)
465    (setf (nickname new-user) new-nickname)
466    (setf (normalized-nickname new-user) (normalize-nickname new-nickname))
467    (dolist (channel channels)
468      (remove-user channel user)
469      (add-user channel new-user))
470    (add-user connection user)
471    new-user))
472
473;; IRC Message
474;;
475
476(defclass irc-message ()
477  ((source
478    :accessor source
479    :initarg :source
480    :type string)
481   (user
482    :accessor user
483    :initarg :user)
484   (host
485    :accessor host
486    :initarg :host
487    :type string)
488   (command
489    :accessor command
490    :initarg :command
491    :type string)
492   (arguments
493    :accessor arguments
494    :initarg :arguments
495    :type list)
496   (trailing-argument
497    :accessor trailing-argument
498    :initarg :trailing-argument
499    :type string)
500   (connection
501    :accessor connection
502    :initarg :connection)
503   (received-time
504    :accessor received-time
505    :initarg :received-time)
506   (raw-message-string
507    :accessor raw-message-string
508    :initarg :raw-message-string
509    :type string)))
510
511(defmethod print-object ((object irc-message) stream)
512  "Print the object for the Lisp reader."
513  (print-unreadable-object (object stream :type t :identity t)
514    (format stream "~A ~A" (source object) (command object))))
515
516(defmethod self-message-p ((message irc-message))
517  "Did we send this message?"
518  (string-equal (source message)
519                (nickname (user (connection message)))))
520
521(defclass irc-error-reply (irc-message) ())
522
523(defmacro define-irc-message (command)
524  (let ((*print-case* :upcase))
525    (let ((name (intern (format nil "IRC-~A-MESSAGE" command))))
526      `(progn
527         (defmethod find-irc-message-class ((type (eql ,command)))
528           (find-class ',name))
529         (export ',name)
530         (defclass ,name (irc-message) ())))))
531
532(defun create-irc-message-classes (class-list)
533  (dolist (class class-list)
534    (eval (list 'define-irc-message class)))) ; argh.  eval.
535
536;; should perhaps wrap this in an eval-when?
537(create-irc-message-classes (mapcar #'second *reply-names*))
538(create-irc-message-classes '(:privmsg :notice :kick :topic :error
539                              :mode :ping :nick :join :part :quit :kill
540                              :pong :invite))
541
542(defmethod find-irc-message-class (type)
543  (find-class 'irc-message))
544
545(defmethod client-log ((connection connection) (message irc-message) &optional (prefix ""))
546  (let ((stream (client-stream connection)))
547    (format stream "~A~A: ~A: ~A~{ ~A~} \"~A\"~%"
548            prefix
549            (received-time message)
550            (command message)
551            (source message)
552            (arguments message)
553            (trailing-argument message))
554    (force-output stream)))
555
556(defmethod apply-to-hooks ((message irc-message))
557  (let ((connection (connection message)))
558    (dolist (hook (get-hooks connection (class-name (class-of message))))
559      (funcall hook message))))
560
561;;
562;; CTCP Message
563;;
564
565(defclass ctcp-mixin ()
566  ((ctcp-command
567    :initarg :ctcp-command
568    :accessor ctcp-command)))
569
570(defclass standard-ctcp-message (ctcp-mixin message) ())
571
572(defmacro define-ctcp-message (ctcp-command)
573  (let ((*print-case* :upcase))
574    (let ((name (intern (format nil "CTCP-~A-MESSAGE" ctcp-command))))
575      `(progn
576         (defmethod find-ctcp-message-class ((type (eql ,ctcp-command)))
577           (find-class ',name))
578         (export ',name)
579         (defclass ,name (ctcp-mixin irc-message) ())))))
580
581(defun create-ctcp-message-classes (class-list)
582  (dolist (class class-list)
583    (eval (list 'define-ctcp-message class)))) ; argh.  eval.  must go away.
584
585;; should perhaps wrap this in an eval-when?
586(create-ctcp-message-classes '(:action :source :finger :ping
587                               :version :userinfo :time :dcc-chat-request
588                               :dcc-send-request))
589
590(defmethod find-ctcp-message-class (type)
591  (find-class 'standard-ctcp-message))
592
593(defmethod client-log ((connection connection) (message ctcp-mixin) &optional (prefix ""))
594  (let ((stream (client-stream connection)))
595    (format stream "~A~A: ~A (~A): ~A~{ ~A~} \"~A\"~%"
596            prefix
597            (received-time message)
598            (command message)
599            (ctcp-command message)
600            (source message)
601            (arguments message)
602            (trailing-argument message))
603    (force-output stream)))
604
Note: See TracBrowser for help on using the repository browser.