tvl-depot/configs/shared/emacs/.emacs.d/elpa/slack-20180712.2222/slack-util.el
William Carroll 17ee0e400b Support Vim, Tmux, Emacs with Stow
After moving off of Meta, Dotfiles has a greater responsibility to
manage configs. Vim, Tmux, and Emacs are now within Stow's purview.
2018-09-10 14:53:23 -04:00

471 lines
19 KiB
EmacsLisp
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

;;; slack-util.el ---utility functions -*- lexical-binding: t; -*-
;; Copyright (C) 2015 yuya.minami
;; Author: yuya.minami <yuya.minami@yuyaminami-no-MacBook-Pro.local>
;; Keywords:
;; This program is free software; you can redistribute it and/or modify
;; it under the terms of the GNU General Public License as published by
;; the Free Software Foundation, either version 3 of the License, or
;; (at your option) any later version.
;; This program is distributed in the hope that it will be useful,
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
;; GNU General Public License for more details.
;; You should have received a copy of the GNU General Public License
;; along with this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;;
;;; Code:
(require 'eieio)
(require 'timer)
(require 'diary-lib)
(defcustom slack-profile-image-file-directory temporary-file-directory
"Default directory for slack profile images."
:group 'slack)
(defcustom slack-image-file-directory temporary-file-directory
"Default directory for slack images."
:group 'slack)
(defcustom slack-image-max-height 300
"Max Height of image. nil is unlimited. integer."
:group 'slack)
(defconst slack-log-levels
'(;; debugging
(trace . 40) (debug . 30)
;; information
(info . 20)
;; errors
(warn . 10) (error . 0))
"Named logging levels.")
(defcustom slack-log-level 'info
"Used in `slack-message-logger'.
One of 'info, 'debug"
:group 'slack)
(defcustom slack-log-time-format
"[%Y-%m-%d %H:%M:%S]"
"Time format for log."
:group 'slack)
(defalias 'slack-if-let*
(if (fboundp 'if-let*)
'if-let*
'if-let))
(defun slack-seq-to-list (seq)
(if (listp seq) seq (append seq nil)))
(defun slack-decode (seq)
(cl-loop for e in (slack-seq-to-list seq)
collect (if (stringp e)
(decode-coding-string e 'utf-8)
(if (listp e)
(slack-decode e)
e))))
(defun slack-class-have-slot-p (class slot)
(and (symbolp slot)
(let* ((stripped (substring (symbol-name slot) 1))
(replaced (replace-regexp-in-string "_" "-"
stripped))
(symbolized (intern replaced)))
(slot-exists-p class symbolized))))
(defun slack-collect-slots (class seq)
(let ((plist (slack-seq-to-list seq)))
(cl-loop for p in plist
if (and (slack-class-have-slot-p class p)
(plist-member plist p))
nconc (let ((value (plist-get plist p)))
(list p (if (stringp value)
(decode-coding-string value 'utf-8)
(if (eq :json-false value)
nil
value)))))))
(defun slack-log-level-to-int (level)
(slack-if-let* ((cell (cl-assoc level slack-log-levels)))
(cdr cell)
20))
(defun slack-message-logger (message level team)
"Display message using `message'."
(let ((user-level (slack-log-level-to-int slack-log-level))
(current-level (slack-log-level-to-int level)))
(when (<= current-level user-level)
(message (format "%s [%s] [%s] %s"
(format-time-string slack-log-time-format)
level
(oref team name)
message)))))
(cl-defun slack-log (msg team &key
(logger #'slack-message-logger)
(level 'debug))
(let ((log (format "%s [%s] %s - %s"
(format-time-string slack-log-time-format)
level
msg
(oref team name)))
(buf (get-buffer-create (slack-log-buffer-name team))))
(when (functionp logger)
(funcall logger msg level team))
(with-current-buffer buf
(setq buffer-read-only nil)
(save-excursion
(goto-char (point-max))
(insert log)
(insert "\n"))
(setq buffer-read-only t))))
(defun company-slack-backend (command &optional arg &rest ignored)
"Completion backend for slack chats. It currently understands
@USER; adding #CHANNEL should be a simple matter of programming."
(interactive (list 'interactive))
(cl-labels
((start-from-line-beginning (str)
(let ((prompt-length (length lui-prompt-string)))
(>= 0 (- (current-column) prompt-length (length str)))))
(prefix-type (str) (cond
((string-prefix-p "@" str) 'user)
((string-prefix-p "#" str) 'channel)
((and (string-prefix-p "/" str)
(start-from-line-beginning str))
'slash)))
(content (str) (substring str 1 nil)))
(cl-case command
(interactive (company-begin-backend 'company-slack-backend))
(prefix (when (string= "slack" (car (split-string (format "%s" major-mode) "-")))
;; (cl-find major-mode '(slack-mode
;; slack-edit-message-mode
;; slack-thread-mode))
(company-grab-line "\\(\\W\\|^\\)\\(@\\w*\\|#\\w*\\|/\\w*\\)"
2)))
(candidates (let ((content (content arg)))
(cl-case (prefix-type arg)
(user
(cl-loop for user in (oref slack-current-team users)
if (and (not (eq (plist-get user :deleted) t))
(string-prefix-p content
(plist-get user :name)))
collect (concat "@" (plist-get user :name))))
(channel
(cl-loop for team in (oref slack-current-team channels)
if (string-prefix-p content
(oref team name))
collect (concat "#" (oref team name))))
(slash
(cl-loop for com in slack-slash-commands-available
if (string-prefix-p content com)
collect (concat "/" com))
))))
(doc-buffer
(cl-case (prefix-type arg)
(slash
(company-doc-buffer
(documentation
(slack-slash-commands-find (substring arg 1))
t)))))
)))
(defun slack-get-ts ()
(get-text-property 0 'ts (thing-at-point 'line)))
(defun slack-linkfy (text link)
(if (not (slack-string-blankp link))
(format "<%s|%s>" link text)
text))
(defun slack-string-blankp (str)
(if str
(> 1 (length str))
t))
(defun slack-log-buffer-name (team)
(format "*Slack Log - %s*" (slack-team-name team)))
(defun slack-log-open-buffer ()
(interactive)
(let ((team (slack-team-select)))
(funcall slack-buffer-function (get-buffer-create (slack-log-buffer-name team)))))
(defun slack-event-log-buffer-name (team)
(format "*Slack Event Log - %s*" (slack-team-name team)))
(defun slack-log-websocket-payload (payload team)
(let* ((bufname (slack-event-log-buffer-name team))
(buf (get-buffer-create bufname)))
(when buf
(with-current-buffer buf
(setq buffer-read-only nil)
(save-excursion
(goto-char (point-max))
(insert (format "[%s] %s\n"
(format-time-string "%Y-%m-%d %H:%M:%S")
payload)))
(setq buffer-read-only t)))))
(defun slack-log-open-websocket-buffer ()
(interactive)
(if websocket-debug
(progn
(let* ((team (slack-team-select))
(websocket (oref team ws-conn)))
(if websocket
(funcall slack-buffer-function
(websocket-get-debug-buffer-create websocket))
(error "Websocket is not connected"))))
(error "`websocket-debug` is not t")))
(defun slack-log-open-event-buffer ()
(interactive)
(let* ((team (slack-team-select))
(bufname (slack-event-log-buffer-name team))
(buf (get-buffer bufname)))
(if buf
(funcall slack-buffer-function buf)
(error "No Event Log Buffer"))))
(defun slack-profile-image-path (image-url team)
(expand-file-name
(concat (md5 (concat (slack-team-name team) "-" image-url))
"."
(file-name-extension image-url))
slack-profile-image-file-directory))
(cl-defun slack-image--create (path &key (width nil) (height nil) (max-height nil) (max-width nil))
(let* ((imagemagick-available-p (image-type-available-p 'imagemagick))
(image (apply #'create-image (append (list path (and imagemagick-available-p 'imagemagick) nil)
(if height (list :height height))
(if width (list :width width))
(if max-height
(list :max-height max-height))
(if max-width
(list :max-width max-width))))))
(if imagemagick-available-p
(slack-image-shrink image max-height)
image)))
(defun slack-image-exists-p (image-spec)
(file-exists-p (slack-image-path (car image-spec))))
(defun slack-image-string (spec)
"SPEC: (list URL WIDTH HEIGHT MAX-HEIGHT MAX-WIDTH)"
(if spec
(slack-if-let* ((path (slack-image-path (car spec))))
(if (file-exists-p path)
(slack-mapconcat-images
(slack-image-slice
(slack-image--create path
:width (cadr spec)
:height (caddr spec)
:max-height (cadddr spec)
:max-width (cadr (cdddr spec)))))
(propertize "[Image]" 'slack-image-spec spec))
"")
""))
(defun slack-image-path (image-url)
(and image-url
(expand-file-name
(concat (md5 image-url)
"."
(file-name-extension image-url))
slack-image-file-directory)))
(defun slack-image-slice (image)
(when image
(let* ((line-height 50.0)
(height (or (plist-get (cdr image) :height)
(cdr (image-size image t))))
(line-count (/ height line-height))
(line (/ 1.0 line-count)))
(if (< line-height height)
(cl-loop for i from 0 to (- line-count 1)
collect (list (list 'slice 0 (* line i) 1.0 line)
image))
(list image)))))
(defun slack-image-shrink (image &optional max-height)
(unless (image-type-available-p 'imagemagick)
(error "Need Imagemagick"))
(if max-height
(let* ((data (plist-get (cdr image) :data))
(file (plist-get (cdr image) :file))
(size (image-size image t))
(height (cdr size))
(width (car size))
(h (min height max-height))
(w (if (< max-height height)
(ceiling
(* (/ (float max-height) height)
width))
width)))
(create-image (or file data) 'imagemagick data :height h :width w))
image))
(defun slack-mapconcat-images (images)
(when images
(cl-labels ((sort-images (images)
(let ((compare (if (or (and (eq system-type 'darwin) (< emacs-major-version 26))
(< emacs-major-version 25))
#'>
#'<)))
(cl-sort images compare :key #'(lambda (image) (caddr (car image))))))
(propertize-image (image)
(propertize "image"
'display image
'face 'slack-profile-image-face)))
(mapconcat #'propertize-image (sort-images images) "\n"))))
(cl-defun slack-url-copy-file (url newname &key (success nil) (error nil) (sync nil) (token nil))
(if (executable-find "curl")
(slack-curl-downloader url newname
:success success
:error error
:token token)
(cl-labels
((on-success (&key data &allow-other-keys)
(when (functionp success) (funcall success)))
(on-error (&key error-thrown symbol-status response data)
(message "Error Fetching Image: %s %s %s, url: %s"
(request-response-status-code response)
error-thrown symbol-status url)
(if (file-exists-p newname)
(delete-file newname))
(case (request-response-status-code response)
(403 nil)
(404 nil)
(t (when (functionp error)
(funcall error
(request-response-status-code response)
error-thrown
symbol-status
url)))))
(parser () (mm-write-region (point-min) (point-max)
newname nil nil nil 'binary t)))
(let* ((url-obj (url-generic-parse-url url))
(need-token-p (and url-obj
(string-match-p "slack"
(url-host url-obj))))
(use-https-p (and url-obj
(string= "https" (url-type url-obj)))))
(request
url
:success #'on-success
:error #'on-error
:parser #'parser
:sync sync
:headers (if (and token use-https-p need-token-p)
(list (cons "Authorization" (format "Bearer %s" token)))))))))
(defun slack-render-image (image team)
(let ((buf (get-buffer-create
(format "*Slack - %s Image*" (slack-team-name team)))))
(with-current-buffer buf
(setq buffer-read-only nil)
(erase-buffer)
(if image
(insert (slack-mapconcat-images (slack-image-slice image)))
(insert "Loading Image..."))
(setq buffer-read-only t)
(goto-char (point-min)))
buf))
(defun slack-parse-time-string (time)
"TIME should be one of:
- a string giving todays time like \"11:23pm\"
(the acceptable formats are HHMM, H:MM, HH:MM, HHam, HHAM,
HHpm, HHPM, HH:MMam, HH:MMAM, HH:MMpm, or HH:MMPM;
a period . can be used instead of a colon : to separate
the hour and minute parts);
- a string giving specific date and time like \"1991/03/23 03:00\";
- a string giving a relative time like \"90\" or \"2 hours 35 minutes\"
(the acceptable forms are a number of seconds without units
or some combination of values using units in timer-duration-words);
- a number of seconds from now;"
(if (numberp time)
(setq time (timer-relative-time nil time)))
(if (stringp time)
(let ((secs (timer-duration time)))
(if secs
(setq time (timer-relative-time nil secs)))))
(if (stringp time)
(progn
(let* ((date-and-time (split-string time " "))
(date (and (eq (length date-and-time) 2) (split-string (car date-and-time) "/")))
(time-str (or (and (eq (length date-and-time) 2) (cadr date-and-time))
(car date-and-time)))
(hhmm (diary-entry-time time-str))
(now (or (and date (decode-time
(encode-time 0 0 0
(string-to-number (nth 2 date))
(string-to-number (nth 1 date))
(string-to-number (nth 0 date))
)))
(decode-time))))
(if (>= hhmm 0)
(setq time
(encode-time 0 (% hhmm 100) (/ hhmm 100) (nth 3 now)
(nth 4 now) (nth 5 now) (nth 8 now)))))))
time)
(defmacro slack-merge-list (old-list new-list)
`(cl-loop for n in ,new-list
do (let ((o (cl-find-if #'(lambda (e) (slack-equalp n e))
,old-list)))
(if o (slack-merge o n)
(push n ,old-list)))))
(cl-defun slack-curl-downloader (url name &key (success nil) (error nil) (token nil))
(cl-labels
((sentinel (proc event)
(cond
((string-equal "finished\n" event)
(when (functionp success) (funcall success)))
(t
(let ((status (process-status proc))
(output (with-current-buffer (process-buffer proc)
(buffer-substring-no-properties (point-min)
(point-max)))))
(if (functionp error)
(funcall error status output url name)
(message "Download Failed. STATUS: %s, EVENT: %s, URL: %s, NAME: %s, OUTPUT: %s"
status
event
url
name
output))
(if (file-exists-p name)
(delete-file name))
(delete-process proc))))))
(let* ((url-obj (url-generic-parse-url url))
(need-token-p (and url-obj
(string-match-p "slack" (url-host url-obj))))
(header (or (and token
need-token-p
(string-prefix-p "https" url)
(format "-H 'Authorization: Bearer %s'" token))
""))
(output (format "--output '%s'" name))
(command (format "curl --silent --show-error --fail --location %s %s '%s'" output header url))
(proc (start-process-shell-command "slack-curl-downloader"
"slack-curl-downloader"
command)))
(set-process-sentinel proc #'sentinel))))
(provide 'slack-util)
;;; slack-util.el ends here