;;; -*- lexical-binding: t; -*- (require 'dash) (require 'dash-functional) (require 'request) ;;; ;;; Configuration ;;; (defvar slack/token nil "Legacy (https://api.slack.com/custom-integrations/legacy-tokens) access token") (defvar slack/include-public-channels 't "Whether or not to inclue public channels in the list of conversations") (defvar slack/include-private-channels 't "Whether or not to inclue public channels in the list of conversations") (defvar slack/include-im 't "Whether or not to inclue IMs (private messages) in the list of conversations") (defvar slack/include-mpim nil "Whether or not to inclue multi-person IMs (multi-person private messages) in the list of conversations") ;;; ;;; Utilities ;;; (defmacro comment (&rest _body) "Comment out one or more s-expressions" nil) (defun ->list (vec) (append vec nil)) (defun json-truthy? (x) (and x (not (equal :json-false x)))) ;;; ;;; Generic API integration ;;; (defvar slack/base-url "https://slack.com/api") (defun slack/get (path params &optional callback) "params is an alist of query parameters" (let* ((params-callback (if (functionp params) `(() . ,params) (cons params callback))) (params (car params-callback)) (callback (cdr params-callback)) (params (append `(("token" . ,slack/token)) params)) (url (concat (file-name-as-directory slack/base-url) path))) (request url :type "GET" :params params :parser 'json-read :success (cl-function (lambda (&key data &allow-other-keys) (funcall callback data)))))) (defun slack/post (path params &optional callback) (let* ((params-callback (if (functionp params) `(() . ,params) (cons params callback))) (params (car params-callback)) (callback (cdr params-callback)) (url (concat (file-name-as-directory slack/base-url) path))) (request url :type "POST" :data (json-encode params) :headers `(("Content-Type" . "application/json") ("Authorization" . ,(format "Bearer %s" slack/token))) :success (cl-function (lambda (&key data &allow-other-keys) (funcall callback data)))))) ;;; ;;; Specific API endpoints ;;; ;; Users (defun slack/users (cb) "Returns users as (id . name) pairs" (slack/get "users.list" (lambda (data) (->> data (assoc-default 'members) ->list (-map (lambda (user) (cons (assoc-default 'id user) (assoc-default 'real_name user)))) (-filter #'cdr) (funcall cb))))) (comment (slack/get "users.list" (lambda (data) (setq response-data data))) (slack/users (lambda (data) (setq --users data))) ) ;; Conversations (defun slack/conversation-types () (->> (list (when slack/include-public-channels "public_channel") (when slack/include-private-channels "private_channel") (when slack/include-im "im") (when slack/include-mpim "mpim")) (-filter #'identity) (s-join ","))) (defun channel-label (chan users-alist) (cond ((json-truthy? (assoc-default 'is_channel chan)) (format "#%s" (assoc-default 'name chan))) ((json-truthy? (assoc-default 'is_im chan)) (let ((user-id (assoc-default 'user chan))) (format "Private message with %s" (assoc-default user-id users-alist)))) ((json-truthy? (assoc-default 'is_mpim chan)) (->> chan (assoc-default 'purpose) (assoc-default 'value))))) (defun slack/conversations (cb) "Calls `cb' with (id . '((label . \"label\") '(topic . \"topic\") '(purpose . \"purpose\"))) pairs" (slack/get "conversations.list" `(("types" . ,(slack/conversation-types)) ("exclude-archived" . "true")) (lambda (data) (setq --data data) (slack/users (lambda (users) (->> data (assoc-default 'channels) ->list (-map (lambda (chan) (cons (assoc-default 'id chan) `((label . ,(channel-label chan users)) (topic . ,(->> chan (assoc-default 'topic) (assoc-default 'value))) (purpose . ,(->> chan (assoc-default 'purpose) (assoc-default 'value))))))) (funcall cb))))))) (comment (slack/get "conversations.list" '(("types" . "public_channel,private_channel,im,mpim")) (lambda (data) (setq response-data data))) (slack/get "conversations.list" '(("types" . "im")) (lambda (data) (setq response-data data))) (slack/conversations (lambda (convos) (setq --conversations convos))) ) ;; Messages (cl-defun slack/post-message (&key text channel-id (on-success #'identity)) (slack/post "chat.postMessage" `((text . ,text) (channel . ,channel-id) (as_user . t)) on-success)) (comment (slack/post-message :text "hi slackbot" :channel-id slackbot-channel-id :on-success (lambda (data) (setq resp data))) ) ;;; ;;; Posting code snippets to slack ;;; (defun prompt-for-channel (cb) (slack/conversations (lambda (conversations) (ivy-read "Select channel: " ;; TODO want to potentially use purpose / topic stuff here (->> conversations (-filter (lambda (c) (assoc-default 'label (cdr c)))) (-map (lambda (chan) (let ((label (assoc-default 'label (cdr chan))) (id (car chan))) (propertize label 'channel-id id))))) :history 'slack/channel-history :action (lambda (selected) (let ((channel-id (get-text-property 0 'channel-id selected))) (funcall cb channel-id) (message "Sent message to %s" selected)))))) nil) (comment (prompt-for-channel #'message) (->> --convos (-filter (lambda (c) (assoc-default 'label (cdr c)))) (-map (lambda (chan) (let ((label (assoc-default 'label (cdr chan))) (id (car chan))) (propertize label 'channel-id id))))) (->> --convos (car) (cdr) (assoc-default 'label)) ) (defun slack-send-code-snippet (&optional snippet-text) (interactive (list (buffer-substring-no-properties (mark) (point)))) (prompt-for-channel (lambda (channel-id) (slack/post-message :text (format "```\n%s```" snippet-text) :channel-id channel-id)))) (provide 'slack-snippets)