1 | ;;;; $Id: specbot.lisp 211 2008-06-30 21:29:32Z ehuelsmann $ |
---|
2 | ;;;; $Source$ |
---|
3 | |
---|
4 | ;;;; specbot.lisp - an example IRC bot for cl-irc |
---|
5 | |
---|
6 | ;;; specbot is an example IRC bot for cl-irc. It runs on |
---|
7 | ;;; irc.freenode.net in the channels #lisp, #scheme and #clhs |
---|
8 | ;;; (preferred for testing). It responds to queries of its various |
---|
9 | ;;; databases, which right now include "clhs" and "r5rs". |
---|
10 | |
---|
11 | ;;; You will need to load and populate the tables for both the clhs |
---|
12 | ;;; and r5rs lookup packages; currently these are available in |
---|
13 | ;;; lisppaste CVS. |
---|
14 | |
---|
15 | ;;; To use it, load the cl-irc system, load specbot.lisp, and |
---|
16 | ;;; invoke (specbot:start-specbot "desirednickname" "desiredserver" |
---|
17 | ;;; "#channel1" "#channel2" "#channel3" ...) |
---|
18 | |
---|
19 | (defpackage :specbot (:use :common-lisp :irc) (:export :start-specbot |
---|
20 | :shut-up |
---|
21 | :un-shut-up)) |
---|
22 | (in-package :specbot) |
---|
23 | |
---|
24 | (defvar *connection*) |
---|
25 | (defvar *nickname* "") |
---|
26 | |
---|
27 | (defun shut-up () |
---|
28 | (setf (irc:client-stream *connection*) (make-broadcast-stream))) |
---|
29 | |
---|
30 | (defun un-shut-up () |
---|
31 | (setf (irc:client-stream *connection*) *trace-output*)) |
---|
32 | |
---|
33 | (defmacro aif (test conseq &optional (else nil)) |
---|
34 | `(let ((it ,test)) |
---|
35 | (if it ,conseq |
---|
36 | (symbol-macrolet ((it ,test)) |
---|
37 | ,else)))) |
---|
38 | |
---|
39 | (defun clhs-lookup (str) |
---|
40 | (and (find-package :clhs-lookup) |
---|
41 | (funcall (intern "SPEC-LOOKUP" :clhs-lookup) |
---|
42 | str))) |
---|
43 | |
---|
44 | (defun r5rs-lookup (str) |
---|
45 | (and (find-package :r5rs-lookup) |
---|
46 | (funcall (intern "SYMBOL-LOOKUP" :r5rs-lookup) |
---|
47 | str))) |
---|
48 | |
---|
49 | (defun cocoa-lookup (str) |
---|
50 | (and (find-package :cocoa-lookup) |
---|
51 | (funcall (intern "SYMBOL-LOOKUP" :cocoa-lookup) |
---|
52 | str))) |
---|
53 | |
---|
54 | (defun elisp-lookup (str) |
---|
55 | (and (find-package :elisp-lookup) |
---|
56 | (funcall (intern "SYMBOL-LOOKUP" :elisp-lookup) |
---|
57 | str))) |
---|
58 | |
---|
59 | (defun clim-lookup (str) |
---|
60 | (and (find-package :clim-lookup) |
---|
61 | (funcall (intern "TERM-LOOKUP" :clim-lookup) |
---|
62 | str))) |
---|
63 | |
---|
64 | (defvar *spec-providers* |
---|
65 | '((clhs-lookup "clhs" "The Common Lisp HyperSpec") |
---|
66 | (r5rs-lookup "r5rs" "The Revised 5th Ed. Report on the Algorithmic Language Scheme") |
---|
67 | (cocoa-lookup "cocoa" "Classes in the Cocoa Foundation and Application kits") |
---|
68 | (elisp-lookup "elisp" "GNU Emacs Lisp Reference Manual") |
---|
69 | (clim-lookup "clim" "Common Lisp Interface Manager II Specification"))) |
---|
70 | |
---|
71 | (defvar *spaces-allowed* |
---|
72 | '(clim-lookup)) |
---|
73 | |
---|
74 | (defvar *alists* nil) |
---|
75 | |
---|
76 | (defun add-simple-alist-lookup (file designator prefix description) |
---|
77 | (unless (assoc designator *alists*) |
---|
78 | (let ((alist (with-open-file (s file :direction :input) (read s)))) |
---|
79 | (push (cons designator alist) *alists*) |
---|
80 | (setf *spec-providers* |
---|
81 | (nconc *spec-providers* |
---|
82 | (list `((simple-alist-lookup ,designator) ,prefix ,description))))))) |
---|
83 | |
---|
84 | (defun simple-alist-lookup (designator string) |
---|
85 | (let ((alist (cdr (assoc designator *alists*)))) |
---|
86 | (cdr (assoc string alist :test #'equalp)))) |
---|
87 | |
---|
88 | (defun valid-message (string prefix &key space-allowed) |
---|
89 | (if (eql (search prefix string :test #'char-equal) 0) |
---|
90 | (and (or space-allowed |
---|
91 | (not (find #\space string :start (length prefix)))) |
---|
92 | (length prefix)) |
---|
93 | nil)) |
---|
94 | |
---|
95 | (defun strip-address (string &key (address *nickname*) (final nil)) |
---|
96 | (loop for i in (list (format nil "~A " address) |
---|
97 | (format nil "~A: " address) |
---|
98 | (format nil "~A:" address) |
---|
99 | (format nil "~A, " address)) |
---|
100 | do (aif (valid-message string i :space-allowed t) |
---|
101 | (return-from strip-address (subseq string it)))) |
---|
102 | (and (not final) string)) |
---|
103 | |
---|
104 | (defun msg-hook (message) |
---|
105 | (let ((destination (if (string-equal (first (arguments message)) *nickname*) |
---|
106 | (source message) |
---|
107 | (first (arguments message)))) |
---|
108 | (to-lookup (strip-address (car (last (arguments message)))))) |
---|
109 | (if (and (or |
---|
110 | (string-equal (first (arguments message)) *nickname*) |
---|
111 | (not (string= to-lookup (car (last (arguments message)))))) |
---|
112 | (member to-lookup '("help" "help?") :test #'string-equal)) |
---|
113 | (progn |
---|
114 | (privmsg *connection* destination |
---|
115 | (format nil "To use the ~A bot, say something like \"database term\", where database is one of (~{~S~^, ~}) and term is the desired lookup. The available databases are:" |
---|
116 | *nickname* |
---|
117 | (mapcar #'second *spec-providers*))) |
---|
118 | (loop for i from 1 for j in *spec-providers* |
---|
119 | with elts = nil |
---|
120 | do (push j elts) |
---|
121 | if (zerop (mod i 4)) |
---|
122 | do (progn |
---|
123 | (privmsg *connection* destination |
---|
124 | (format nil "~{~{~*~S, ~A~}~^; ~}" |
---|
125 | (nreverse elts))) |
---|
126 | (setf elts nil))) |
---|
127 | ) |
---|
128 | (loop for type in *spec-providers* |
---|
129 | for actual-fun = (if (typep (first type) 'symbol) |
---|
130 | (first type) |
---|
131 | (lambda (lookup) (destructuring-bind (fun first-arg) (first type) |
---|
132 | (funcall fun first-arg lookup)))) |
---|
133 | do |
---|
134 | (aif (strip-address to-lookup :address (second type) :final t) |
---|
135 | (let ((looked-up (funcall actual-fun it))) |
---|
136 | (if (and (<= 0 (count #\space it) |
---|
137 | (if (member actual-fun *spaces-allowed*) 1 0)1) |
---|
138 | (not looked-up)) |
---|
139 | (setf looked-up (format nil "Sorry, I couldn't find anything for ~A." it))) |
---|
140 | (and looked-up |
---|
141 | (privmsg *connection* destination looked-up)))))))) |
---|
142 | |
---|
143 | (defparameter *754-file* |
---|
144 | (merge-pathnames "754.lisp-expr" |
---|
145 | (make-pathname |
---|
146 | :directory |
---|
147 | (pathname-directory |
---|
148 | (or *load-truename* |
---|
149 | *default-pathname-defaults*))))) |
---|
150 | |
---|
151 | (defparameter *ppc-file* |
---|
152 | (merge-pathnames "ppc-assem.lisp-expr" |
---|
153 | (make-pathname |
---|
154 | :directory |
---|
155 | (pathname-directory |
---|
156 | (or *load-truename* |
---|
157 | *default-pathname-defaults*))))) |
---|
158 | |
---|
159 | (defparameter *sus-file* |
---|
160 | (merge-pathnames "sus.lisp-expr" |
---|
161 | (make-pathname |
---|
162 | :directory |
---|
163 | (pathname-directory |
---|
164 | (or *load-truename* |
---|
165 | *default-pathname-defaults*))))) |
---|
166 | |
---|
167 | (defparameter *man-file* |
---|
168 | (merge-pathnames "man.lisp-expr" |
---|
169 | (make-pathname |
---|
170 | :directory |
---|
171 | (pathname-directory |
---|
172 | (or *load-truename* |
---|
173 | *default-pathname-defaults*))))) |
---|
174 | |
---|
175 | (defun start-specbot (nick server &rest channels) |
---|
176 | (add-simple-alist-lookup *754-file* 'ieee754 "ieee754" "Section numbers of IEEE 754") |
---|
177 | (add-simple-alist-lookup *ppc-file* 'ppc "ppc" "PowerPC assembly mnemonics") |
---|
178 | (add-simple-alist-lookup *sus-file* 'sus "posix" "Single UNIX Specification") |
---|
179 | (add-simple-alist-lookup *man-file* 'man "man" "Mac OS X Man Pages") |
---|
180 | (setf *nickname* nick) |
---|
181 | (setf *connection* (connect :nickname *nickname* :server server)) |
---|
182 | (mapcar #'(lambda (channel) (join *connection* channel)) channels) |
---|
183 | (add-hook *connection* 'irc::irc-privmsg-message 'msg-hook) |
---|
184 | #+(or sbcl |
---|
185 | openmcl) |
---|
186 | (start-background-message-handler *connection*) |
---|
187 | #-(or sbcl |
---|
188 | openmcl) |
---|
189 | (read-message-loop *connection*)) |
---|
190 | |
---|
191 | (defun shuffle-hooks () |
---|
192 | (irc::remove-hooks *connection* 'irc::irc-privmsg-message) |
---|
193 | (add-hook *connection* 'irc::irc-privmsg-message 'msg-hook)) |
---|