;;; slack-thread.el --- -*- lexical-binding: t; -*- ;; Copyright (C) 2017 南優也 ;; Author: 南優也 ;; 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 'lui) (require 'slack-util) (require 'slack-room) (require 'slack-channel) (require 'slack-im) (require 'slack-message) (require 'slack-request) (defvar lui-prompt-string "> ") (defconst all-threads-url "https://slack.com/api/subscriptions.thread.getView") (defconst thread-mark-url "https://slack.com/api/subscriptions.thread.mark") (defcustom slack-thread-also-send-to-room 'ask "Whether a thread message should also be sent to its room. If nil: don't send to the room. If `ask': ask the user every time. Any other non-nil value: send to the room." :type '(choice (const :tag "Never send message to the room." nil) (const :tag "Ask the user every time." ask) (const :tag "Always send message to the room." t))) (define-derived-mode slack-thread-mode slack-mode "Slack - Thread" "" (lui-set-prompt lui-prompt-string) (setq lui-input-function 'slack-thread-message--send)) (defclass slack-thread () ((thread-ts :initarg :thread_ts :initform "") (messages :initarg :messages :initform '()) (has-unreads :initarg :has_unreads :initform nil) (mention-count :initarg :mention_count :initform 0) (reply-count :initarg :reply_count :initform 0) (replies :initarg :replies :initform '()) (active :initarg :active :initform t) (root :initarg :root :type slack-message) (unread-count :initarg :unread_count :initform 0) (last-read :initarg :last_read :initform "0"))) (defmethod slack-thread-messagep ((m slack-message)) (if (and (oref m thread-ts) (not (slack-message-thread-parentp m))) t nil)) (defun slack-thread-start () (interactive) (slack-if-let* ((buf slack-current-buffer)) (slack-buffer-start-thread buf (slack-get-ts)))) (defun slack-thread-message--send (message) (slack-if-let* ((buf slack-current-buffer)) (slack-buffer-send-message buf message))) (defun slack-thread-send-message (room team message thread-ts) (let ((message (slack-message-prepare-links (slack-escape-message message) team)) (broadcast (if (eq slack-thread-also-send-to-room 'ask) (y-or-n-p (format "Also send to %s ? " (slack-room-name room))) slack-thread-also-send-to-room))) (progn (slack-message-inc-id team) (with-slots (message-id sent-message self-id) team (let* ((payload (list :id message-id :channel (oref room id) :reply_broadcast broadcast :thread_ts thread-ts :type "message" :user self-id :text message)) (json (json-encode payload)) (obj (slack-message-create payload team :room room))) (slack-ws-send json team) (puthash message-id obj sent-message)))))) (defun slack-thread-show-or-create () (interactive) (slack-if-let* ((buf slack-current-buffer)) (if (slack-thread-message-buffer-p buf) (error "Already in thread") (slack-buffer-display-thread buf (slack-get-ts))))) (cl-defmethod slack-thread-request-messages ((thread slack-thread) room team &key after-success) (cl-labels ((on-success (&key data &allow-other-keys) (slack-request-handle-error (data "slack-thread-request-messages") (let ((messages (mapcar #'(lambda (payload) (slack-message-create payload team :room room)) (plist-get data :messages)))) (oset thread messages (slack-room-sort-messages (cl-remove-if #'slack-message-thread-parentp messages))))) (if after-success (funcall after-success)))) (slack-request (slack-request-create (slack-room-replies-url room) team :params (list (cons "thread_ts" (oref thread thread-ts)) (cons "channel" (oref room id))) :success #'on-success)))) (defmethod slack-thread-show-messages ((thread slack-thread) room team) (cl-labels ((after-success () (let ((buf (slack-create-thread-message-buffer room team (oref thread thread-ts)))) (slack-buffer-display buf)))) (slack-thread-request-messages thread room team :after-success #'after-success))) (defmethod slack-thread-to-string ((m slack-message) team) (with-slots (thread) m (if thread (let* ((usernames (mapconcat #'identity (cl-remove-duplicates (mapcar #'(lambda (reply) (slack-user-name (plist-get reply :user) team)) (oref thread replies)) :test #'string=) " ")) (text (format "\n%s reply from %s" (oref thread reply-count) usernames))) (propertize text 'face '(:underline t) 'keymap (let ((map (make-sparse-keymap))) (define-key map [mouse-1] #'slack-thread-show-or-create) (define-key map (kbd "RET") #'slack-thread-show-or-create) map))) ""))) (defmethod slack-thread-create ((m slack-message) team &optional payload) (if payload (let ((replies (plist-get payload :replies)) (reply-count (plist-get payload :reply_count)) (unread-count (plist-get payload :unread_count)) (last-read (plist-get payload :last_read))) (make-instance 'slack-thread :thread_ts (oref m ts) :root m :replies replies :reply_count (or reply-count 0) :unread_count (or unread-count 1) :last_read last-read)) (make-instance 'slack-thread :thread_ts (oref m ts) :root m))) (defmethod slack-merge ((old slack-thread) new) (oset old replies (oref new replies)) (oset old reply-count (oref new reply-count)) (oset old unread-count (oref new unread-count))) (defun slack-thread-update-state (payload team) (slack-if-let* ((message-payload (plist-get payload :message)) (thread-ts (plist-get message-payload :thread_ts)) (room (slack-room-find (plist-get payload :channel) team)) (message (slack-room-find-message room thread-ts)) (thread (slack-message-get-thread message team)) (new-thread (slack-thread-create message team message-payload))) (progn (slack-merge thread new-thread) (slack-message-update message team t t)) (message "THREAD_TS: %s, ROOM: %s, MESSAGE: %s THREAD: %s, NEW_THREAD:%s" thread-ts (not (null room)) (not (null message)) (not (null thread)) (not (null new-thread))))) (defmethod slack-thread-equal ((thread slack-thread) other) (and (string-equal (oref thread thread-ts) (oref other thread-ts)) (string-equal (oref (oref thread root) channel) (oref (oref other root) channel)))) (cl-defun slack-thread-get-all (&key (sync nil) (ts nil)) (let ((team (slack-team-select))) (cl-labels ((on-success (&key data &allow-other-keys) (slack-request-handle-error (data "slack-thread-get-all") (let ((threads-data (append (plist-get data :threads) nil)) (total-unread (plist-get data :total_unread_replies)) (more (if (eq :json-false (plist-get data :has_more)) nil t)) (new-count (plist-get data :new_threads_count))) (with-slots (threads) team (with-slots (initializedp total-unread-replies new-threads-count has-more) threads (setq has-more more) (setq initializedp t) (setq total-unread-replies total-unread) (setq new-threads-count new-count) (let ((parents (cl-loop for thread in threads-data collect (slack-message-create (plist-get thread :root_msg) team)))) (mapc #'(lambda (parent) (slack-message-update parent team nil t)) parents)))))))) (slack-request (slack-request-create all-threads-url team :type "POST" :params (list (cons "limit" "10") (cons "current_ts" (or ts (format-time-string "%s")))) :success #'on-success))))) (defmethod slack-thread-title ((thread slack-thread) team) (with-slots (root) thread (let ((room (slack-room-find (oref root channel) team)) (body (slack-message-body root team))) (when room (format "%s - %s" (slack-room-name room) (concat (substring body 0 (min 50 (length body))) "...")))))) (defun slack-thread-select (&optional reload) (interactive) (cl-labels ((load-threads (threads) (slack-thread-get-all :sync t :ts (cl-first (cl-sort (mapcar #'(lambda (thread) (oref thread thread-ts)) threads) #'string<)))) (select-thread (threads team has-more) (let* ((alist (cl-remove-if-not #'(lambda (cons) (car cons)) (mapcar #'(lambda (thread) (let ((title (slack-thread-title thread team))) (and title (cons title thread)))) threads))) (maybe-has-more (if has-more (append alist (list (cons "(load more)" 'load-more))) alist)) (selected (slack-select-from-list (maybe-has-more "Select Thread: ")))) selected)) (collect-thread-parents (messages) (mapcar #'(lambda (m) (oref m thread)) (cl-remove-if #'(lambda (m) (not (slack-message-thread-parentp m))) messages))) (collect-threads (team) (cl-loop for room in (with-slots (groups ims channels) team (append ims groups channels)) append (collect-thread-parents (oref room messages))))) (let* ((team (slack-team-select))) (with-slots (initializedp has-more) (oref team threads) (if (or (not initializedp) has-more) (load-threads (collect-threads team)))) (let ((selected (select-thread (collect-threads team) team nil))) (if (eq selected 'load-more) (slack-thread-select t) (slack-thread-show-messages selected (slack-room-find (oref (oref selected root) channel) team) team)))))) (defmethod slack-thread-delete-message ((thread slack-thread) message) (with-slots (messages reply-count) thread (setq messages (cl-remove-if #'(lambda (e) (string= (oref e ts) (oref message ts))) messages)) (setq reply-count (length messages)))) (defmethod slack-thread-update-mark ((thread slack-thread) room msg team) (with-slots (thread-ts) thread (with-slots (id) room (with-slots (ts) msg (cl-labels ((on-success (&key data &allow-other-keys) (slack-request-handle-error (data "slack-thread-mark")))) (slack-request (slack-request-create thread-mark-url team :params (list (cons "channel" id) (cons "thread_ts" thread-ts) (cons "ts" ts)) :success #'on-success))))))) (defmethod slack-thread-marked ((thread slack-thread) payload) (let ((unread-count (plist-get payload :unread_count)) (last-read (plist-get payload :last_read))) (oset thread unread-count unread-count) (oset thread last-read last-read))) (defun slack-room-unread-threads () (interactive) (slack-if-let* ((buf slack-current-buffer)) (slack-buffer-display-unread-threads buf))) (defmethod slack-thread-update-last-read ((thread slack-thread) msg) (with-slots (ts) msg (oset thread last-read ts))) (provide 'slack-thread) ;;; slack-thread.el ends here