trilemabot.lisp  1 ;;;; trilemabot.lisp
  2 
  3 (in-package #:trilemabot)
  4 
  5 ;; These are used to check that we're joining #trilema; make sure that
  6 ;; your bot is configured accordingly if you want to use
  7 ;; #trilema-specific functionality.
  8 (defparameter *trilema-server* ".freenode.net")
  9 (defparameter *trilema-channel* "#trilema")
 10 (defparameter *trilema-deedbot-nick* "deedbot")
 11 
 12 (defclass trilemabot (ircbot)
 13   ((in-chan :accessor trilemabot-in-chan :initform nil)
 14    (voiced :accessor trilemabot-voiced :initform nil)
 15    (voice-otp-stash :accessor trilemabot-voice-otp-stash
 16 		    :initarg :voice-otp-stash
 17 		    :initform nil)
 18    (inbox :accessor trilemabot-inbox
 19 	  :initform nil)
 20    (cmd-prefix :reader trilemabot-cmd-prefix :initarg :cmd-prefix)))
 21 
 22 ;; <command> ::= <cmd-prefix><space>*<command><space>+<arguments>
 23 (defgeneric trilemabot-handle-cmd (command bot message target arguments)
 24   (:documentation "Handle prefixed `command' sent to `bot'.
 25 
 26 `message' is the cl-irc message object sent to the bot. `arguments'
 27 is a string containing everything sent after the command."))
 28 
 29 (defmethod ircbot-connect :after ((bot trilemabot))
 30   (with-slots (connection) bot
 31     (add-hook connection 'irc-join-message
 32 	      #'(lambda (message) (trilemabot-join-channel bot message)))
 33     (add-hook connection 'irc-part-message
 34 	      #'(lambda (message) (trilemabot-part-channel bot message)))
 35     (add-hook connection 'irc-privmsg-message
 36 	      #'(lambda (message) (trilemabot-handle-privmsg bot message)))
 37     (add-hook connection 'irc-mode-message
 38 	      #'(lambda (message) (trilemabot-check-mode bot message)))))
 39 
 40 (defmethod ircbot-disconnect :after ((bot trilemabot) &optional (quit-msg ""))
 41   (declare (ignore quit-msg))
 42   (with-slots (in-chan voiced) bot
 43     (setf in-chan nil
 44 	  voiced nil)))
 45 
 46 (defmethod trilemabot-join-channel ((bot trilemabot) message)
 47   (destructuring-bind (channel) (arguments message)
 48     (when (and (string= (source message) (ircbot-nick bot))
 49 	       (string= channel *trilema-channel*)
 50 	       (search *trilema-server* (ircbot-server bot)))
 51       (setf (trilemabot-in-chan bot) t)
 52       (trilemabot-voice bot))))
 53 
 54 (defmethod trilemabot-part-channel ((bot trilemabot) message)
 55   (let ((channel (car (arguments message))))
 56    (when (and (string= (source message) (ircbot-nick bot))
 57 	      (string= channel *trilema-channel*)
 58 	      (search *trilema-server* (ircbot-server bot)))
 59      (setf (trilemabot-in-chan bot) nil
 60 	   (trilemabot-voiced bot) nil))))
 61 
 62 (defmethod trilemabot-handle-privmsg ((bot trilemabot) message)
 63   (destructuring-bind (target message-text) (arguments message)
 64     ;; Messages from deedbot go to inbox
 65     (when (and (string= (source message) *trilema-deedbot-nick*)
 66 	       (string= target (ircbot-nick bot)))
 67       (format *standard-output* "<~a>: ~a~%"
 68 	      (source message) message-text)
 69       (push (list :from (source message)
 70 		  :time (received-time message)
 71 		  :message message-text)
 72 	    (trilemabot-inbox bot)))
 73     ;; Prefixed commands go to trilemabot-handle-cmd
 74     (let ((command (parse-command bot message-text)))
 75       (when (= 2 (length command))
 76 	(trilemabot-handle-cmd (car command) bot message target (cadr command))))))
 77 
 78 (defmethod trilemabot-check-mode ((bot trilemabot) message)
 79   (when (= 3 (length (arguments message))) ; mode change for user in chan
 80     (destructuring-bind (channel mode nick) (arguments message)
 81       (when (and (string= channel *trilema-channel*)
 82 		 (string= nick (ircbot-nick bot)))
 83 	(cond
 84 	  ((or (string= mode "+o")
 85 	       (string= mode "+v")) (setf (trilemabot-voiced bot) t))
 86 	  ((or (string= mode "-o")
 87 	       (string= mode "-v")) (setf (trilemabot-voiced bot) nil)))))))
 88 
 89 (defmethod trilemabot-add-voice-otps ((bot trilemabot) &rest otps)
 90   (with-slots (voice-otp-stash) bot
 91     (setf voice-otp-stash (nconc voice-otp-stash otps))))
 92 
 93 (defmethod trilemabot-voice ((bot trilemabot))
 94   (with-slots (voice-otp-stash) bot
 95     (if voice-otp-stash
 96 	(trilemabot-send-otp bot (pop voice-otp-stash))
 97 	(format *standard-output* "[trilemabot ~a@~a] No OTPs available.~%"
 98 		(ircbot-nick bot) (ircbot-server bot)))))
 99 
100 (defmethod trilemabot-send-up ((bot trilemabot) &optional nick)
101   (ircbot-send-message bot *trilema-deedbot-nick*
102 		       (make-bot-command "!!up"
103 					 (or nick (ircbot-nick bot)))))
104 
105 (defmethod trilemabot-send-otp ((bot trilemabot) otp)
106   (ircbot-send-message bot *trilema-deedbot-nick*
107 		       (make-bot-command "!!v" otp)))
108 
109 (defun make-bot-command (command &rest arguments)
110   "Make raw bot command string consisting of `command' and
111 space-separated `arguments'."
112   (with-output-to-string (out)
113     (write-string command out)
114     (dolist (arg arguments)
115       (write-string " " out)
116       (write-string arg out))))
117 
118 (defmethod trilemabot-save-voice-otp-stash ((bot trilemabot) filespec)
119   (with-open-file (out filespec
120 		       :direction :output
121 		       :if-exists :supersede
122 		       :if-does-not-exist :create)
123     (print (trlb:trilemabot-voice-otp-stash bot) out)
124     (finish-output out)))
125 
126 ;; A default command handler
127 (trilemabot-define-default-cmd (command bot message target args)
128   (format *standard-output* "[trilemabot] Unhandled command: ~s ~a from ~a to ~a.~%"
129 	  command args (source message) target))
130 
131 ;; Wrapper over defmethod to specialize trilemabot-handle-cmd
132 (defmacro trilemabot-define-cmd ((command bot message-var target-var arguments-var)
133                            &body body)
134   (let ((%command-var (gensym "g-cmd-command-var")))
135     `(defmethod trilemabot-handle-cmd ((,%command-var (eql ',command))
136 				       (,bot trilemabot)
137 				       ,message-var ,target-var ,arguments-var)
138        ,@body)))
139 
140 (defmacro trilemabot-define-default-cmd ((command-var
141 					  bot message-var target-var arguments-var)
142                                    &body body)
143   `(defmethod trilemabot-handle-cmd (,command-var (,bot trilemabot)
144                            ,message-var ,target-var ,arguments-var)
145      ,@body))
146 
147 ;; Prefixed command parsing
148 (defun parse-command (bot message-text)
149   (when (prefix-matches-p bot message-text)
150     (let ((tokens (with-input-from-string (in (trim-prefix bot message-text))
151 		    (list (intern (symbol-name (read in nil)) :keyword)
152 			  (read-line in nil "")))))
153       (and (car tokens) tokens))))
154 
155 (defun trim-prefix (bot message-text)
156   (when (prefix-matches-p bot message-text)
157    (string-left-trim " "
158     (subseq message-text (length (trilemabot-cmd-prefix bot))))))
159 
160 (defun prefix-matches-p (bot message-text)
161   (let* ((prefix (trilemabot-cmd-prefix bot))
162          (prefix-end (length prefix)))
163     (when (>= (length message-text) prefix-end)
164         (string= prefix message-text
165                  :start1 0 :end1 prefix-end
166                  :start2 0 :end2 prefix-end))))
167 
168 (defun make-trilemabot (server port nick password channels cmd-prefix
169 			&optional voice-otps)
170   (make-instance 'trilemabot
171 		 :server server
172 		 :port port
173 		 :nick nick
174 		 :password password
175 		 :channels channels
176 		 :cmd-prefix cmd-prefix
177 		 :voice-otp-stash voice-otps))