/
nilbot-commands.lisp
398 lines (340 loc) · 13.4 KB
/
nilbot-commands.lisp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
;; -*- Lisp -*-
;; nilbot-commands.lisp --
;;
;; Copyright (C) 2009,2011,2012 David Vazquez
;;
;; This program is free software: you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;;
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;;
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;;
(in-package :nilbot)
;;; User who invoked nilbot, target of nilbot ouptut and the user
;;; permissions respectively. They are dynamically bound when a
;;; privmsg is received.
(defvar *user*)
(defvar *recipient*)
(defvar *receive-message-hook* '(process-message))
;;; non-NIL if the server supports the capability IDENTIFY-MSG. We use
;;; this in order to be confident of the user and does not require
;;; authentication.
(defvar *identify-msg-p* nil)
;;; Output
;;; IRC Flood is often an impediment to produce useful output. We
;;; implement here an important feature: continuable output. Every
;;; response of nilbot command is truncated, but it is recorded in
;;; order to the user will be capable of continue the output with the
;;; ,more command.
(defvar *max-output-lines* 6)
;;; Number of seconds which a continuable output is recorded.
(defvar *max-pending-output-live* 300)
(defstruct (output-record (:constructor make-output-record (mark tail)))
(timestamp (get-universal-time))
(mark)
(tail))
(defvar *pending-output*
(make-hash-table :test #'equal))
(defun count-pending-output (to)
(let ((recorded-ouptut (gethash to *pending-output*)))
(if (null recorded-ouptut)
0
(length (cdr (output-record-mark recorded-ouptut))))))
(defun store-pending-output (to message)
(let ((recorded-output (gethash to *pending-output*)))
;; Create a new recorded-output register if no one existed.
(when (null recorded-output)
(let ((cell (list ':message-mark)))
(setf recorded-output (make-output-record cell cell))
(setf (gethash to *pending-output*) recorded-output)))
(with-slots (timestamp tail)
recorded-output
(let* ((new-cell (list message)))
(setf (cdr tail) new-cell)
(setf tail new-cell)))))
(defun reset-pending-output (to)
(remhash to *pending-output*))
(defun finish-pending-output ()
(do-hash-table (to output) *pending-output*
;; Clean deprecated output-records
(when (> (- (get-universal-time) (output-record-timestamp output))
*max-pending-output-live*)
(reset-pending-output to))
(loop for tail on (cdr (output-record-mark output))
for head = (car tail)
for count from 0
until (eq head '---more---)
do (irc:privmsg *irc* to head)
finally
(cond
((null tail)
(reset-pending-output to)
(return t))
(t
(unless (zerop count)
(irc:privmsg *irc* to "[more]"))
(setf (cdr (output-record-mark output)) tail)
(return nil))))))
(defun continue-pending-output (to)
(let ((output (gethash to *pending-output*)))
(when (and output (eq (cadr (output-record-mark output)) '---more---))
(pop (cdr (output-record-mark output))))))
;;; High level functions.
(defun more (&optional (to *recipient*))
(store-pending-output to '---more---))
;;; non-nil if the response must be immediate, instead of continuable.
(defvar *immediate-response-p* nil)
(defun response-to (to fmt &rest args)
(cond
(*immediate-response-p*
(irc:privmsg *irc* to (apply #'format nil fmt args)))
(t
(when (zerop (mod (1+ (count-pending-output to)) *max-output-lines*))
(more to))
(store-pending-output to (apply #'format nil fmt args)))))
(defun response (fmt &rest args)
(apply #'response-to *recipient* fmt args))
(defun action-to (to fmt &rest args)
(irc::ctcp *irc* to (format nil "ACTION ~?" fmt args)))
(defun action (fmt &rest args)
(apply #'action-to *recipient* fmt args))
(defun myself ()
(irc:nickname (irc:user *irc*)))
(defun myselfp (str)
(string= str (myself)))
(defun cap-handler (message)
(destructuring-bind (target &optional response capabilities)
(irc:arguments message)
(declare (ignore target))
;; The *IDENTIFY-MSG-P* variable is dangerous. We must be
;; conservative here and set *IDENTIFY-MSG-P* when we are sure the
;; server support the capability.
(setf *identify-msg-p* nil)
(when (and response (string= response "ACK"))
(let ((capabs (split-string capabilities)))
(when (find "identify-msg" capabs :test #'string=)
(setf *identify-msg-p* t))))))
(defun privmsg-handler (message)
(with-simple-restart (irc-toplevel "Return to IRC toplevel loop.")
(let ((source (irc:source message)))
(destructuring-bind (target input)
(irc:arguments message)
(dolist (hook *receive-message-hook*)
(funcall hook source target input))))))
(defun process-message (origin target message)
;; If the IDENTIFY-MSG is avalaible, we require the user is
;; identified in the services of the IRC server.
(when *identify-msg-p*
(if (char= (char message 0) #\+)
(setq message (subseq message 1))
(return-from process-message)))
;; Process the message
(let (prefix)
;; Check if the message is a nilbot command
(cond
((eql (char message 0) *default-prefix*)
(setq prefix 1))
;; 'nilbot: ' messages
((let* ((mark (format nil "~a: " (irc:nickname (irc:user *irc*))))
(posi (search mark message)))
(and (integerp posi) (zerop posi)))
(setq prefix (length (format nil "~a: " (irc:nickname (irc:user *irc*))))))
;; queries
((myselfp target)
(setq prefix 0))
(t
(return-from process-message nil)))
;; Invoke the command
(with-input-from-string (stream message :start prefix)
(let ((cmd (parse-command stream))
(arg (subseq (or (read-line stream nil) " ") 1)))
(let ((*user* origin)
(*recipient* (if (myselfp target) origin target)))
(unless (permission= (user-permission *user*) "undesirable")
(handler-case (run-command cmd arg)
(simple-error (err)
(let ((*immediate-response-p* t))
(apply #'response
(simple-condition-format-control err)
(simple-condition-format-arguments err)))))))))))
;;; Permissions functions
(deftype permission ()
`(satisfies permissionp))
(defvar *permissions*
#("undesirable" "nobody" "user" "admin"))
(defun permissionp (x)
(find x *permissions* :test #'string-ci=))
(defun permission= (perm1 perm2)
(declare (permission perm1 perm2))
(string-ci= perm1 perm2))
(defun permission<= (perm1 perm2)
(declare (permission perm1 perm2))
(<= (position perm1 *permissions* :test #'string-ci=)
(position perm2 *permissions* :test #'string-ci=)))
;;; Require PERM priviledge level.
(defun require-permission (perm)
(unless (permission<= perm (user-permission *user*))
(if (find (char perm 0) "aeiou")
(error "You need be an ~a to do this." perm)
(error "You need be a ~a to do this." perm))))
;;; This hashtable keeps the association between strings and handlers.
(defvar *command-handlers*
(make-hash-table :test #'equal))
(defclass handler ()
((documentation
:initarg :documentation
:reader handler-documentation)
(module
:initform (let ((name (package-name *package*)))
(if (string-prefix-p "NILBOT" name)
(and (< 6 (length name))
(eql (char name 6) #\.)
(subseq name 7))
(error "Defining a command from a non-nilbot package.")))
:reader handler-module)
(function
:initarg :function
:initform (required-arg)
:reader handler-function)
(parse-arguments-p
:initarg :parse-arguments-p
:initform t
:reader handler-parse-arguments-p)
;; This slot is only informative. The function `run-command' will
;; work even if this slot is T and the user is not an admin. Each
;; command is responsible to require to the user the required
;; priviledges.
(permission
:initarg :permission
:initform "user"
:reader handler-permission)
(aliases
:initarg :aliases
:initform ()
:reader handler-aliases)
;; Taskbot will not discard the pending output if this is
;; non-nil. This is used to implement continuable output actually.
(keep-last-output-p
:initarg :keep-last-output-p
:initform nil
:reader handler-keep-last-output-p)))
(defclass alias ()
((handler
:initarg :handler
:initform (required-arg)
:reader alias-handler)))
(define-predicate-type handler)
(define-predicate-type alias)
(defun find-handler (handler-spec)
(etypecase handler-spec
(null
nil)
(handler
handler-spec)
(alias
(alias-handler handler-spec))
(string
(let ((entry (gethash (string-upcase handler-spec) *command-handlers*)))
(find-handler entry)))))
(defun create-command (&rest initargs &key name &allow-other-keys)
(setf (gethash name *command-handlers*)
(apply #'make-instance 'handler :allow-other-keys t initargs)))
(defun create-command-alias (alias command)
(let ((handler (find-handler command)))
(if handler
(setf (gethash alias *command-handlers*)
(make-instance 'alias :handler handler))
(error "The command `~a' does not exist." command))))
(defmethod used-modules ((name string))
(cond
((zerop (length name))
nil)
((char= (char name 0) #\#)
(used-modules (find-channel name)))
(t
(used-modules (find-user name)))))
(defmethod (setf used-modules) (new-value (name string))
(cond
((zerop (length name))
nil)
((char= (char name 0) #\#)
(setf (used-modules (find-channel name)) new-value))
(t
(setf (used-modules (find-user name)) new-value))))
(defun run-command (command argument-line)
(let ((handler (find-handler command)))
(when (and handler (not (handler-keep-last-output-p handler)))
(reset-pending-output *recipient*))
(handler-case
(cond
((or (null handler)
(and (handler-module handler)
(not (find (handler-module handler) (used-modules *recipient*) :test #'equalp))))
(error "Unknown command"))
;; Commands with parsed arguments
((handler-parse-arguments-p handler)
(with-input-from-string (stream argument-line)
(apply (handler-function handler)
(parse-arguments stream))))
;; Command with raw argument
((not (handler-parse-arguments-p handler))
(funcall (handler-function handler) argument-line)))
;; (type-error (error)
;; (error "The datum ~a was expected to be of type ~a."
;; (type-error-datum error)
;; (type-error-expected-type error)))
)
(finish-pending-output)))
(defun arglist-unparsed-argument-p (arglist)
(and (not (null arglist))
(eq (car arglist) '&unparsed-argument)))
;;; Define a nilbot command. You can see nilbot-system.lisp for
;;; several examples of usage.
(defmacro define-command (name (&rest args) options &body code)
(let ((documentation (cadr (assoc :documentation options)))
(aliases (cdr (assoc :aliases options)))
(permission (or (second (assoc :permission options)) "user"))
(keep-last-output-p (second (assoc :keep-last-output-p options)))
(fname (symbolize 'irc-handler- name)))
(check-type documentation (or null string))
(check-type permission permission)
`(progn
;; Function handler
(defun ,fname ,(if (arglist-unparsed-argument-p args) (cdr args) args)
(block ,name
,(if permission `(require-permission ,permission))
,@code))
;; Register handler
(create-command :name ,(string name)
:documentation ,documentation
:function ',fname
:parse-arguments-p ,(not (arglist-unparsed-argument-p args))
:permission ,permission
:keep-last-output-p ,keep-last-output-p)
;; Register aliases
(dolist (alias ',aliases)
(create-command-alias alias ,(string name))))))
;;; Utility macro to define subcommands.
(defmacro subcommand-dispatch (subcommand arguments &body clausules)
(check-type subcommand symbol)
(with-gensyms (subcommand-var arguments-var)
`(let ((,subcommand-var ,subcommand)
(,arguments-var ,arguments))
(cond
,@(loop for clausule in clausules
collect
(destructuring-bind ((name &rest args) &body code)
clausule
`((string-ci= ,subcommand-var ,name)
(destructuring-bind ,args ,arguments-var
,@code))))
(t
(error "~a is an invalid subcommand." ,subcommand-var))))))
;; nilbot-commands.lisp ends here