git.fiddlerwoaroof.com
Browse code

Improbe slack api interface

fiddlerwoaroof authored on 14/09/2016 07:48:53
Showing 4 changed files
... ...
@@ -26,6 +26,7 @@
26 26
           )
27 27
   :serial t
28 28
   :components ((:file "package")
29
+               (:file "event-pump")
29 30
                (:file "js-executor")
30 31
                (:file "slack-client")
31 32
                (:file "test-ui")))
... ...
@@ -9,6 +9,9 @@
9 9
 	   #:help #:make-message #:modules #:latest-id #:work-queue #:result-queue
10 10
 	   #:ws-client #:waiting-pings #:ts #:channel))
11 11
 
12
+(defpackage slacker.api
13
+  (:use))
14
+
12 15
 (defpackage :hhgbot-augmented-assistant
13 16
   (:use :cl :alexandria :serapeum :slacker))
14 17
 ;; (defpackage #:hhgbot
... ...
@@ -1,13 +1,5 @@
1 1
 (in-package :slacker)
2 2
 
3
-(defclass event-pump ()
4
-  ((%ws-client :accessor ws-client :initarg :ws-client)
5
-   (%waiting-pings :accessor waiting-pings :initform 0)
6
-   (%modules :accessor modules :initform (make-hash-table))
7
-   (%latest-id :accessor latest-id :initform 0)
8
-   (%work-queue :accessor work-queue :initform (make-instance 'chanl:unbounded-channel))
9
-   (%result-queue :accessor result-queue :initform (make-instance 'chanl:unbounded-channel))))
10
-
11 3
 (defmethod attach-module ((event-pump event-pump) module &rest args &key)
12 4
   (setf (gethash (make-keyword module)
13 5
 		 (modules event-pump))
... ...
@@ -189,6 +181,10 @@
189 181
 	   ,@body)
190 182
        (setf (gethash ,name *command-table*) (function ,command-sym)))))
191 183
 
184
+(defun safe-apply (func event-pump ts channel args)
185
+  (with-simple-restart (continue "Skip command")
186
+    (apply func event-pump ts channel args)))
187
+
192 188
 (defun handle-command (event-pump ts channel command args)
193 189
   (declare (ignorable args))
194 190
   (let* ((command (subseq (plump:decode-entities command) 1))
... ...
@@ -197,8 +193,51 @@
197 193
     (terpri)
198 194
     (print command)
199 195
     (if handler
200
-	(apply handler event-pump ts channel args)
201
-	(queue-message event-pump channel (concat "I don't underand the command `" command "`.")))))
196
+	(safe-apply handler event-pump ts channel args)
197
+	(queue-message event-pump channel (concat "I don't understand the command `" command "`.")))))
198
+
199
+(defun slack-api-call (method &rest args)
200
+  (bb:with-promise (resolve reject)
201
+    (bt:make-thread
202
+     (lambda ()
203
+       (handler-case
204
+	   (let ((api-result (yason:parse
205
+			      (babel:octets-to-string 
206
+			       (drakma:http-request (concat "https://slack.com/api/" method "?token=" *api-token*)
207
+						    :method :post
208
+						    :content (quri:url-encode-params
209
+							      (loop for (key value) on args by #'cddr
210
+								 collect (cons (string-downcase key) value)))
211
+						    :proxy (list "127.0.0.1" 8080))))))
212
+	     ;todo error handling . . .
213
+	     (resolve api-result)) 
214
+	 (t (c) (reject c)))))))
215
+
216
+(defgeneric api-call (name args)
217
+  (:method ((name symbol) (args list))
218
+    (slack-api-call ,)))
219
+
220
+(defmacro define-api-wrapper (name required-args &rest args)
221
+  (flet ((name-case (string)
222
+	   (let ((parts (split-sequence #\- (string-downcase string))))
223
+	     (apply #'concatenate 'string
224
+		    (car parts)
225
+		    (mapcar #'string-capitalize (cdr parts))))))
226
+    (let* ((api-method-name (name-case name)))
227
+      `(progn (defun ,name (,@required-args &rest r &key ,@args)
228
+		(apply #'slack-api-call ,api-method-name
229
+		       ,@(loop for req-arg in required-args
230
+			    append (list (make-keyword req-arg) req-arg))
231
+		       r))
232
+	      (eval-when (:compile-toplevel :load-toplevel :execute)
233
+		(let ((*package* 'slacker.api))
234
+		  (import ',name)
235
+		  (export ',name)))))))
236
+
237
+
238
+(defmacro define-api-wrappers (&body body)
239
+  `(progn ,@(loop for (name required-args . rest) in body
240
+		 collect `(define-api-wrapper ,name ,required-args ,@rest))))
202 241
 
203 242
 (defun edit-message (ts channel text)
204 243
   (babel:octets-to-string
... ...
@@ -237,3 +276,12 @@
237 276
 	("text" . ,data)))
238 277
      s)))
239 278
 
279
+
280
+(in-package :slacker.api)
281
+
282
+(slacker::define-api-wrappers
283
+  (chat.delete (ts channel) as_user)
284
+  (chat.me-message (channel text))
285
+  (chat.post-message (channel text) parse link_name attachments unfurl_links unfurl_media username as_user icon_uri icon_emoji)
286
+  (chat.update (ts channel text) attachments parse link_names as_user))
287
+
... ...
@@ -5,14 +5,22 @@
5 5
 (defvar *client*)
6 6
 ;; Macros
7 7
 
8
+(defun start-in-repl (&optional (start-bot t))
9
+  (ubiquitous:restore :hhgbot-augmented-assistant)
10
+  (setf slacker::*api-token* (ubiquitous:value :api-token :atomampd))
11
+  (if start-bot
12
+      (start-with-apitoken)
13
+      slacker::*api-token*))
14
+
8 15
 (defun start-with-apitoken ()
9 16
   (ubiquitous:restore :hhgbot-augmented-assistant)
10
-  (let ((slacker::*api-token* (ubiquitous:restore :api-token :atomampd)))
17
+  (let ((slacker::*api-token* (ubiquitous:value :api-token :atomampd)))
11 18
     (unless slacker::*api-token*
12 19
       (format *terminal-io* "~&API Token? ")
13 20
       (finish-output *terminal-io*)
14 21
       (setf slacker::*api-token* (read-line)))
15
-    (slacker:coordinate-threads)))
22
+    (values (slacker:coordinate-threads)
23
+	    slacker::*api-token*)))
16 24
 
17 25
 (defmacro if-let* ((&rest bindings) &body (then-form &optional else-form))
18 26
   "Like if-let, but sets bindings sequentially.  Doesn't short-circuit."
... ...
@@ -29,22 +37,27 @@
29 37
 (define-command "myip" (event-pump ts channel)
30 38
   (in-wq (event-pump)
31 39
     (blackbird:alet ((ip (carrier:request "http://api.ipify.org/" :return-body t)))
32
-      (format *zxcv* "~&IP: ~a~%" ip)
40
+      (format *zxcv* "~&IP: ~a~%TS: ~a~%CHANNEL: ~a~%CLIENT: ~A~%" (babel:octets-to-string ip) ts channel event-pump)
33 41
       (edit-message ts channel
34 42
 		    (concat "My ip is: "
35 43
 			    (babel:octets-to-string ip))))))
36 44
 
37 45
 (define-command "jira" (event-pump ts channel project &optional issue-number &rest rest)
38
-  (apply #'edit-message ts channel
39
-	 (fw.lu:ensure-list
40
-	  (cond (rest
41
-		 "I don't understand . . .")
42
-		(issue-number
43
-		 (format nil "https://atomampd.atlassian.net/browse/~A-~A"
44
-			 project issue-number))
45
-		(t
46
-		 (format nil "https://atomampd.atlassian.net/browse/ATOMOS-~a"
47
-			 project))))))
46
+  (let ((drakma:*drakma-default-external-format* :utf-8))
47
+    (apply #'edit-message ts channel
48
+	   (fw.lu:ensure-list
49
+	    (cond (rest
50
+		   "I don't understand . . .")
51
+		  (issue-number
52
+		   (format nil "https://atomampd.atlassian.net/browse/~A-~A"
53
+			   project issue-number))
54
+		  (t
55
+		   (format nil "https://atomampd.atlassian.net/browse/ATOMOS-~a"
56
+			   project)))))))
57
+
58
+(define-command "pr" (event-pump ts channel num)
59
+  (let ((num (parse-integer num)))
60
+    (edit-message ts channel (format nil "https://bitbucket.org/atomampd/atomos/pull-requests/~d?w=1" num))))
48 61
 
49 62
 (define-command "js>" (event-pump ts channel &rest args)
50 63
   (declare (ignorable ts))
... ...
@@ -66,7 +79,95 @@
66 79
 	(format s seq)))))
67 80
 
68 81
 (define-command "paste" (event-pump ts channel)
69
-  (ubiquitous:restore :atomampd-slack)
82
+  (with-simple-restart (abort "Stop command")
83
+    (ubiquitous:restore :atomampd-slack)
84
+    (format t "foo")
85
+    (let ((drakma:*drakma-default-external-format* :utf-8))
86
+      (edit-message ts channel
87
+		    (format nil "```~%~a~%```"
88
+			    (pop (ubiquitous:value :clip)))))))
89
+
90
+(define-command "id" (event-pump ts channel &optional (for "channel"))
70 91
   (edit-message ts channel
71
-		(format nil "```~%~a~%```"
72
-			(pop (ubiquitous:value :clip)))))
92
+		(string-case for
93
+		  ("channel" channel)
94
+		  (t (concat "don't know the id for " for)))))
95
+    
96
+(defmacro with ((var val) &body body)
97
+  `(let ((,var ,val))
98
+     ,@body))
99
+
100
+(ql:quickload '(:vecto :ironclad))
101
+
102
+(defun hash-of-vector (vec)
103
+  (ironclad:byte-array-to-hex-string (ironclad:digest-sequence :sha256 vec)))
104
+
105
+(defpackage #:vecto-example
106
+  (:use :cl :alexandria :serapeum :slacker :vecto)
107
+  (:shadowing-import-from :alexandria :rotate))
108
+
109
+(in-package :vecto-example)
110
+(defmacro with ((var val) &body body)
111
+  `(let ((,var ,val))
112
+     ,@body))
113
+
114
+
115
+(defun octets-to-36string (octets)
116
+  (string-join (map 'list (lambda (it)
117
+			    (format nil "~2,1,0,'0@a"
118
+				    (with (*print-base* 36)
119
+				      (write-to-string it ))))
120
+		    octets)))
121
+
122
+(defmacro with-digested-output ((stream (data-sym digest-sym)) data-form &body body)
123
+  (with-gensyms (digest-stream data-stream)
124
+    `(let* ((,digest-stream (ironclad:make-digesting-stream :sha256))
125
+	    (,data-stream (ironclad:make-octet-output-stream))
126
+	    (,stream (make-broadcast-stream ,data-stream ,digest-stream)))
127
+       ,data-form
128
+       (let ((,data-sym (ironclad:get-output-stream-octets ,data-stream))
129
+	     (,digest-sym (ironclad:produce-digest ,digest-stream)))
130
+	 ,@body))))
131
+
132
+(define-command "ring-word" (event-pump ts channel &optional (content #(#x3BB)))
133
+  (bt:make-thread
134
+   (lambda ()
135
+     (declare (optimize (debug 3)))
136
+     (let ((*default-pathname-defaults* "/home/edwlan/github_repos/hhgbot/"))
137
+       (with-canvas (:width 90 :height 90)
138
+	 (let ((font (get-font "times.ttf"))
139
+	       (step (/ pi 7)))
140
+	   (set-font font 40)
141
+	   (translate 45 45)
142
+	   (format hhgbot-augmented-assistant::*zxcv* "~&content: ~s~%" content)
143
+	   (draw-centered-string 0 -10 (map 'vector #'char-code content))
144
+	   (set-rgb-stroke 1 0 0)
145
+	   (centered-circle-path 0 0 35)
146
+	   (stroke)
147
+	   (set-rgba-stroke 0 0 1.0 0.5)
148
+	   (set-line-width 4)
149
+	   (dotimes (i 14)
150
+	     (with-graphics-state
151
+	       (vecto:rotate (* i step))
152
+	       (move-to 30 0)
153
+	       (line-to 40 0)
154
+	       (stroke))))
155
+	 (let* ((image-directory (ensure-directories-exist "/home/edwlan/public_html/slack_images/"))
156
+		(image-basename (concat channel "-" ts ".png"))
157
+		(image-filename (merge-pathnames image-basename image-directory))
158
+		(payload
159
+		 (concat "token=" *api-token*
160
+			 "&channel=" channel
161
+			 "&text=..." 
162
+			 "&as_user=true"
163
+			 "&attachments="
164
+			 (quri:url-encode
165
+			  (concat
166
+			   "[{\"fallback\": \"foo\", \"ts\":" ts ",\"image_url\": \"https://srv2.elangley.org/~edwlan/slack_images/"
167
+			   image-basename "\"}]")))))
168
+	   (vecto:save-png image-filename)
169
+	   (babel:octets-to-string
170
+	    (drakma:http-request "https://slack.com/api/chat.postMessage"
171
+				 :method :post
172
+				 :content payload))))))
173
+     :name "Image Maker"))