git.fiddlerwoaroof.com
hhgbot-2.lisp
60e29634
 (defpackage :hhgbot-2
   (:use :cl :alexandria :serapeum :slacker :fw.lu))
8daf399c
 
 (defpackage :quote-server
   (:use :cl :ningle :araneus :serapeum :alexandria :fw.lu)
801447ba
   (:export #:start))
8daf399c
 
60e29634
 (cl:in-package :hhgbot-2)
 
e98c1493
 (defun normalize-ref (value)
   (when (or (alexandria:ends-with-subseq "co" value)
             (alexandria:ends-with-subseq "s.c" value))
     (setf value (format nil "~a." value)))
   (when (alexandria:ends-with-subseq "sc" value)
     (setf value (format nil "~as.c." (subseq value 0 (- (length value) 2)))))
   (when (alexandria:ends-with-subseq "sc." value)
     (setf value (format nil "~as.c." (subseq value 0 (- (length value) 3)))))
 
   value)
 
60e29634
 ;; Special Variables
 
6416324b
 (defvar *xxx* (make-synonym-stream '*standard-output*))
60e29634
 (defvar *client*)
 (defvar *queue-pair* nil)
 (defvar *slack-url* "https://~a.slack.com")
 (defparameter *refs* (make-hash-table :test 'equalp))
 ;; Macros
 
ad204acd
 (defclass logging-slackbot ()
   ((%users :initform (make-hash-table :test 'equal))
    (%conversations :initform (make-hash-table :test 'equal))))
6416324b
 
ad204acd
 
e98c1493
 (defclass hhgbot-event-pump (slacker:event-pump #+(or)slacker.montezuma-store:montezuma-store logging-slackbot
 			     slacker.postmodern-store:postmodern-store)
6416324b
   ()
e98c1493
   #+(or)(:default-initargs :index-path "/tmp/slack-idx/"))
6416324b
 
60e29634
 (defun start-in-repl (&optional (start-bot t) (team-id :atomampd))
   (ubiquitous:restore :hhgbot-augmented-assistant)
   (setf slacker::*api-token* (ubiquitous:value :api-token :atomampd))
   (if start-bot
       (start-with-apitoken team-id)
       slacker::*api-token*))
 
 (defun start-with-apitoken (&optional (team-id :atomampd))
   (unless *queue-pair*
     (setf *queue-pair* (make-instance 'slacker::queue-pair)))
 
   (ubiquitous:restore :hhgbot-augmented-assistant)
   (let ((slacker::*api-token* (ubiquitous:value :api-token team-id)))
     (unless slacker::*api-token*
       (format *terminal-io* "~&API Token? ")
       (finish-output *terminal-io*)
       (let ((value (read-line)))
9fe08141
         (setf (ubiquitous:value :api-token team-id) value
               slacker::*api-token* value)))
e98c1493
     (values (slacker:coordinate-threads *queue-pair* 'hhgbot-event-pump
 					'(:postgres-connection-spec ("edwlan" "edwlan" nil :unix)))
9fe08141
             slacker::*api-token*)))
 
 (defun ensure-unescaped (src)
   (if (and (starts-with #\` src)
            (ends-with #\` src))
       (cond
         ((and (>= (length src) 6)
               (starts-with-subseq "```" src)
               (ends-with-subseq "```" src))
          (subseq src 3 (- (length src) 3)))
         ((>= (length src) 2)
          (subseq src 1 (- (length src) 1)))
         (t src))
       src))
60e29634
 
 (define-command "js>" (event-pump message channel &rest args)
   (declare (ignorable message))
9fe08141
   (let ((hhgbot-augmented-assistant::*js-executor* (get-module :js-executor event-pump))
         (js-src (ensure-unescaped (string-join args " "))))
60e29634
     (blackbird:alet ((result (hhgbot-augmented-assistant::submit-js hhgbot-augmented-assistant::*js-executor*
9fe08141
                                                                     js-src)))
60e29634
       (let ((result (cl-js:to-string result)))
9fe08141
         (queue-message event-pump channel
                        (subseq result 0
                                (min 1000
                                     (length result)))
                        :quote t
                        :thread (ensure-thread message))))))
60e29634
 
 (define-command "id" (event-pump message channel &optional (for "channel"))
   (queue-message event-pump channel
9fe08141
                  (string-case for
                    ("channel" channel)
                    (t (concat "don't know the id for " for)))
                  :quote t
                  :thread (ensure-thread message)))
60e29634
 
 (define-command "sources" (event-pump message channel &rest args)
   (declare (ignore args))
   (queue-message event-pump channel
9fe08141
                  (format nil "~{~a~^, ~}" (hash-table-keys *refs*))
                  :thread (ensure-thread message)))
60e29634
 
 (define-command "ref>" (event-pump message channel &optional source ref &rest args)
   (declare (ignore args))
e98c1493
   (when args
     (setf ref (format nil "~a~{~a~}" ref args)))
 
60e29634
   (if (and source ref)
       (when-let ((source-h (gethash source *refs*)))
9fe08141
         (queue-message event-pump channel
e98c1493
                        (gethash (normalize-ref ref) source-h (concat "Can't find " source " " ref))
9fe08141
                        :thread (keep-in-thread message)))
60e29634
       (queue-message event-pump channel (concat "Must provide both a source and a reference. See ;sources")
9fe08141
                      :thread (keep-in-thread message))))
60e29634
 
 (defmacro define-message-command (name (&rest args) &body body)
   (let ((message-sym (cadr args)))
     `(define-command ,name (,@args)
        (queue-message event-pump channel
9fe08141
                       (progn ,@body)
                       :thread (keep-in-thread ,message-sym)))))
 
1ffe31ab
 (define-command "latina" (event-pump message channel word &rest a)
   (declare (ignore a))
   (let* ((results (words-coprocess::get-word-results word))
801447ba
          (json-objs (split-sequence #\newline results :remove-empty-subseqs t))
          (parsed (mapcar 'yason:parse json-objs)))
1ffe31ab
     (queue-message event-pump channel
801447ba
                    (format nil "~{> ~a~2%~}"
                            (remove-if-not #'identity
                                           (mapcan (op (gethash "meanings" _))
                                                   parsed))))))
60e29634
 
 (define-message-command "random-quote" (event-pump message channel &rest args)
   args
   (let-each (:be *)
     (hash-table-keys *refs*)
     (random-elt *)
     (gethash * *refs*)
     (let ((keys (hash-table-keys *)))
       (gethash (random-elt keys) *))))
 
1ffe31ab
 (defun trace-value (&rest args)
   (format *trace-output* "===>>> ~{~s~^ ~}" args)
   (values-list args))
 
 (define-message-command "arc" (event-pump message channel &rest args)
e98c1493
   (if (equal (gethash "user" message)
 	     "U0CSPP3SB")
       (let ((r (with-output-to-string (s)
 		 (multiple-value-bind (results idx)
 		     (slacker.montezuma-store:search-index *client* "message" (string-join args " "))
 		   (montezuma:each results
 				   (lambda (h)
 				     (format s "> ~a: ~a~%"
 					     (local-time:format-timestring
 					      nil
 					      (local-time:unix-to-timestamp
 					       (floor 
 						(parse-number
 						 (montezuma:document-value (montezuma:get-document idx (montezuma:doc h))
 									   "ts")))
 					       )
 					      :format local-time:+rfc3339-format+)
 					     
1ffe31ab
 					     (montezuma:document-value (montezuma:get-document idx (montezuma:doc h))
e98c1493
 								       "text"))))))))
 	(if (= 0 (length r))
 	    (format nil "No results found for: `~a`" (string-join args " "))
 	    r))
       (format nil "No results found for: `~a`" (string-join args " "))))
1ffe31ab
 
6416324b
 (defun extract-channel-info (channels)
   (funcall (data-lens:pick
             (compose
              (data-lens:applying (lambda (name id is-member is-private is-mpim)
                                    (list name id
                                          (plist-hash-table
                                           (list "is-member" is-member
                                                 "is-private" is-private
                                                 "is-mpim" is-mpim)
                                           :test 'equal))))
              (data-lens:juxt (op (gethash "name" _))
                              (op (gethash "id" _))
                              (op (gethash "is_member" _))
                              (op (gethash "is_private" _))
                              (op (gethash "is_mpim" _)))))
            (gethash "channels" channels)))
 
60e29634
 (defun find-channel (name)
   (bb:alet* ((r (slacker.api:channels.list))
6416324b
              (channels (funcall (data-lens:pick
                                  (data-lens:juxt (op (gethash "name" _))
                                                  (op (gethash "id" _))))
                                 (gethash "channels" r))))
801447ba
     #+nil    (fw.su:log-json channels)
60e29634
     (assoc name channels :test 'equal)))
 
6416324b
 (defmacro with-output-to-json-string ((s &rest args &key indent) &body body)
   "Set up a JSON streaming encoder context, then evaluate BODY.
 Return a string with the generated JSON output."
   (declare (ignore indent))
   `(with-output-to-string (,s)
      (with-open-stream (,s (yason:make-json-output-stream s ,@args))
        ,@body)))
 
 (defmacro dbind* (destructuring-expression promise-gen &body body)
   `(bb:attach ,promise-gen
               (fw.lu:destructuring-lambda (,destructuring-expression)
                 ,@body)))
 
 (defmethod slacker:handle-message :before (type (event-pump hhgbot-event-pump) ts channel message)
   (declare (ignore type ts channel))
801447ba
   #+nil
6416324b
   (index-message message)
   (values))
 
 (define-command "notify-channel" (event-pump message channel
                                              &optional target
                                              &rest args)
60e29634
   (format *xxx* "~&~a: ~{~a~^ ~}~%" target args)
6416324b
   (dbind* (&optional target-name target-id) (find-channel target)
801447ba
     (if target-name
         (progn
           (with (message (string-join args #\space))
             (when-let* ((start-link (position #\< message))
                         (stop-link (position #\> message :start start-link))
                         (_ (> stop-link (+ 4 start-link))))
               (setf message (concat (subseq message 0 start-link)
                                     (subseq message (1+ start-link) stop-link)
                                     (subseq message (1+ stop-link)))))
             (queue-message event-pump target-id
                            message))
           (queue-message event-pump target-id
                          (format nil "Notifying channel ~a" target-name)
                          :thread (ensure-thread message)))
         (queue-message event-pump target-id (format nil "Can't find channel `~a`" target)
                        :thread (ensure-thread message)))))
60e29634
 
 (defparameter *reaction-store* (make-hash-table :test 'equalp :synchronized t))
 
 (defun format-reaction-store ()
   (format nil "~{~a~^~%~}"
9fe08141
           (loop for reaction in (sort (hash-table-keys *reaction-store*) #'string-lessp)
                 for (count users) = (gethash reaction *reaction-store*)
                 collect (format nil ":~a: `~a` ~{<@~a>~^ ~}" reaction count (remove-duplicates users :test 'equalp)))))
60e29634
 
 (define-message-command "show-reactions" (event-pump message channel &rest args)
9fe08141
   args
60e29634
   (format-reaction-store))
 
 (defmethod slacker:handle-message ((type (eql :reaction_added)) event-pump ts channel message)
   ;; {"type":"reaction_added","user":"U0CSPP3SB","item":{"type":"message","channel":"G1YA9SR0S","ts":"1489124136.780019"},"reaction":"+1","item_user":"U0CSPP3SB","event_ts":"1489124534.806294","ts":"1489124534.806294"}
   (let ((reaction-info (ensure-gethash (gethash "reaction" message) *reaction-store* (list 0 nil))))
     (incf (car reaction-info))
     (push (gethash "user" message) (cadr reaction-info))))
 
 (defun read-refs (stream)
   (loop for x = (read stream nil)
9fe08141
         while x
         for (type source ref text) = x
         do
            (setf (gethash ref
                           (ensure-gethash (string source)
                                           *refs*
                                           (make-hash-table :test 'equalp)))
                  text)))
60e29634
 (defun initialize-quotes ()
   (loop for filename in '("qda.sexp" "refs.sexp" "scg.sexp" "ss.sexp")
9fe08141
         do
            (with-input-from-file (s filename :external-format :utf-8)
              (read-refs s))))
60e29634
 
8daf399c
 (defun main ()
   (initialize-quotes)
   (quote-server:start)
   (setf (values *client* slacker::*api-token*)
801447ba
         (start-in-repl)))
8daf399c
 
60e29634
 (in-package :quote-server)
 
 (defparameter *app* (make-instance '<app>))
 
 (define-controller quote (params)
   (let* ((text (cdr (assoc :text params)))
9fe08141
          (ref (cdr (assoc :ref params))))
e98c1493
     (cons (cons text ref)
 	  (gethash (hhgbot-2::normalize-ref ref)
 		   (gethash text hhgbot-2::*refs*)))))
60e29634
 
 (define-controller random-quote (params)
   (declare (ignore params))
   (let-each (:be *)
     (hash-table-keys hhgbot-2::*refs*)
     (random-elt *)
     (let* ((text (gethash * hhgbot-2::*refs*))
9fe08141
            (keys (hash-table-keys text))
            (selected-elt (random-elt keys)))
60e29634
       (cons (cons * selected-elt) (gethash (random-elt keys) text)))))
 
 (define-view random-quote (model)
   (destructuring-bind ((book . ref) . text) model
     `(302
e98c1493
       ("Location"  ,(format nil "/q/~a/~a"
9fe08141
                             book ref))
60e29634
       (,text))))
 
 (define-spinneret-view quote (quote)
   (let ((title (format nil "Quote: ~a, ~a" (caar quote) (cdar quote)))
e98c1493
         (permalink (format nil "/q/~a/~a"
9fe08141
                            (caar quote)
e98c1493
                            (hhgbot-2::normalize-ref (cdar quote)))))
60e29634
     (:html
      (:head (:title title)
9fe08141
             (:link :href "https://fonts.googleapis.com/css?family=Lato:400&subset=latin,latin-ext" :rel "stylesheet" :type "text/css")
1ffe31ab
             (:style "p { font-family: 'Lato', sans-serif; width: 50vw; margin-left: 25vw; } p::first-letter {color:red} p:first-child { margin-top: 10vh; }")
9fe08141
             (:meta :property "og:title" :content title)
             (:meta :property "fb:app_id" :content "521205154682685")
             (:meta :property "og:url" :content permalink)
             (:meta :property "og:description" :content (cdr quote)))
60e29634
      (:body
       (:p (cdr quote))
       (:p (:a :href permalink permalink))
1ffe31ab
       (:p (:a :href "/" "Random quote"))))))
60e29634
 
 (defroutes *app*
   (("/" :GET) (as-route 'random-quote))
   (("/q/:text/:ref" :GET) (as-route 'quote))
   )
 
 (defparameter *word-index* (make-array 100 :adjustable t :fill-pointer 0))
 
 (defun index-quote (text ref)
   (let ((quote (split-sequence-if-not (op (alphanumericp _))
9fe08141
                                       (pick (list text ref) hhgbot-2::*refs*)
                                       :remove-empty-subseqs t)))
60e29634
     quote))
 
 (let ((handler '()))
   (defun start ()
     (push (clack:clackup *app*) handler))
   (defun stop ()
     (clack:stop (pop handler))))