;;; slack-message-formatter.el --- format message text -*- lexical-binding: t; -*- ;; Copyright (C) 2015 yuya.minami ;; Author: yuya.minami ;; 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 . ;;; Commentary: ;; ;;; Code: (require 'eieio) (require 'slack-user) (require 'slack-room) (defface slack-profile-image-face '((t (:background "#fff"))) "Face used to profile image." :group 'slack) (defface slack-message-output-text '((t (:weight normal :height 0.9))) "Face used to text message." :group 'slack) (defface slack-message-output-header '((t (:foreground "#FFA000" :weight bold :height 1.0 :underline t))) "Face used to text message." :group 'slack) (defface slack-message-output-reaction '((t (:box (:line-width 1 :style released-button)))) "Face used to reactions." :group 'slack) (defface slack-message-deleted-face '((t (:strike-through t))) "Face used to deleted message." :group 'slack) (defface slack-attachment-header '((t (:weight bold))) "Face used to shared message header." :group 'slack) (defface slack-attachment-footer '((t (:height 0.8))) "Face used to shared message footer." :group 'slack) (defface slack-attachment-pad '((t (:weight ultra-bold))) "Face used to shared message pad." :group 'slack) (defface slack-attachment-field-title '((t (:weight bold :height 1.0))) "Face used to attachment field title." :group 'slack) (defcustom slack-date-formats '((date_num . "%Y-%m-%d") (date . "%B %d,%Y") (date_short . "%b %d,%Y") (date_long . "%A %B %d,%Y") (date_pretty . "%B %d,%Y") (date_short_pretty . "%b %d,%Y") (date_long_pretty . "%A %B %d,%Y") (time . "%H:%M") (time_secs . "%H:%M:%S")) "Date formats for Slack's date token. this format string passed to `format-time-string' function. see \"Formatting dates\" section in https://api.slack.com/docs/message-formatting" :group 'slack) (defun slack-message-put-header-property (header) (if header (propertize header 'face 'slack-message-output-header))) (defun slack-message-put-text-property (text) (if text (propertize text 'face 'slack-message-output-text))) (defun slack-message-put-hard (text) (if text (propertize text 'hard t))) (defun slack-message-put-deleted-property (text) (if text (propertize text 'face 'slack-message-deleted-face))) (defun slack-message-time-to-string (ts) (when ts (when (stringp ts) (setf ts (string-to-number ts))) (format-time-string "%Y-%m-%d %H:%M:%S" (seconds-to-time ts)))) (defmethod slack-message-header ((m slack-message) team) (slack-message-sender-name m team)) (defmethod slack-message-starred-p ((m slack-message)) (oref m is-starred)) (defmethod slack-message-starred-str ((m slack-message)) (if (slack-message-starred-p m) ":star:" "")) (defun slack-format-message (&rest args) (let ((messages args)) (mapconcat #'identity (cl-remove-if #'(lambda (e) (< (length e) 1)) messages) "\n"))) (defmethod slack-message-profile-image ((m slack-message) team) (slack-user-image (slack-user-find m team) team)) (defmethod slack-message-header-with-image ((m slack-message) header team) (let ((image (slack-message-profile-image m team))) (if image (format "%s %s" (propertize "image" 'display image 'face 'slack-profile-image-face) header) header))) (defun slack-message-header-to-string (m team) (let ((header (format "%s %s" (slack-message-put-header-property (slack-message-header m team)) (slack-message-starred-str m)))) (if (slack-team-display-profile-imagep team) (slack-message-header-with-image m header team) header))) (defmethod slack-message-body-to-string ((m slack-message) team) (let ((raw-body (slack-message-body m team))) (if (oref m deleted-at) (slack-message-put-deleted-property raw-body) (slack-message-put-text-property raw-body)))) (defun slack-format-reactions (reactions team) (concat "\n" (mapconcat #'(lambda (r) (slack-reaction-to-string r team)) reactions " "))) (defmethod slack-message-reaction-to-string ((m slack-message) team) (let ((reactions (slack-message-reactions m))) (when reactions (slack-format-reactions reactions team)))) (defmethod slack-message-to-string ((m slack-message) team) (let ((text (if (slot-boundp m 'text) (oref m text)))) (let* ((header (slack-message-header-to-string m team)) (attachment-body (slack-message-attachment-body m team)) (body (slack-message-body-to-string m team)) (files (mapconcat #'(lambda (file) (slack-message-to-string file (slack-ts m) team)) (oref m files) "\n")) (reactions (slack-message-reaction-to-string m team)) (thread (slack-thread-to-string m team))) (slack-format-message header body (if (< 0 (length files)) (format "\n%s" files) files) attachment-body reactions thread)))) (defmethod slack-message-body ((m slack-message) team) (with-slots (text) m (slack-message-unescape-string text team))) (defmethod slack-message-body ((m slack-reply-broadcast-message) team) (format "Replied to a thread: \n%s" (slack-message-unescape-string (oref m text) team))) (defmethod slack-message-body-to-string ((m slack-file-comment-message) team) (with-slots (file comment deleted-at) m (let ((commented-user (slack-user-name (plist-get comment :user) team)) (comment-body (plist-get comment :comment)) (file-id (plist-get file :id)) (file-user (slack-user-name (plist-get file :user) team)) (file-title (plist-get file :title)) (text-propertize (or (and deleted-at #'slack-message-put-deleted-property) #'slack-message-put-text-property))) (format "%s %s: %s" (funcall text-propertize (format "@%s commented on @%s's file" commented-user file-user)) (slack-file-link-info file-id file-title) (funcall text-propertize comment-body))))) (defmethod slack-team-display-image-inlinep ((_m slack-message) team) (slack-team-display-attachment-image-inlinep team)) (defmethod slack-message-attachment-body ((m slack-message) team) (with-slots (attachments) m (let ((body (mapconcat #'(lambda (attachment) (slack-message-to-string attachment team)) attachments "\n\t-\n"))) (if (< 0 (length body)) (slack-message-unescape-string (format "\n%s" body) team))))) (defmethod slack-message-to-alert ((m slack-message) team) (with-slots (text attachments files) m (let ((alert-text (cond ((and text (< 0 (length text))) text) ((and attachments (< 0 (length attachments))) (mapconcat #'slack-attachment-to-alert attachments " ")) ((and files (< 0 (length files))) (mapconcat #'(lambda (file) (oref file title)) files " "))))) (slack-message-unescape-string alert-text team)))) (defun slack-message-unescape-string (text team) (when text (let* ((and-unescpaed (replace-regexp-in-string "&" "&" text)) (lt-unescaped (replace-regexp-in-string "<" "<" and-unescpaed)) (gt-unescaped (replace-regexp-in-string ">" ">" lt-unescaped))) (slack-message-unescape-date-format (slack-message-unescape-command (slack-message-unescape-user-id (slack-message-unescape-channel gt-unescaped team) team)))))) (defun slack-message-unescape-user-id (text team) (let ((user-regexp "<@\\(U.*?\\)>")) (cl-labels ((unescape-user-id (text) (concat "@" (or (slack-message-replace-user-name text) (let ((user (slack-user--find (match-string 1 text) team))) (plist-get user :name)) (match-string 1 text))))) (replace-regexp-in-string user-regexp #'unescape-user-id text t t)))) (defun slack-message-replace-user-name (text) (let ((user-name-regexp "<@U.*?|\\(.*?\\)>")) (cl-labels ((replace-user-id-with-name (text) (match-string 1 text))) (if (string-match-p user-name-regexp text) (replace-regexp-in-string user-name-regexp #'replace-user-id-with-name text nil t))))) (defun slack-message-unescape-date-format (text) (let ((date-regexp "") (time-format-regexp "{\\(.*?\\)}")) (cl-labels ((unescape-date-string (text) (let* ((time (match-string 1 text)) (format-string (match-string 2 text)) (link (match-string 3 text)) (fallback (match-string 4 text))) (replace-regexp-in-string time-format-regexp #'(lambda (text) (unescape-datetime-format time link text fallback)) format-string))) (unescape-datetime-format (unix-time link text fallback) (let* ((match (match-string 1 text)) (template (cl-assoc (intern match) slack-date-formats))) (if template (slack-linkfy (format-time-string (cdr template) (float-time (string-to-number unix-time))) (and link (substring link 1 (length link)))) fallback)))) (replace-regexp-in-string date-regexp #'unescape-date-string text nil t)))) (defun slack-message-unescape-command (text) (let ((command-regexp "")) (cl-labels ((unescape-command (text) (let ((match (match-string 1 text))) (if (string-prefix-p "date" match) (format "" match) (concat "@" match))))) (replace-regexp-in-string command-regexp #'unescape-command text nil t)))) (defun slack-message-unescape-channel (text team) (let ((channel-regexp "<#\\(C.*?\\)\\(|.*?\\)?>")) (cl-labels ((unescape-channel (text) (let ((name (match-string 2 text)) (id (match-string 1 text))) (concat "#" (or (and name (substring name 1)) (slack-if-let* ((room (slack-room-find id team))) (oref room name) id)))))) (replace-regexp-in-string channel-regexp #'unescape-channel text t)))) (provide 'slack-message-formatter) ;;; slack-message-formatter.el ends here