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)) |
---|