Browse code
Improbe slack api interface
Showing 4 changed files
... | ... |
@@ -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")) |