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