(require 'json)
(defgroup slack nil
"Slack client."
:prefix "slack-"
:group 'applications)
(defcustom slack-buffer-name
"*Slack*"
"Slack buffer name."
:type 'string
:group 'slack)
(defconst slack-auth-default-team nil)
(defconst slack-auth-default-token nil)
(defconst slack-auth-file-name (expand-file-name "~/.slack-auth"))
(defcustom slack-auth
`((,slack-auth-default-team . ,slack-auth-default-token))
"Slack authorizations."
:type 'list
:group 'slack)
(defconst slack-url "https://slack.com/api/")
(defvar slack-team nil
"Current slack team name.")
(defvar slack-token nil
"Current slack token.")
(defvar slack-state
'((:top . nil)
(:teams . nil))
"Current state of client.")
(defvar slack-data (make-hash-table)
"Most recent data queried from server.")
(defvar slack-data-new nil
"Newer server data to process.")
(defmacro slack-json-true (val)
"Return non-nil if val is not `eq' to ':json_false',
otherwise return nil."
`(not (eq ,val :slack-json-false)))
(defmacro slack-json-false (val)
"Return non-nil if val is `eq' to ':json_false',
otherwise return nil."
`(eq ,val :slack-json-false))
(defmacro slack-generate-header (text)
"Return TEXT as a header."
`(propertize ,text 'face 'bold))
(defun slack-query (callback type method &optional args)
"Call CALLBACK with resulting JSON from submitting ARGS to slack METHOD."
(let* ((url-request-method "GET")
(args (concat "?token=" (url-hexify-string slack-token)
(mapconcat (lambda (arg)
(concat (url-hexify-string (car arg))
"="
(url-hexify-string (cdr arg))))
args
"&")))
(query (concat slack-url method args))
url-http-end-of-headers)
(url-retrieve query `(lambda (status)
(goto-char url-http-end-of-headers)
(funcall (function ,callback) ,type (json-read))))))
(defun slack-data-new-set (type json)
"Set data TYPE to JSON."
(push (cons type json) slack-data-new)
(when (slack-load-data-sets)
(slack-refresh)))
(defun slack-load-data-sets ()
"Process new data sets and return non-nil if a change was made."
(let (change)
(while (plusp (length slack-data-new))
(let ((new (pop slack-data-new)))
(let ((cur (gethash (car new) slack-data)))
(when (cl-set-difference new cur)
(setf (gethash (car new) slack-data) (cdr new)
change t)))))
change))
(defun slack-auth-list (&optional team token)
"Return list of authorizations (team token pairs)."
(let* ((dups-list (append (and team token (cons team token))
(and slack-auth (car slack-auth) (caar slack-auth) slack-auth)
(and (file-exists-p slack-auth-file-name) (load slack-auth-file-name)
slack-auth (car slack-auth) (caar slack-auth) slack-auth)))
(auth-list (cl-do ((dups dups-list (cdr dups))
(auth-list))
((not dups) auth-list)
(unless (member (caar dups) (mapcar #'car auth-list))
(push (car dups) auth-list)))))
(nreverse auth-list)))
(defun slack-switch-team (&optional team token)
"Switch to TEAM using TOKEN.
\nTEAM defaults to `slack-team'.
TOKEN will be looked up in `slack-auth-list' if not given."
(let* ((team (or team slack-team))
(token (or token (cdr (assoc team (slack-auth-list))) slack-token)))
(when (or (not (equal team slack-team))
(not (equal token slack-token)))
(setq slack-team team
slack-token token))
(setcdr (assq :teams slack-state) nil) (slack-group-list)
(slack-channel-list)))
(defun slack-refresh ()
"Display data and interface according to `slack-state'."
(save-excursion
(cl-labels ((widget-create-team (team token &optional spaces)
(widget-insert (make-string (or spaces 0) ? ))
(widget-create 'push-button
:value (format "%s" team)
:notify `(lambda (&rest ignore)
(slack-switch-team ,team ,token)))
(widget-insert "\n"))
(widget-create-channel (name &optional spaces)
(widget-insert (make-string (or spaces 0) ? ))
(widget-create 'push-button
:value (format "%s" name)
:notify `(lambda (&rest ignore)
(message "Channel: %s" ,name)))
(widget-insert "\n")))
(when (get-buffer slack-buffer-name)
(kill-buffer slack-buffer-name))
(let ((buffer (get-buffer-create slack-buffer-name))
(channels-json (gethash :channel slack-data)))
(set-buffer buffer)
(if (cdr (assq :teams slack-state))
(progn
(widget-insert (slack-generate-header "Teams"))
(widget-insert "\n\n")
(mapc (lambda (x) (widget-create-team (car x) (cdr x) 2)) (slack-auth-list))
(widget-insert "\n"))
(progn
(widget-create 'push-button
:value slack-team
:notify (lambda (&rest ignore)
(setcdr (assq :teams slack-state) t) (slack-refresh)))
(widget-insert "\n\n")
(widget-insert (slack-generate-header "Channels"))
(widget-insert "\n\n")
(if (slack-json-true (cdr (assoc 'ok channels-json)))
(let* ((channels (cdr (assoc 'channels channels-json)))
(non-member-channels (cl-remove-if (lambda (x) (slack-json-false (cdr (assoc 'is_member x)))) channels))
(member-channels (cl-remove-if (lambda (x) (slack-json-true (cdr (assoc 'is_member x)))) channels)))
(when (and non-member-channels (plusp (length non-member-channels)))
(mapc (lambda (x) (widget-create-channel (cdr (assoc 'name x)) 2)) non-member-channels)
(widget-insert "\n"))
(when (and member-channels (plusp (length member-channels)))
(mapc (lambda (x) (widget-create-channel (cdr (assoc 'name x)) 2)) member-channels)
(widget-insert "\n")))
(widget-insert (format "Error: %s\n\n" (cdr (assoc 'error channels-json)))))))
(widget-create 'push-button
:value "Refresh"
:notify (lambda (&rest ignore)
(slack-switch-team)))
(widget-insert "\n")
(widget-create 'push-button
:value "Customize"
:notify (lambda (&rest ignore)
(customize-group 'slack)))
(use-local-map widget-keymap)
(widget-setup)
(switch-to-buffer buffer)
(goto-char (point-min))
(widget-forward 1)))))
(defun slack-group-list ()
"Fetch list of groups."
(slack-query 'slack-data-new-set :group "groups.list"))
(defun slack-channel-list ()
"Fetch list of channels."
(slack-query 'slack-data-new-set :channel "channels.list"))
(defun slack (&optional team token)
"Start Slack client."
(interactive)
(let* ((auth-list (slack-auth-list team token))
(auth (or (and slack-team slack-token (cons slack-team slack-token))
(and auth-list (car auth-list) (caar auth-list) (cdar auth-list) (car auth-list))
(cons (read-string "Team: ") (read-string "Token: "))))
(team (car auth))
(token (cdr auth)))
(when (or (not (equal team slack-team))
(not (equal token slack-token)))
(setq slack-team team
slack-token token))
(let ((buffer (get-buffer-create slack-buffer-name)))
(set-buffer buffer)
(unless (local-variable-if-set-p 'slack-state)
(make-local-variable 'slack-state))
(unless (local-variable-if-set-p 'slack-data)
(make-local-variable 'slack-data))
(unless (local-variable-if-set-p 'slack-data-new)
(make-local-variable 'slack-data-new))
(slack-switch-team))))
(provide 'slack)