source: tags/init/example/cliki.lisp

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

Initial revision

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 8.4 KB
Line 
1;;;; $Id: cliki.lisp 2 2004-01-05 14:13:03Z eenge $
2;;;; $Source$
3
4;;;; cliki.lisp - CLiki as an infobot; only works on SBCL.
5
6;;; To use it, load the net-nittin-irc and cl-ppcre systems, load
7;;; cliki.lisp, and invoke (cliki::start-cliki-bot "desirednickname"
8;;; "desiredserver" "#channel1" "#channel2" "#channel3" ...)
9
10(defpackage :cliki (:use :common-lisp :irc :sb-bsd-sockets :cl-ppcre))
11(in-package :cliki)
12
13(defvar *small-definitions* nil)
14
15(defun read-small-definitions ()
16  (setf *small-definitions* nil)
17  (with-open-file (sd-file "sd.lisp-expr" :direction :input :if-does-not-exist nil)
18    (when sd-file
19      (block nil
20        (loop (let ((defn (read sd-file nil)))
21                (if defn (push defn *small-definitions*)
22                    (return (setf *small-definitions* (nreverse *small-definitions*))))))))))
23
24(defun write-small-definitions ()
25  (with-open-file (sd-file "sd.lisp-expr" :direction :output :if-exists :supersede)
26    (mapc #'(lambda (defn)
27              (prin1 defn sd-file)
28              (format sd-file "~%")) *small-definitions*)))
29
30(defun write-top-definition ()
31  (with-open-file (sd-file "sd.lisp-expr" :direction :output :if-exists :append)
32    (prin1 (car *small-definitions*) sd-file)
33    (format sd-file "~%")))
34
35(defun add-small-definition (term defn)
36  (push (cons term defn) *small-definitions*)
37  (write-small-definitions))
38
39(defun url-port (url)
40  (assert (string-equal url "http://" :end1 7))
41  (let ((port-start (position #\: url :start 7)))
42    (if port-start (parse-integer url :start (1+ port-start) :junk-allowed t) 80)))
43
44(defun url-host (url)
45  (assert (string-equal url "http://" :end1 7))
46  (let* ((port-start (position #\: url :start 7))
47         (host-end (min (or (position #\/ url :start 7) (length url))
48                        (or port-start (length url)))))
49    (subseq url 7 host-end)))
50
51(defun url-connection (url)
52  (let ((s (make-instance 'inet-socket :type :stream :protocol :tcp))
53        (host (url-host url))
54        (port (url-port url)))
55    (declare (ignore port))
56    (socket-connect
57     s (car (host-ent-addresses (get-host-by-name (url-host url))))
58     (url-port url))
59    (let ((stream (socket-make-stream s :input t :output t :buffering :full)))
60      ;; we are exceedingly unportable about proper line-endings here.
61      ;; Anyone wishing to run this under non-SBCL should take especial care
62      (format stream "GET ~A HTTP/1.0~%Host: ~A~%User-Agent: CLiki Bot~%~%" url host)
63      (force-output stream)
64      (list
65       (let* ((l (read-line stream))
66              (space (position #\Space l)))
67         (parse-integer l :start (1+ space) :junk-allowed t))
68       (loop for line = (read-line stream nil nil)
69             until (or (null line) (eql (elt line 0) (code-char 13)))
70             collect
71             (let ((colon (position #\: line)))
72               (cons (intern (string-upcase (subseq line 0 colon)) :keyword)
73                     (string-trim (list #\Space (code-char 13))
74                                  (subseq line (1+ colon))))))
75       stream))))
76
77(defun encode-for-url (str)
78  (setf str (regex-replace-all " " str "%20"))
79  (setf str (regex-replace-all "," str "%2C"))
80  (setf str (regex-replace-all "`" str "%60"))
81  ;(format t "hi ~A~%" str)
82  str)
83
84(defun cliki-first-sentence (term)
85  (let* ((cliki-url (format nil "http://www.cliki.net/~A"
86                     (encode-for-url term)))
87         (url (concatenate 'string cliki-url "?source")))
88    (block cliki-return
89      (handler-case
90          (sb-ext:with-timeout 5
91            (destructuring-bind (response headers stream)
92                (block got
93                  (loop
94                     (destructuring-bind (response headers stream) (url-connection url)
95                       (unless (member response '(301 302))           
96                         (return-from got (list response headers stream)))
97                       (close stream)
98                       (setf url (cdr (assoc :location headers))))))
99              (unwind-protect
100                   (if (not (eql response 200))
101                       (format nil "The term ~A was not found in CLiki." term)
102                       (let ((first-line ""))
103                         (loop for i from 1 to 5 do ;; scan the first 5 lines
104                          (progn
105                            (multiple-value-bind (next-line missing-newline-p)
106                                (read-line stream nil)
107                              (if next-line
108                                  (setf first-line (concatenate 'string first-line next-line (string #\newline)))
109                                  (return-from cliki-return (format nil "The end of the page was reached before a definition was found in ~A" cliki-url))))
110                            (setf first-line (regex-replace-all "\\r" first-line " "))
111                            (setf first-line (regex-replace-all "\\n" first-line " "))
112                            (setf first-line (regex-replace-all "_\\(([^)]*)\\)" first-line "\\1"))
113                            (setf first-line (regex-replace-all "\\*\\(([^)]*)\\)" first-line "\\1"))
114                            (setf first-line (regex-replace-all "<[^>]+>" first-line ""))
115                            (setf first-line (regex-replace-all "^(([^.]|\\.\\S)+)\\.\\s+.*$" first-line "\\1."))
116                            (setf first-line (regex-replace-all "(\\s)\\s+" first-line "\\1"))
117                            (setf first-line (regex-replace-all "^\\s(.+)$" first-line "\\1"))
118                            (when (scan "^([^.]|\\.\\S)+\\.$" first-line)
119                                (setf first-line (concatenate 'string first-line " " cliki-url))
120                                (return-from cliki-return first-line))))
121                         (format nil "No definition was found in the first 5 lines of ~A" cliki-url)))
122                (if stream (close stream)))))
123        (condition (c &rest whatever) (return-from cliki-return (format nil "An error was encountered in lookup.")))))))
124
125(defvar *cliki-connection*)
126(defvar *cliki-nickname*)
127
128(defmacro aif (test conseq &optional (else nil))
129  `(let ((it ,test))
130     (if it ,conseq
131       (symbol-macrolet ((it ,test))
132         ,else))))
133
134(defparameter *cliki-attention-prefix* "minion: ")
135
136(defparameter *cliki-bot-help* "The minion bot supplies small definitions and performs lookups on CLiki. To use it, try ``minion: term?''. To add a term for IRC, try saying ``minion: add \"term\" as: definition'' or ``minion: alias \"term\" as: term''; otherwise, edit the corresponding CLiki page.")
137
138(defun cliki-lookup (term-with-question)
139  (let ((first-pass (regex-replace-all "^(\\s*)([^?]+)(\\?*)$" term-with-question "\\2")))
140    (setf first-pass (regex-replace-all "\\s\\s+" first-pass ""))
141    (setf first-pass (regex-replace-all "\\s*$" first-pass ""))
142    (if (scan "^add \"([^\"]+)\" as: (.+)$" first-pass)
143        (let ((term (regex-replace "^add \"([^\"]+)\" .*$" first-pass "\\1"))
144              (defn (regex-replace "^add \"[^\"]+\" as: (.+)$" first-pass "\\1")))
145          (add-small-definition term defn)
146          "OK, done.")
147        (if (scan "^alias \"([^\"]+)\" as: (.+)$" first-pass)
148            (let ((term (regex-replace "^alias \"([^\"]+)\" .*$" first-pass "\\1"))
149                  (defn (regex-replace "^alias \"[^\"]+\" as: (.+)$" first-pass "\\1")))
150              (add-small-definition term (list defn))
151              "OK, done.")
152            (or
153             (if (string-equal first-pass "help") *cliki-bot-help*)
154             (if (scan "^(?i)do my bidding!*$" first-pass) "Yes, my master.")
155             (concatenate 'string first-pass ": "
156                          (or (let ((term (cdr (assoc first-pass *small-definitions* :test #'string-equal))))
157                                (if term (if (stringp term) term (cliki-lookup (car term)))))
158                              (cliki-first-sentence first-pass))))))))
159
160(defun valid-cliki-message (message)
161  (eql (search *cliki-attention-prefix* (trailing-argument message) :test #'char-equal) 0))
162
163(defun msg-hook (message)
164  (if (string-equal (first (arguments message)) *cliki-nickname*)
165      (if (valid-cliki-message message)
166          (privmsg *cliki-connection* (source message) (cliki-lookup (subseq (trailing-argument message) (length *cliki-attention-prefix*))))
167        (privmsg *cliki-connection* (source message) (cliki-lookup (trailing-argument message))))
168    (if (valid-cliki-message message)
169        (privmsg *cliki-connection* (first (arguments message)) (cliki-lookup (subseq (trailing-argument message) (length *cliki-attention-prefix*)))))))
170
171(defvar *cliki-nickserv-password* "")
172
173(defun notice-hook (message)
174  (if (and (string-equal (source message) "NickServ")
175           (scan "owned by someone else" (trailing-argument message)))
176      (privmsg *cliki-connection* (source message) (format nil "IDENTIFY ~A" *cliki-nickserv-password*))))
177
178(defun start-cliki-bot (nick server &rest channels)
179  (read-small-definitions)
180  (setf *cliki-nickname* nick)
181  (setf *cliki-connection* (connect :nickname *cliki-nickname* :server server))
182  (mapcar #'(lambda (channel) (join *cliki-connection* channel)) channels)
183  (add-hook *cliki-connection* 'irc::irc-privmsg-message 'msg-hook)
184  (add-hook *cliki-connection* 'irc::irc-notice-message 'notice-hook)
185  #+sbcl (start-background-message-handler *cliki-connection*)
186  #-sbcl (read-message-loop *cliki-connection*))
187
188(defun shuffle-hooks ()
189  (irc::remove-hooks *cliki-connection* 'irc::irc-privmsg-message)
190  (add-hook *cliki-connection* 'irc::irc-privmsg-message 'msg-hook))
Note: See TracBrowser for help on using the repository browser.