discoursel/discoursel.el

352 lines
12 KiB
EmacsLisp
Raw Normal View History

;;; Code:
(require 'json)
(require 'plz)
(require 'message)
(require 'rx)
(defgroup discoursel nil
"Discourse TODO")
(defcustom discoursel-instance "https://discourse.ubuntu.com"
"The Discourse instance."
:group 'discoursel
:type 'string)
(defcustom discoursel-fetch-all-posts-from-topic t
"Whether to fetch all posts from a certain topic.
If t, the default, all posts are fetched (which may take a while
depending on the topic's length).
If nil, pagination is enabled and the user needs to ask for more
posts manually and incrementally."
:group 'discoursel
:type 'boolean)
(defvar discoursel-buffer-list nil
"List of discoursel buffers.")
(defface discoursel-topic-face
'((((class color) (background light)) (:foreground "#000"))
(((class color) (background dark)) (:foreground "#fff")))
"Face used for topics."
:group 'discoursel)
(defface discoursel-username-face
'((((class color) (background light)) (:foreground "#aa0"))
(((class color) (background dark)) (:foreground "#ff0")))
"Face used for usernames."
:group 'discoursel)
(defface discoursel-code-face
'((((class color) (background light)) (:background "#cdcecc" :extend t))
(((class color) (background dark)) (:background "#17211c" :extend t)))
"Face used for code blocks."
:group 'discoursel)
(defun discoursel--parse-pre (tag)
(let ((p (point))
(shr-folding-mode 'none))
(shr-ensure-newline)
(shr-generic tag)
(shr-ensure-newline)
(add-text-properties p (point)
'(pre-tag t face discoursel-code-face))))
(defun discoursel--get (endpoint &optional args)
(condition-case plzerror
(plz 'get (concat discoursel-instance "/" endpoint ".json"
(when args
(concat "?"
(mapconcat (lambda (elt)
(concat (car elt) "=" (cdr elt)))
args "&"))))
:as #'json-read)
(plz-error
(error "Could not query endpoint '%s': '%s'"
endpoint (plz-error-message plzerror)))))
(defun discoursel--latest-topics ()
(let* ((r (discoursel--get "latest"))
(topics (alist-get 'topics (alist-get 'topic_list r)))
(users (alist-get 'users r)))
(with-current-buffer (discoursel-get-create-buffer)
(cl-map 'vector
(lambda (topic)
(let* ((userid (alist-get 'user_id (aref (alist-get 'posters topic) 0)))
(username (seq-find
(lambda (v)
(= (alist-get 'id v) userid))
users))
(inhibit-read-only t))
(insert
(propertize (format "%s (by %s)\n"
(propertize (alist-get 'title topic)
'face 'discoursel-topic-face)
(propertize (alist-get 'name username)
'face 'discoursel-username-face))
'discoursel-topic-slug (alist-get 'slug topic)
'discoursel-topic-id (alist-get 'id topic)))))
topics))))
;;;###autoload
(defun discoursel-update ()
(interactive)
(with-current-buffer (discoursel-get-create-buffer)
(save-excursion
(let ((inhibit-read-only t))
(discoursel--latest-topics)))))
(defun discoursel--get-name-or-username (post)
(let ((name (alist-get 'display_username post)))
(if (and name (not (string-empty-p name)))
name
(alist-get 'username post))))
(defun discoursel--parse-heading (dom)
"Prepend <hN> tag (DOM) with the equivalent number of sharp (#)
characters."
(let* ((idx (1- (length dom)))
;; This is a bit hacky and assumes that the text will always
;; be available as the last item in the list.
(tagname (symbol-name (car dom)))
(level (string-to-number
(string-remove-prefix "h" tagname)))
(sharps (make-string level ?#))
(face (intern (concat "shr-" tagname)))
(possible-text (nth idx dom)))
(when (stringp possible-text)
(setf (nth idx dom) (concat sharps " " possible-text)))
(shr-heading dom face)))
(defun discoursel-topic-next-entry ()
(interactive)
(let ((pos (point)))
(save-excursion
(setq pos (next-single-char-property-change pos 'post-id))
(goto-char pos)
(while (and (not (eobp))
(not (looking-at-p (rx line-start "Author:"))))
(setq pos (next-single-char-property-change pos 'post-id))
(goto-char pos))
(when (eobp)
(setq pos nil)))
(when pos
(goto-char pos))))
(defun discoursel-topic-previous-entry ()
(interactive)
(let ((pos (point)))
(setq pos (or (previous-single-property-change pos 'post-id)
(point-min)))
(goto-char pos)
(while (and (not (bobp))
(not (looking-at-p (rx line-start (or "Author:" "Title:")))))
(setq pos (or (previous-single-property-change pos 'post-id)
(point-min)))
(goto-char pos))))
(defun discoursel--insert-post (post &optional post-title)
(let* ((post-slug (alist-get 'topic_slug post))
(post-id (alist-get 'id post))
; (topic-title (alist-get 'title r))
(post-date (alist-get 'created_at post))
(post-date-string
(format-time-string "%c"
(encode-time
(parse-time-string post-date))))
(post-author (discoursel--get-name-or-username post))
(post-html-content (alist-get 'cooked post)))
;; (userid (alist-get 'user_id r))
;; (participants (alist-get 'participants (alist-get 'details r)))
;; (topic-username (alist-get 'username first-post))
;; (topic-author-alist (seq-find (lambda (p)
;; (string= (alist-get 'username p) topic-username))
;; participants))
;; ; (topic-author (alist-get 'username topic-author-alist))
;; (topic-flairname (alist-get 'name topic-author-alist))
;; (topic-author (concat topic-flairname " ("(alist-get 'username first-post) ")"))
(save-excursion
(insert
(with-temp-buffer
(when post-title
(insert (format (propertize "Title: %s\n" 'face 'message-header-name)
(propertize post-title 'face 'message-header-subject))))
(insert (format (propertize "Author: %s\n" 'face 'message-header-name)
(propertize post-author 'face 'message-header-to)))
(insert (format (propertize "Date: %s\n" 'face 'message-header-name)
(propertize post-date-string 'face 'message-header-other)))
(insert "\n")
(save-excursion
(insert post-html-content))
(let ((shr-use-fonts nil)
(shr-external-rendering-functions
'((pre . discoursel--parse-pre)
(h1 . discoursel--parse-heading)
(h2 . discoursel--parse-heading)
(h3 . discoursel--parse-heading)
(h4 . discoursel--parse-heading)
(h5 . discoursel--parse-heading)
(h6 . discoursel--parse-heading))))
(shr-render-region (point) (point-max)))
(put-text-property (point-min) (point-max)
'post-id post-id)
(buffer-string))))))
(defun discoursel-render-posts-from-topic (topic &optional start-idx)
(let* ((posts (alist-get 'posts (alist-get 'post_stream topic)))
(topic-slug (alist-get 'slug topic))
(buf (discoursel-get-create-buffer topic-slug))
(startidx (or start-idx 0)))
(with-current-buffer buf
(save-excursion
(let ((inhibit-read-only t))
(cl-loop for idx from startidx to (1- (length posts)) do
(goto-char (point-max))
(insert
"\n"
(with-temp-buffer
(insert "<hr>")
(shr-render-region (point-min) (point-max))
(buffer-string)))
(discoursel--insert-post (aref posts idx))))))))
(defun discoursel--fetch-all-posts-from-topic (topic-id number-of-pages)
(cl-loop for page from 2 to number-of-pages do
(with-delayed-message (2 (format "Rendering page %d out of %d..." page number-of-pages))
(let* ((topic (discoursel--get (concat "t/" topic-id)
`(("page" . ,(number-to-string page)))))
(posts (alist-get 'posts (alist-get 'post_stream topic))))
(discoursel-render-posts-from-topic topic)))))
(defun discoursel--open-topic (&optional id)
(interactive)
(when (and (not (eq major-mode 'discoursel-mode))
(not id))
(user-error "You must be in a discoursel buffer"))
(let* ((topic-id (number-to-string
(or id (get-text-property (point) 'discoursel-topic-id))))
(topic (discoursel--get (concat "t/" topic-id)))
(posts (alist-get 'posts (alist-get 'post_stream topic)))
(first-post (aref posts 0))
(topic-slug (alist-get 'slug topic))
(topic-title (alist-get 'title topic))
(buf (discoursel-get-create-buffer topic-slug))
(number-of-pages (ceiling (/ (float (alist-get 'posts_count topic))
(float (alist-get 'chunk_size topic))))))
(switch-to-buffer buf)
(with-current-buffer buf
(discoursel-topic-mode)
(save-excursion
(let ((inhibit-read-only t))
(erase-buffer)
(discoursel--insert-post first-post topic-title)
(discoursel-render-posts-from-topic topic 1)
(when (> number-of-pages 1)
(if discoursel-fetch-all-posts-from-topic
(discoursel--fetch-all-posts-from-topic topic-id number-of-pages)
(message "TODO"))))))))
(defun discoursel-open-topic (id)
(interactive "nTopic ID: ")
(discoursel--open-topic id))
(defun discoursel-get-create-buffer (&optional name)
(let* ((bufname (concat "*discoursel"
(when name (concat "-" name))
"*"))
(buf (get-buffer-create bufname)))
(prog1 buf
(unless (memq buf discoursel-buffer-list)
(add-to-list 'discoursel-buffer-list buf)))))
(defun discoursel-kill-buffer (&optional buf)
(interactive)
(let* ((tmpb (or buf (current-buffer)))
(b (get-buffer tmpb)))
(unless b
(error "Invalid buffer %s" b))
(with-current-buffer b
(unless (derived-mode-p 'discoursel-mode)
(error "Cannot kill buffer %s: not a discoursel buffer" (buffer-name))))
(kill-buffer b)
(setq discoursel-buffer-list (remove b discoursel-buffer-list))))
(defun discoursel-exit ()
(interactive)
(dolist (buf discoursel-buffer-list)
(kill-buffer buf))
(setq discoursel-buffer-list nil))
(defvar discoursel-mode-map
(let ((map (make-sparse-keymap)))
(prog1 map
(suppress-keymap map)
(define-key map "n" #'next-line)
(define-key map "p" #'previous-line)
(define-key map (kbd "RET") #'discoursel--open-topic)
(define-key map "j" #'discoursel-open-topic)
(define-key map "q" #'discoursel-exit)
(define-key map "g" #'discoursel-update)))
"Keymap for `discoursel-mode'.")
(defvar discoursel-topic-mode-map
(let ((map (make-sparse-keymap)))
(prog1 map
(suppress-keymap map)
(define-key map "n" #'next-line)
(define-key map "p" #'previous-line)
(define-key map (kbd "M-n") #'discoursel-topic-next-entry)
(define-key map (kbd "M-p") #'discoursel-topic-previous-entry)
(define-key map "q" #'discoursel-kill-buffer)))
"Keymap for `discoursel-topic-mode'.")
(define-derived-mode discoursel-mode special-mode "Discourse client"
"Major mode for interacting with Discourse instances.
\\{discoursel-mode-map}"
(use-local-map discoursel-mode-map))
(define-derived-mode discoursel-topic-mode discoursel-mode "Discoursel topic"
"Major mode for interacting with Discourse topics.
\\{discoursel-topic-mode-map}"
(use-local-map discoursel-topic-mode-map))
(defun discoursel ()
"Enter discoursel."
(interactive)
(switch-to-buffer (discoursel-get-create-buffer))
(unless (eq major-mode 'discoursel-mode)
(discoursel-mode))
(discoursel-update))
;; TODO
;; (with-current-buffer (get-buffer-create "test-discourse")
;; (insert (plz 'get "https://discourse.ubuntu.com/raw/39765"))
;; (markdown-mode)
;; (setq buffer-read-only t))
;; (with-current-buffer (get-buffer-create "test-discourse4")
;; (let* ((ttt (plz 'get "https://discourse.ubuntu.com/latest.json" :as #'json-read))
;; (topics (alist-get 'topics (alist-get 'topic_list ttt)))
;; (users (alist-get 'users ttt)))
;; (cl-map 'vector
;; (lambda (topic)
;; (let* ((uid (alist-get 'user_id (aref (alist-get 'posters topic) 0)))
;; (user (seq-find (lambda (v) (= (alist-get 'id v) uid))
;; users)))
;; (insert
;; (propertize (format "%s [%s]\n" (alist-get 'title topic) (alist-get 'name user))
;; 'discoursel-slug (alist-get 'slug topic)))))
;; topics)))
(provide 'discoursel)
;;; discoursel.el ends here