Signed-off-by: Sergio Durigan Junior <sergiodj@sergiodj.net>
This commit is contained in:
Sergio Durigan Junior 2024-04-14 22:56:00 -04:00
parent 245d2b702e
commit 21ae96f532
Signed by: sergiodj
GPG key ID: D0EB762865FC5E36

View file

@ -5,11 +5,18 @@
(require 'plz) (require 'plz)
(require 'message) (require 'message)
(require 'rx) (require 'rx)
(require 'markdown-mode)
(defgroup discoursel nil (defgroup discoursel nil
"Discourse TODO") "Discourse TODO")
(defcustom discoursel-instance "https://discourse.ubuntu.com" (defcustom discoursel-instances
'((ubuntu
:url "https://discourse.ubuntu.com"
:name "Ubuntu")
(fedora
:url "https://discussion.fedoraproject.org"
:name "Fedora"))
"The Discourse instance." "The Discourse instance."
:group 'discoursel :group 'discoursel
:type 'string) :type 'string)
@ -55,24 +62,38 @@ posts manually and incrementally."
(add-text-properties p (point) (add-text-properties p (point)
'(pre-tag t face discoursel-code-face)))) '(pre-tag t face discoursel-code-face))))
(defun discoursel--get (endpoint &optional args) (defun discoursel--get (instance endpoint &optional args)
(condition-case plzerror (condition-case plzerror
(plz 'get (concat discoursel-instance "/" endpoint ".json" (let* ((instance-url (plist-get (cdr instance) ':url))
(full-url (concat instance-url "/" endpoint ".json")))
(plz 'get (concat full-url
(when args (when args
(concat "?" (concat "?"
(mapconcat (lambda (elt) (mapconcat (lambda (elt)
(concat (car elt) "=" (cdr elt))) (concat (car elt) "=" (cdr elt)))
args "&")))) args "&"))))
:as #'json-read) :as #'json-read))
(plz-error (plz-error
(error "Could not query endpoint '%s': '%s'" (error "Could not query endpoint '%s': '%s'"
endpoint (plz-error-message plzerror))))) endpoint (plz-error-message plzerror)))))
(defun discoursel--latest-topics () (defun discoursel--get-invisibility-spec (instance &optional section)
(let* ((r (discoursel--get "latest")) (let* ((instance-name (symbol-name (car instance)))
(spec-str (concat "discoursel-visibility-" instance-name
(when section
(concat "-" section)))))
(intern spec-str)))
(defun discoursel--latest-topics (instance)
(let* ((r (discoursel--get instance "latest"))
(instance-symbol (car instance))
(topics (alist-get 'topics (alist-get 'topic_list r))) (topics (alist-get 'topics (alist-get 'topic_list r)))
(users (alist-get 'users r))) (users (alist-get 'users r))
(with-current-buffer (discoursel-get-create-buffer) (initial-point (point)))
(insert (propertize ">> Latest topics" 'invisible (discoursel--get-invisibility-spec instance)))
(insert
(with-temp-buffer
(insert "\n")
(cl-map 'vector (cl-map 'vector
(lambda (topic) (lambda (topic)
(let* ((userid (alist-get 'user_id (aref (alist-get 'posters topic) 0))) (let* ((userid (alist-get 'user_id (aref (alist-get 'posters topic) 0)))
@ -82,22 +103,61 @@ posts manually and incrementally."
users)) users))
(inhibit-read-only t)) (inhibit-read-only t))
(insert (insert
(propertize (format "%s (by %s)\n" (propertize (format " %s (by %s)\n"
(propertize (alist-get 'title topic) (propertize (alist-get 'title topic)
'face 'discoursel-topic-face) 'face 'discoursel-topic-face)
(propertize (alist-get 'name username) (propertize (alist-get 'name username)
'face 'discoursel-username-face)) 'face 'discoursel-username-face))
'discoursel-topic-slug (alist-get 'slug topic) 'discoursel-topic-slug (alist-get 'slug topic)
'discoursel-topic-id (alist-get 'id topic))))) 'discoursel-topic-id (alist-get 'id topic)))))
topics)))) topics)
(put-text-property (point-min) (point-max)
'invisible `(,(discoursel--get-invisibility-spec instance "latest")
,(discoursel--get-invisibility-spec instance)))
(buffer-string)))
(add-text-properties (1- initial-point) (point-max)
`(discoursel-section "latest"
discoursel-instance ,instance-symbol))))
;;;###autoload
(defun discoursel-update () (defun discoursel-update ()
(interactive) (interactive)
(with-current-buffer (discoursel-get-create-buffer) (with-current-buffer (discoursel-get-create-buffer)
(save-excursion (save-excursion
(let ((inhibit-read-only t)) (let ((inhibit-read-only t))
(discoursel--latest-topics))))) (erase-buffer)
(dolist (instance discoursel-instances)
(let* ((instance-symbol (car instance))
(instance-plist (cdr instance))
(instance-name (plist-get instance-plist ':name)))
(insert (propertize (format "💻 %s" instance-name)
'discoursel-instance instance-symbol))
(insert (propertize "\n" 'invisible (discoursel--get-invisibility-spec instance)))
;; (let ((latest-topic-initial-point (point))
;; (latest-topic-final-point))
;; (insert ">> Latest topics")
;; (setq latest-topic-final-point (point))
;; (insert "\n")
(discoursel--latest-topics instance)
(insert "\n")))
(kill-line -2)))))
; (put-text-property latest-topic-final-point (point) 'invisible 'latest-topics))))))
;; (put-text-property latest-topic-initial-point
;; latest-topic-final-point
;; 'overlay (make-overlay (1+ latest-topic-final-point) (point-max))))))))
(defun discoursel-toggle-heading ()
(interactive)
(when-let* ((instance-to-toggle (get-text-property (point) 'discoursel-instance))
(instance (assoc instance-to-toggle discoursel-instances)))
(let* ((section-to-toggle (get-text-property (point) 'discoursel-section))
(spec (cons (discoursel--get-invisibility-spec instance
section-to-toggle)
t))
(invisible-p (and (listp buffer-invisibility-spec)
(member spec buffer-invisibility-spec))))
(if invisible-p
(remove-from-invisibility-spec spec)
(add-to-invisibility-spec spec)))))
(defun discoursel--get-name-or-username (post) (defun discoursel--get-name-or-username (post)
(let ((name (alist-get 'display_username post))) (let ((name (alist-get 'display_username post)))
@ -125,11 +185,11 @@ characters."
(interactive) (interactive)
(let ((pos (point))) (let ((pos (point)))
(save-excursion (save-excursion
(setq pos (next-single-char-property-change pos 'post-id)) (setq pos (next-single-char-property-change pos 'discoursel-post-id))
(goto-char pos) (goto-char pos)
(while (and (not (eobp)) (while (and (not (eobp))
(not (looking-at-p (rx line-start "Author:")))) (not (looking-at-p (rx line-start "Author:"))))
(setq pos (next-single-char-property-change pos 'post-id)) (setq pos (next-single-char-property-change pos 'discoursel-post-id))
(goto-char pos)) (goto-char pos))
(when (eobp) (when (eobp)
(setq pos nil))) (setq pos nil)))
@ -139,37 +199,28 @@ characters."
(defun discoursel-topic-previous-entry () (defun discoursel-topic-previous-entry ()
(interactive) (interactive)
(let ((pos (point))) (let ((pos (point)))
(setq pos (or (previous-single-property-change pos 'post-id) (setq pos (or (previous-single-property-change pos 'discoursel-post-id)
(point-min))) (point-min)))
(goto-char pos) (goto-char pos)
(while (and (not (bobp)) (while (and (not (bobp))
(not (looking-at-p (rx line-start (or "Author:" "Title:"))))) (not (looking-at-p (rx line-start (or "Author:" "Title:")))))
(setq pos (or (previous-single-property-change pos 'post-id) (setq pos (or (previous-single-property-change pos 'discoursel-post-id)
(point-min))) (point-min)))
(goto-char pos)))) (goto-char pos))))
(defun discoursel--insert-post (post &optional post-title) (defun discoursel--insert-post (instance post &optional post-title)
(let* ((post-slug (alist-get 'topic_slug post)) (let* ((post-slug (alist-get 'topic_slug post))
(post-id (alist-get 'id post)) (post-id (alist-get 'id post))
; (topic-title (alist-get 'title r)) (post-number (alist-get 'post_number post))
(post-date (alist-get 'created_at post)) (post-date (alist-get 'created_at post))
(post-date-string (post-date-string
(format-time-string "%c" (format-time-string "%c"
(encode-time (encode-time
(parse-time-string post-date)))) (parse-time-string post-date))))
(post-author (discoursel--get-name-or-username post)) (post-author-name (discoursel--get-name-or-username post))
(post-html-content (alist-get 'cooked post))) (post-author-username (alist-get 'username post))
(post-html-content (alist-get 'cooked post))
;; (userid (alist-get 'user_id r)) (instance-symbol (car instance)))
;; (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 (save-excursion
(insert (insert
(with-temp-buffer (with-temp-buffer
@ -177,7 +228,7 @@ characters."
(insert (format (propertize "Title: %s\n" 'face 'message-header-name) (insert (format (propertize "Title: %s\n" 'face 'message-header-name)
(propertize post-title 'face 'message-header-subject)))) (propertize post-title 'face 'message-header-subject))))
(insert (format (propertize "Author: %s\n" 'face 'message-header-name) (insert (format (propertize "Author: %s\n" 'face 'message-header-name)
(propertize post-author 'face 'message-header-to))) (propertize post-author-name 'face 'message-header-to)))
(insert (format (propertize "Date: %s\n" 'face 'message-header-name) (insert (format (propertize "Date: %s\n" 'face 'message-header-name)
(propertize post-date-string 'face 'message-header-other))) (propertize post-date-string 'face 'message-header-other)))
(insert "\n") (insert "\n")
@ -193,11 +244,15 @@ characters."
(h5 . discoursel--parse-heading) (h5 . discoursel--parse-heading)
(h6 . discoursel--parse-heading)))) (h6 . discoursel--parse-heading))))
(shr-render-region (point) (point-max))) (shr-render-region (point) (point-max)))
(put-text-property (point-min) (point-max) (add-text-properties (point-min) (point-max)
'post-id post-id) `(discoursel-post-id ,post-id
discoursel-post-number ,post-number
discoursel-post-author-username ,post-author-username
discoursel-post-author-name ,post-author-name
discoursel-instance ,instance-symbol))
(buffer-string)))))) (buffer-string))))))
(defun discoursel-render-posts-from-topic (topic &optional start-idx) (defun discoursel-render-posts-from-topic (instance topic &optional start-idx)
(let* ((posts (alist-get 'posts (alist-get 'post_stream topic))) (let* ((posts (alist-get 'posts (alist-get 'post_stream topic)))
(topic-slug (alist-get 'slug topic)) (topic-slug (alist-get 'slug topic))
(buf (discoursel-get-create-buffer topic-slug)) (buf (discoursel-get-create-buffer topic-slug))
@ -213,24 +268,28 @@ characters."
(insert "<hr>") (insert "<hr>")
(shr-render-region (point-min) (point-max)) (shr-render-region (point-min) (point-max))
(buffer-string))) (buffer-string)))
(discoursel--insert-post (aref posts idx)))))))) (discoursel--insert-post instance (aref posts idx))))))))
(defun discoursel--fetch-all-posts-from-topic (topic-id number-of-pages) (defun discoursel--fetch-all-posts-from-topic (instance topic-id number-of-pages)
(cl-loop for page from 2 to number-of-pages do (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)) (with-delayed-message (2 (format "Rendering page %d out of %d..." page number-of-pages))
(let* ((topic (discoursel--get (concat "t/" topic-id) (let ((topic (discoursel--get instance
`(("page" . ,(number-to-string page))))) (concat "t/" topic-id)
(posts (alist-get 'posts (alist-get 'post_stream topic)))) `(("page" . ,(number-to-string page))))))
(discoursel-render-posts-from-topic topic))))) (discoursel-render-posts-from-topic instance topic)))))
(defun discoursel--open-topic (&optional id) (defun discoursel--open-topic (&optional instance id)
(interactive) (interactive)
(when (and (not (eq major-mode 'discoursel-mode)) (when (and (not (eq major-mode 'discoursel-mode))
(not id)) (not id))
(user-error "You must be in a discoursel buffer")) (user-error "You must be in a discoursel buffer"))
(let* ((topic-id (number-to-string (let* ((instance-name (or instance
(get-text-property (point) 'discoursel-instance)
(user-error "You must select a topic to open")))
(instance-data (assoc instance-name discoursel-instances))
(topic-id (number-to-string
(or id (get-text-property (point) 'discoursel-topic-id)))) (or id (get-text-property (point) 'discoursel-topic-id))))
(topic (discoursel--get (concat "t/" topic-id))) (topic (discoursel--get instance-data (concat "t/" topic-id)))
(posts (alist-get 'posts (alist-get 'post_stream topic))) (posts (alist-get 'posts (alist-get 'post_stream topic)))
(first-post (aref posts 0)) (first-post (aref posts 0))
(topic-slug (alist-get 'slug topic)) (topic-slug (alist-get 'slug topic))
@ -244,16 +303,60 @@ characters."
(save-excursion (save-excursion
(let ((inhibit-read-only t)) (let ((inhibit-read-only t))
(erase-buffer) (erase-buffer)
(discoursel--insert-post first-post topic-title) (discoursel--insert-post instance-data first-post topic-title)
(discoursel-render-posts-from-topic topic 1) (discoursel-render-posts-from-topic instance-data topic 1)
(when (> number-of-pages 1) (when (> number-of-pages 1)
(if discoursel-fetch-all-posts-from-topic (if discoursel-fetch-all-posts-from-topic
(discoursel--fetch-all-posts-from-topic topic-id number-of-pages) (discoursel--fetch-all-posts-from-topic instance-data
(message "TODO")))))))) topic-id
number-of-pages)
(message "TODO")))
(put-text-property (point-min) (point-max)
'discoursel-topic-id (string-to-number topic-id)))))))
(defun discoursel-open-topic (id) (defun discoursel-reply-to-post (&optional arg)
(interactive "nTopic ID: ") (interactive "P")
(discoursel--open-topic id)) (let* ((post-id (or (get-text-property (point) 'discoursel-post-id)
(user-error "You must choose a post to reply to")))
(instance (assoc (get-text-property (point) 'discoursel-instance)
discoursel-instances))
(buf (discoursel-get-create-buffer (number-to-string post-id)))
(original-topic-point (point))
(original-topic-buffer (current-buffer)))
(switch-to-buffer-other-window buf)
(with-current-buffer buf
(when arg
(let ((post-raw-contents
(alist-get 'raw
(discoursel--get instance (concat "posts/"
(number-to-string post-id)))))
(post-author-username (get-text-property original-topic-point
'discoursel-post-author-username
original-topic-buffer))
(post-number (get-text-property original-topic-point
'discoursel-post-number
original-topic-buffer))
(topic-id (get-text-property original-topic-point
'discoursel-topic-id
original-topic-buffer)))
(insert (format "[quote=\"%s, post:%d, topic:%d\"]\n"
post-author-username
post-number
topic-id))
(insert post-raw-contents)
(goto-char (point-min))
;; We can't quote the [quote...] tag.
(next-line)
(while (re-search-forward (rx line-start) nil t)
(replace-match "> " t t))
(goto-char (point-max))
(insert "\n\n")))
(discoursel-post-reply-mode))
(message "Type C-c C-c to post reply, C-c C-k to discard it")))
(defun discoursel-open-topic (instance id)
(interactive "nInstance: \nnTopic ID: ")
(discoursel--open-topic instance id))
(defun discoursel-get-create-buffer (&optional name) (defun discoursel-get-create-buffer (&optional name)
(let* ((bufname (concat "*discoursel" (let* ((bufname (concat "*discoursel"
@ -290,6 +393,7 @@ characters."
(define-key map "p" #'previous-line) (define-key map "p" #'previous-line)
(define-key map (kbd "RET") #'discoursel--open-topic) (define-key map (kbd "RET") #'discoursel--open-topic)
(define-key map "j" #'discoursel-open-topic) (define-key map "j" #'discoursel-open-topic)
(define-key map (kbd "TAB") #'discoursel-toggle-heading)
(define-key map "q" #'discoursel-exit) (define-key map "q" #'discoursel-exit)
(define-key map "g" #'discoursel-update))) (define-key map "g" #'discoursel-update)))
"Keymap for `discoursel-mode'.") "Keymap for `discoursel-mode'.")
@ -302,9 +406,16 @@ characters."
(define-key map "p" #'previous-line) (define-key map "p" #'previous-line)
(define-key map (kbd "M-n") #'discoursel-topic-next-entry) (define-key map (kbd "M-n") #'discoursel-topic-next-entry)
(define-key map (kbd "M-p") #'discoursel-topic-previous-entry) (define-key map (kbd "M-p") #'discoursel-topic-previous-entry)
(define-key map (kbd "C-c C-r") #'discoursel-reply-to-post)
(define-key map "q" #'discoursel-kill-buffer))) (define-key map "q" #'discoursel-kill-buffer)))
"Keymap for `discoursel-topic-mode'.") "Keymap for `discoursel-topic-mode'.")
(defvar discoursel-post-reply-mode-map
(let ((map (make-sparse-keymap)))
(prog1 map
(define-key map (kbd "C-c C-k") #'kill-buffer-and-window)))
"Keymap for `discoursel-post-reply-mode.")
(define-derived-mode discoursel-mode special-mode "Discourse client" (define-derived-mode discoursel-mode special-mode "Discourse client"
"Major mode for interacting with Discourse instances. "Major mode for interacting with Discourse instances.
\\{discoursel-mode-map}" \\{discoursel-mode-map}"
@ -315,12 +426,18 @@ characters."
\\{discoursel-topic-mode-map}" \\{discoursel-topic-mode-map}"
(use-local-map discoursel-topic-mode-map)) (use-local-map discoursel-topic-mode-map))
(define-derived-mode discoursel-post-reply-mode markdown-mode "Discoursel post reply"
"Major mode for replying to Discourse posts.
\\{discoursel-post-reply-mode-map}"
(use-local-map discoursel-post-reply-mode-map))
(defun discoursel () (defun discoursel ()
"Enter discoursel." "Enter discoursel."
(interactive) (interactive)
(switch-to-buffer (discoursel-get-create-buffer)) (switch-to-buffer (discoursel-get-create-buffer))
(unless (eq major-mode 'discoursel-mode) (unless (eq major-mode 'discoursel-mode)
(discoursel-mode)) (discoursel-mode))
(setq buffer-invisibility-spec nil)
(discoursel-update)) (discoursel-update))