diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
new file mode 100644
index 0000000..59940d1
--- /dev/null
+++ b/.github/workflows/test.yml
@@ -0,0 +1,120 @@
+name: CI
+
+on:
+ pull_request:
+ paths-ignore:
+ - '**.md'
+ - '**.rst'
+ push:
+ paths-ignore:
+ - '**.md'
+ - '**.rst'
+ branches-ignore:
+ - 'master'
+ - 'main'
+
+jobs:
+ build:
+ runs-on: ${{ matrix.os }}
+ strategy:
+ matrix:
+ os: [ubuntu-latest]
+ emacs_version: [27.2, 28.2, 29.2]
+ ruby_version: [2.6]
+
+ steps:
+ - uses: actions/checkout@v2
+
+ - uses: ruby/setup-ruby@v1
+ with:
+ ruby-version: ${{ matrix.ruby_version }}
+
+ - uses: purcell/setup-emacs@master
+ with:
+ version: ${{ matrix.emacs_version }}
+
+ - uses: actions/cache@v2
+ id: cache-cask-packages
+ with:
+ path: .cask
+ key: cache-cask-packages-000
+
+ - uses: actions/cache@v2
+ id: cache-cask-executable
+ with:
+ path: ~/.cask
+ key: cache-cask-executable-000
+
+ - uses: conao3/setup-cask@master
+ if: steps.cache-cask-executable.outputs.cache-hit != 'true'
+ with:
+ version: snapshot
+
+ - name: paths
+ run: |
+ echo "$HOME/local/bin" >> $GITHUB_PATH
+ echo "$HOME/.cask/bin" >> $GITHUB_PATH
+ echo "$HOME/.local/bin" >> $GITHUB_PATH
+ echo "LD_LIBRARY_PATH=$HOME/.local/lib" >> $GITHUB_ENV
+
+ - uses: actions/cache@v2
+ if: startsWith(runner.os, 'Linux')
+ with:
+ path: ~/.cache/rubocop_cache
+ key: ${{ runner.os }}-rubocop
+
+ - uses: actions/cache@v2
+ if: startsWith(runner.os, 'macOS')
+ with:
+ path: ~/Library/Caches/rubocop_cache
+ key: ${{ runner.os }}-rubocop
+
+ - uses: actions/cache@v2
+ with:
+ path: ~/local
+ key: ${{ runner.os }}-local-000
+
+ - uses: actions/cache@v2
+ with:
+ path: ~/.emacs.d
+ key: emacs.d
+
+ - uses: actions/cache@v2
+ with:
+ path: ~/.cask
+ key: cask-000
+
+ - uses: actions/cache@v2
+ with:
+ path: nndiscourse/vendor/bundle
+ key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
+ restore-keys: |
+ ${{ runner.os }}-gems-
+
+ - name: bundler
+ run: |
+ gem install --user-install bundler:2.0.2
+
+ - name: apt-get
+ if: startsWith(runner.os, 'Linux')
+ run: |
+ sudo apt-get -yq update
+ DEBIAN_FRONTEND=noninteractive sudo apt-get -yq install gnutls-bin sharutils gnupg2 dirmngr libreadline-dev libcurl4-openssl-dev
+
+ - name: gnupg
+ if: startsWith(runner.os, 'macOS')
+ run: brew list gnupg &>/dev/null || HOMEBREW_NO_AUTO_UPDATE=1 brew install gnupg
+
+ - name: versions
+ run: |
+ ruby --version
+ rake --version
+ bundle --version
+ curl --version
+ emacs --version
+ gpg --version
+
+ - name: test
+ run: |
+ make test-run
+ make test
diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..3ff6ee3
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,13 @@
+*.elc
+.cask
+nndiscourse-autoloads.el
+.rspec
+bin/
+vendor/
+nndiscourse*.gem
+dist/
+.bundle
+tests/Mail
+tests/News
+tests/.newsrc
+.ecukes*
diff --git a/Cask b/Cask
new file mode 100644
index 0000000..b0f6706
--- /dev/null
+++ b/Cask
@@ -0,0 +1,12 @@
+(source gnu)
+(source melpa)
+
+(package-file "nndiscourse.el")
+(files "nndiscourse.el" ("nndiscourse" "nndiscourse/.ruby-version" "nndiscourse/Gemfile" "nndiscourse/Gemfile.lock" "nndiscourse/nndiscourse.gemspec" "nndiscourse/nndiscourse.thor" "nndiscourse/lib"))
+
+(development
+ (depends-on "ert-runner")
+ (depends-on "package-lint")
+ (depends-on "json-rpc")
+ (depends-on "rbenv")
+ (depends-on "ecukes"))
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..f288702
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,674 @@
+ GNU GENERAL PUBLIC LICENSE
+ Version 3, 29 June 2007
+
+ Copyright (C) 2007 Free Software Foundation, Inc. " nil t))
+ (null (re-search-forward "\\( \\s-*>\\( .*?\\)\\) " nil t)))
+ do (let* ((start (match-beginning 1))
+ (end (match-end 1))
+ (matched (match-string 2)))
+ (perform-replace
+ ".*"
+ (concat " \n"
+ (with-temp-buffer
+ (insert matched)
+ (fill-region (point-min) (point-max))
+ (insert
+ (prog1
+ (cl-subseq (replace-regexp-in-string
+ "\n" " \n"
+ " "))))
+
+(defun nndiscourse-add-entry (hashtb e field)
+ "Add to HASHTB a lookup consisting of entry E's id to its FIELD."
+ (nndiscourse--sethash (plist-get e :id) (plist-get e field) hashtb))
+
+(defsubst nndiscourse--summary-exit ()
+ "Call `gnus-summary-exit' without the hackery."
+ (remove-function (symbol-function 'gnus-summary-exit)
+ (symbol-function 'nndiscourse--score-pending))
+ (gnus-summary-exit)
+ (add-function :after (symbol-function 'gnus-summary-exit)
+ (symbol-function 'nndiscourse--score-pending)))
+
+(deffoo nndiscourse-request-group-scan (group &optional server info)
+ "\\[gnus-group-get-new-news-this-group] from *Group* calls this."
+ (nndiscourse--with-group server group
+ (gnus-message 5 "nndiscourse-request-group-scan: scanning %s..." group)
+ (nndiscourse-request-scan nil server)
+ (gnus-get-unread-articles-in-group
+ (or info (gnus-get-info gnus-newsgroup-name))
+ (gnus-active (gnus-info-group info)))
+ (gnus-message 5 "nndiscourse-request-group-scan: scanning %s...done" group))
+ t)
+
+;; gnus-group-select-group
+;; gnus-group-read-group
+;; gnus-summary-read-group
+;; gnus-summary-read-group-1
+;; gnus-summary-setup-buffer
+;; sets gnus-newsgroup-name
+;; gnus-select-newsgroup
+;; gnus-request-group
+;; nndiscourse-request-group
+(deffoo nndiscourse-request-group (group &optional server _fast _info)
+ (nndiscourse--with-group server group
+ (let* ((num-headers (length (nndiscourse-get-headers server group)))
+ (status (format "211 %d %d %d %s" num-headers
+ (or (nndiscourse--first-article-number server group) 1)
+ (or (nndiscourse--last-article-number server group) 0)
+ group)))
+ (gnus-message 7 "nndiscourse-request-group: %s" status)
+ (nnheader-insert "%s\n" status))
+ t))
+
+(defun nndiscourse--request-item (id server)
+ "Retrieve ID from SERVER as a property list."
+ (let* ((port (nndiscourse-proc-info-port (cdr (assoc server nndiscourse-processes))))
+ (conn (json-rpc-connect nndiscourse-localhost port))
+ (utf-decoder (lambda (x)
+ (decode-coding-string (with-temp-buffer
+ (set-buffer-multibyte nil)
+ (insert x)
+ (buffer-string))
+ 'utf-8))))
+ (add-function :filter-return (symbol-function 'json-read-string) utf-decoder)
+ (unwind-protect
+ (condition-case err (json-rpc conn "get_post" id)
+ (error (gnus-message 3 "nndiscourse--request-item: %s" (error-message-string err))
+ nil))
+ (remove-function (symbol-function 'json-read-string) utf-decoder))))
+
+(defun nndiscourse-get-categories (server)
+ "Query SERVER /categories.json."
+ (seq-filter (lambda (x) (eq json-false (plist-get x :read_restricted)))
+ (let ((cats (funcall #'nndiscourse-rpc-request server "categories")))
+ (when (seqp cats) cats))))
+
+(cl-defun nndiscourse-get-topics (server slug &key (page 0))
+ "Query SERVER /c/SLUG/l/latest.json, optionally for PAGE."
+ (funcall #'nndiscourse-rpc-request server
+ "category_latest_topics"
+ :category_slug slug :page page))
+
+(cl-defun nndiscourse-get-posts (server &key (before 0))
+ "Query SERVER /posts.json for posts before BEFORE."
+ (plist-get (let ((result (funcall #'nndiscourse-rpc-request server
+ "posts" :before before)))
+ (when (listp result) result))
+ :latest_posts))
+
+(defun nndiscourse--number-to-header (server group topic-id post-number)
+ "O(n) search for SERVER GROUP TOPIC-ID POST-NUMBER in headers."
+ (declare (indent defun))
+ (when-let ((headers (nndiscourse-get-headers server group))
+ (found (seq-position
+ headers (cons topic-id post-number)
+ (lambda (plst loc)
+ (cl-destructuring-bind (topic-id* . post-number*) loc
+ (and (= topic-id* (plist-get plst :topic_id))
+ (= post-number* (plist-get plst :post_number))))))))
+ (elt headers found)))
+
+(defun nndiscourse--earliest-header (server group topic-id)
+ "O(n) search for first header satisfying SERVER GROUP TOPIC-ID."
+ (declare (indent defun))
+ (when-let ((headers (nndiscourse-get-headers server group)))
+ (seq-find (lambda (plst) (= topic-id (plist-get plst :topic_id)))
+ headers)))
+
+(defsubst nndiscourse-hash-count (table-or-obarray)
+ "Return number items in TABLE-OR-OBARRAY."
+ (let ((result 0))
+ (nndiscourse--maphash (lambda (&rest _args) (cl-incf result)) table-or-obarray)
+ result))
+
+(defsubst nndiscourse-hash-values (table-or-obarray)
+ "Return right hand sides in TABLE-OR-OBARRAY."
+ (let (result)
+ (nndiscourse--maphash (lambda (_key value) (push value result)) table-or-obarray)
+ result))
+
+(defsubst nndiscourse-hash-keys (table-or-obarray)
+ "Return left hand sides in TABLE-OR-OBARRAY."
+ (let (result)
+ (nndiscourse--maphash (lambda (key _value) (push key result)) table-or-obarray)
+ result))
+
+(defun nndiscourse--incoming (server)
+ "Drink from the SERVER firehose."
+ (interactive)
+ (when (zerop (nndiscourse-hash-count (nndiscourse-by-server server :categories-hashtb)))
+ (nndiscourse-request-list server))
+ (cl-loop
+ with new-posts
+ for page-bottom = 1 then (plist-get (elt posts (1- (length posts))) :id)
+ for posts = (nndiscourse-get-posts server :before (1- page-bottom))
+ until (null posts)
+ do (unless (nndiscourse-by-server server :last-id)
+ (setf (nndiscourse-by-server server :last-id)
+ (1- (plist-get (elt posts (1- (length posts))) :id))))
+ do (cl-do* ((k 0 (1+ k))
+ (plst (and (< k (length posts)) (elt posts k))
+ (and (< k (length posts)) (elt posts k))))
+ ((or (null plst)
+ (<= (plist-get plst :id) (nndiscourse-by-server server :last-id))))
+ (push plst new-posts))
+ until (<= (1- (plist-get (elt posts (1- (length posts))) :id))
+ (nndiscourse-by-server server :last-id))
+ finally
+ (let ((counts (gnus-make-hashtable)))
+ (dolist (plst new-posts)
+ (setf (nndiscourse-by-server server :last-id) (plist-get plst :id))
+ (when-let ((not-deleted (not (plist-get plst :deleted_at)))
+ (type (plist-get plst :post_type))
+ (category-id (plist-get plst :category_id))
+ (group (nndiscourse-get-category server category-id))
+ (full-group (gnus-group-full-name
+ group
+ (cons 'nndiscourse (list server)))))
+ (if-let ((it (plist-get plst :reply_to_post_number)))
+ (nndiscourse-set-ref server
+ (plist-get plst :id)
+ (plist-get (nndiscourse--number-to-header
+ server group
+ (plist-get plst :topic_id) it)
+ :id))
+ (when-let ((it (plist-get (nndiscourse--earliest-header
+ server group
+ (plist-get plst :topic_id))
+ :id)))
+ (nndiscourse-set-ref server (plist-get plst :id) it)))
+ (nndiscourse--replace-hash type (lambda (x) (1+ (or x 0))) counts)
+ (if-let ((info (gnus-get-info full-group)))
+ (progn
+ (unless (gnus-info-read info)
+ (with-suppressed-warnings ((obsolete gnus-range-normalize))
+ (setf (gnus-info-read info)
+ (gnus-range-normalize `(1 . ,(1- (plist-get plst :id)))))))
+ (when-let ((last-number (nndiscourse--last-article-number server group))
+ (next-number (plist-get plst :id))
+ (gap `(,(1+ last-number) . ,(1- next-number))))
+ (when (<= (car gap) (cdr gap))
+ (with-suppressed-warnings ((obsolete gnus-range-normalize)
+ (obsolete gnus-range-add))
+ (setf (gnus-info-read info)
+ (gnus-range-add (gnus-info-read info)
+ (gnus-range-normalize gap))))
+ (when (gnus-info-marks info)
+ (setf (alist-get 'unexist (gnus-info-marks info)) nil)))))
+ (gnus-message 3 "nndiscourse--incoming: cannot update read for %s" group))
+ (nndiscourse-set-headers server group
+ (nconc (nndiscourse-get-headers server group) (list plst)))))
+ (gnus-message
+ 5 (concat "nndiscourse--incoming: "
+ (format "last-id: %s, " (nndiscourse-by-server server :last-id))
+ (let ((result ""))
+ (nndiscourse--maphash
+ (lambda (key value)
+ (setq result (concat result (format "type=%s +%s " key value))))
+ counts)
+ result))))))
+
+(deffoo nndiscourse-request-scan (&optional _group server)
+ (when (nndiscourse-good-server server)
+ (if (> 2 (- (truncate (float-time)) (nndiscourse-by-server server :last-scan-time)))
+ (gnus-message 7 "nndiscourse-request-scan: last scanned at %s"
+ (current-time-string (nndiscourse-by-server server :last-scan-time)))
+ (cl-destructuring-bind (seconds num-gc seconds-gc)
+ (benchmark-run (nndiscourse--incoming server))
+ (setf (nndiscourse-by-server server :last-scan-time) (truncate (float-time)))
+ (gnus-message 5 (concat "nndiscourse-request-scan: Took %s seconds,"
+ " with %s gc runs taking %s seconds")
+ seconds num-gc seconds-gc)))))
+
+(defsubst nndiscourse--make-message-id (id)
+ "Construct a valid Gnus message id from ID."
+ (format "<%s@discourse.org>" id))
+
+(defsubst nndiscourse--make-references (server id)
+ "For SERVER, construct a space delimited string of message ancestors of ID."
+ (mapconcat (lambda (ref) (nndiscourse--make-message-id ref))
+ (nndiscourse-get-refs server id) " "))
+
+(defsubst nndiscourse--make-header (server group article-number)
+ "Construct mail headers from article header.
+For SERVER GROUP article headers, construct mail headers from ARTICLE-NUMBER'th
+article header. Gnus manual does say the term `header` is oft conflated."
+ (when-let ((header (nndiscourse--get-header server group article-number)))
+ (let ((score (plist-get header :score))
+ (reads (plist-get header :reads)))
+ (make-full-mail-header
+ article-number
+ (plist-get header :topic_title)
+ (plist-get header :username)
+ (format-time-string "%a, %d %h %Y %T %z (%Z)" (date-to-time (plist-get header :created_at)))
+ (nndiscourse--make-message-id (plist-get header :id))
+ (nndiscourse--make-references server (plist-get header :id))
+ 0 0 nil
+ (append `((X-Discourse-Name . ,(plist-get header :name)))
+ `((X-Discourse-ID . ,(plist-get header :id)))
+ `((X-Discourse-Permalink . ,(format "%s/t/%s/%s/%s"
+ server
+ (plist-get header :topic_slug)
+ (plist-get header :topic_id)
+ (plist-get header :id))))
+ (and (numberp score)
+ `((X-Discourse-Score . ,(number-to-string (truncate score)))))
+ (and (numberp reads)
+ `((X-Discourse-Reads . ,(number-to-string (truncate reads))))))))))
+
+;; CORS denial
+(defalias 'nndiscourse--request #'ignore)
+
+(deffoo nndiscourse-request-article (article-number &optional group server buffer)
+ (unless buffer (setq buffer nntp-server-buffer))
+ (nndiscourse--with-group server group
+ (with-current-buffer buffer
+ (erase-buffer)
+ (let* ((header (nndiscourse--get-header server group article-number))
+ (mail-header (nndiscourse--make-header server group article-number))
+ (score (cdr (assq 'X-Discourse-Score (mail-header-extra mail-header))))
+ (permalink (cdr (assq 'X-Discourse-Permalink (mail-header-extra mail-header))))
+ (body (nndiscourse--massage (plist-get header :cooked))))
+ (when body
+ (insert
+ "Newsgroups: " group "\n"
+ "Subject: " (mail-header-subject mail-header) "\n"
+ "From: " (or (mail-header-from mail-header) "nobody") "\n"
+ "Date: " (mail-header-date mail-header) "\n"
+ "Message-ID: " (mail-header-id mail-header) "\n"
+ "References: " (mail-header-references mail-header) "\n"
+ "Archived-at: " permalink "\n"
+ "Score: " score "\n"
+ "\n")
+ (mml-insert-multipart "alternative")
+ (mml-insert-tag 'part 'type "text/html"
+ 'disposition "inline"
+ 'charset "utf-8")
+ (save-excursion (mml-insert-tag '/part))
+ (when-let
+ ((parent (car (last (nndiscourse-get-refs server (plist-get header :id)))))
+ (parent-author
+ (or (plist-get (nndiscourse--get-header server group parent)
+ :username)
+ "Someone"))
+ (parent-body (nndiscourse--massage
+ (plist-get
+ (nndiscourse--get-header server group parent)
+ :cooked))))
+ (insert (nndiscourse--citation-wrap parent-author parent-body)))
+ (insert body)
+ (insert "\n")
+ (if (mml-validate)
+ (message-encode-message-body)
+ (gnus-message 2 "nndiscourse-request-article: Invalid mml:\n%s"
+ (buffer-string)))
+ (cons group article-number))))))
+
+(deffoo nndiscourse-retrieve-headers (article-numbers &optional group server _fetch-old)
+ (with-current-buffer nntp-server-buffer
+ (erase-buffer)
+ (nndiscourse--with-group server group
+ (dolist (i article-numbers)
+ (when-let ((header (nndiscourse--make-header server group i)))
+ (nnheader-insert-nov header)))
+ 'nov)))
+
+;; Primarily because `gnus-get-unread-articles' won't update unreads
+;; upon install (nndiscourse won't yet be in type-cache),
+;; I am counting on logic in `gnus-read-active-file-1' in `gnus-get-unread-articles'
+;; to get here upon install.
+(deffoo nndiscourse-retrieve-groups (_groups &optional server)
+ (when (nndiscourse-good-server server)
+ ;; Utterly insane thing where `gnus-active-to-gnus-format' expects
+ ;; `gnus-request-list' output to be in `nntp-server-buffer'
+ ;; and populates `gnus-active-hashtb'
+ (nndiscourse-request-list server)
+ (with-current-buffer nntp-server-buffer
+ (with-suppressed-warnings ((obsolete gnus-select-method))
+ (let (gnus-server-method-cache
+ (gnus-select-method '(nnnil)))
+ (gnus-active-to-gnus-format
+ (gnus-server-to-method (format "nndiscourse:%s" server))
+ gnus-active-hashtb nil t))))
+ (mapc (lambda (group)
+ (let ((full-name (gnus-group-full-name group `(nndiscourse ,server))))
+ (gnus-get-unread-articles-in-group (gnus-get-info full-name)
+ (gnus-active full-name))))
+ (nndiscourse-hash-values (nndiscourse-by-server server :categories-hashtb)))
+ ;; `gnus-read-active-file-2' will now repeat what I just did. Brutal.
+ 'active))
+
+(deffoo nndiscourse-request-list (&optional server)
+ (let ((groups (nndiscourse-hash-values (nndiscourse-by-server server :categories-hashtb))))
+ (when (and (nndiscourse-good-server server) (nndiscourse-server-opened server))
+ (with-current-buffer nntp-server-buffer
+ (unless groups
+ (mapc
+ (lambda (plst)
+ (let ((group (plist-get plst :slug)))
+ (when (and group (not (zerop (length group))))
+ (let* ((category-id (plist-get plst :id))
+ (full-name (gnus-group-full-name group `(nndiscourse ,server)))
+ (subcategory-ids (append (plist-get plst :subcategory_ids) nil))
+ (must-subscribe (not (gnus-get-info full-name))))
+ (erase-buffer)
+ ;; only `gnus-activate-group' seems to call `gnus-parse-active'
+ (gnus-activate-group full-name nil nil `(nndiscourse ,server))
+ (when must-subscribe
+ (funcall (if (fboundp 'gnus-group-set-subscription)
+ #'gnus-group-set-subscription
+ (with-no-warnings
+ #'gnus-group-unsubscribe-group))
+ full-name gnus-level-default-subscribed t))
+ (nndiscourse-set-category server category-id group)
+ (dolist (sub-id subcategory-ids)
+ (nndiscourse-set-category server sub-id group))
+ (push group groups)))))
+ (nndiscourse-get-categories server)))
+ (erase-buffer)
+ (mapc (lambda (group)
+ (insert
+ (format "%s %d %d y\n" group
+ (or (nndiscourse--last-article-number server group) 0)
+ (or (nndiscourse--first-article-number server group) 1))))
+ groups)))
+ t))
+
+(defun nndiscourse-sentinel (process event)
+ "Wipe headers state when PROCESS dies from EVENT."
+ (unless (string= "open" (substring event 0 4))
+ (gnus-message 2 "nndiscourse-sentinel: process %s %s"
+ (car (process-command process))
+ (replace-regexp-in-string "\n$" "" event))
+ (nndiscourse-close-server (process-name process))
+ (gnus-backlog-shutdown)))
+
+(defun nndiscourse--message-user (server beg end _prev-len)
+ "Message SERVER related alert with `buffer-substring' from BEG to END."
+ (let ((string (buffer-substring beg end))
+ (magic "::user::"))
+ (when (string-prefix-p magic string)
+ (message "%s: %s" server (substring string (length magic))))))
+
+;; C-c C-c from followup buffer
+;; message-send-and-exit
+;; message-send
+;; message-send-method-alist=message-send-news-function=message-send-news
+;; gnus-request-post
+;; nndiscourse-request-post
+(deffoo nndiscourse-request-post (&optional _server)
+ nil)
+
+(defun nndiscourse--browse-post (&rest _args)
+ "What happens when I click on discourse Subject."
+ (when-let ((group-article gnus-article-current)
+ (server (nth 1 (gnus-find-method-for-group (car group-article))))
+ (header (nndiscourse--get-header
+ server
+ (gnus-group-real-name (car group-article))
+ (cdr group-article)))
+ (url (format "%s://%s/t/%s/%s/%s"
+ nndiscourse-scheme
+ server
+ (plist-get header :topic_slug)
+ (plist-get header :topic_id)
+ (plist-get header :post_number))))
+ (browse-url url)))
+
+(defun nndiscourse--header-button-alist ()
+ "Construct a buffer-local `gnus-header-button-alist' for nndiscourse."
+ (let* ((result (copy-alist gnus-header-button-alist))
+ (references-value (assoc-default "References" result
+ (lambda (x y) (string-match-p y x))))
+ (references-key (car (rassq references-value result))))
+ (setq result (cl-delete "^Subject:" result :test (lambda (x y) (cl-search x (car y)))))
+ (setq result (cl-delete references-key result :test (lambda (x y) (cl-search x (car y)))))
+ (push (append '("^\\(Message-I[Dd]\\|^In-Reply-To\\):") references-value) result)
+ (push '("^Subject:" ": *\\(.+\\)$" 1 (>= gnus-button-browse-level 0)
+ nndiscourse--browse-post 1)
+ result)
+ result))
+
+(defsubst nndiscourse--fallback-link ()
+ "Cannot render post."
+ (let* ((header (nndiscourse--get-header
+ (nth 1 (gnus-find-method-for-group (car gnus-article-current)))
+ (gnus-group-real-name (car gnus-article-current))
+ (cdr gnus-article-current)))
+ (body (nndiscourse--massage (plist-get header :cooked))))
+ (with-current-buffer gnus-original-article-buffer
+ (article-goto-body)
+ (delete-region (point) (point-max))
+ (insert body))))
+
+(defalias 'nndiscourse--display-article
+ (lambda (article &optional all-headers header)
+ (condition-case-unless-debug err
+ (gnus-article-prepare article all-headers header)
+ (error
+ (if nndiscourse-render-post
+ (progn
+ (gnus-message 7 "nndiscourse--display-article: '%s' (falling back...)"
+ (error-message-string err))
+ (nndiscourse--fallback-link)
+ (gnus-article-prepare article all-headers))
+ (error (error-message-string err))))))
+ "In case of shr failures, dump original link.")
+
+(defun nndiscourse-dump-diagnostics (server)
+ "Makefile recipe test-run. SERVER second element of `gnus-select-method'."
+ (if-let ((it (nndiscourse-alist-get server nndiscourse-processes nil nil #'equal)))
+ (dolist (b `(,byte-compile-log-buffer
+ ,gnus-group-buffer
+ "*Messages*"
+ ,(buffer-name (process-buffer (nndiscourse-proc-info-process it)))
+ ,(format " *%s-stderr*" server)))
+ (when (buffer-live-p (get-buffer b))
+ (princ (format "\nBuffer: %s\n%s\n\n" b (with-current-buffer b (buffer-string)))
+ #'external-debugging-output)))
+ (error "Server %s not found among %s" server (mapcar #'car nndiscourse-processes))))
+
+(defsubst nndiscourse--dense-time (time)
+ "Convert TIME to a floating point number.
+
+Written by John Wiegley (https://github.com/jwiegley/dot-emacs)."
+ (+ (* (car time) 65536.0)
+ (cadr time)
+ (/ (or (car (cdr (cdr time))) 0) 1000000.0)))
+
+(defalias 'nndiscourse--format-time-elapsed
+ (lambda (header)
+ (condition-case nil
+ (let ((date (mail-header-date header)))
+ (if (> (length date) 0)
+ (let*
+ ((then (nndiscourse--dense-time
+ (apply #'encode-time (parse-time-string date))))
+ (now (nndiscourse--dense-time (current-time)))
+ (diff (- now then))
+ (str
+ (cond
+ ((>= diff (* 86400.0 7.0 52.0))
+ (if (>= diff (* 86400.0 7.0 52.0 10.0))
+ (format "%3dY" (floor (/ diff (* 86400.0 7.0 52.0))))
+ (format "%3.1fY" (/ diff (* 86400.0 7.0 52.0)))))
+ ((>= diff (* 86400.0 30.0))
+ (if (>= diff (* 86400.0 30.0 10.0))
+ (format "%3dM" (floor (/ diff (* 86400.0 30.0))))
+ (format "%3.1fM" (/ diff (* 86400.0 30.0)))))
+ ((>= diff (* 86400.0 7.0))
+ (if (>= diff (* 86400.0 7.0 10.0))
+ (format "%3dw" (floor (/ diff (* 86400.0 7.0))))
+ (format "%3.1fw" (/ diff (* 86400.0 7.0)))))
+ ((>= diff 86400.0)
+ (if (>= diff (* 86400.0 10.0))
+ (format "%3dd" (floor (/ diff 86400.0)))
+ (format "%3.1fd" (/ diff 86400.0))))
+ ((>= diff 3600.0)
+ (if (>= diff (* 3600.0 10.0))
+ (format "%3dh" (floor (/ diff 3600.0)))
+ (format "%3.1fh" (/ diff 3600.0))))
+ ((>= diff 60.0)
+ (if (>= diff (* 60.0 10.0))
+ (format "%3dm" (floor (/ diff 60.0)))
+ (format "%3.1fm" (/ diff 60.0))))
+ (t
+ (format "%3ds" (floor diff)))))
+ (stripped
+ (replace-regexp-in-string "\\.0" "" str)))
+ (concat (cond
+ ((= 2 (length stripped)) " ")
+ ((= 3 (length stripped)) " ")
+ (t ""))
+ stripped))))
+ ;; print some spaces and pretend nothing happened.
+ (error " ")))
+ "Return time elapsed since HEADER was sent.
+
+Written by John Wiegley (https://github.com/jwiegley/dot-emacs).")
+
+;; Evade melpazoid!
+(funcall #'fset 'gnus-user-format-function-S
+ (symbol-function 'nndiscourse--format-time-elapsed))
+
+(let ((custom-defaults
+ ;; For now, revert any user overrides that I can't predict.
+ (mapcar (lambda (x)
+ (let* ((var (cl-first x))
+ (sv (get var 'standard-value)))
+ (when (eq var 'gnus-default-adaptive-score-alist)
+ (setq sv (list `(quote
+ ,(mapcar (lambda (entry)
+ (cons (car entry)
+ (assq-delete-all 'from (cdr entry))))
+ (eval (car sv)))))))
+ (cons var sv)))
+ (seq-filter (lambda (x) (eq 'custom-variable (cl-second x)))
+ (append (get 'gnus-score-adapt 'custom-group)
+ (get 'gnus-score-default 'custom-group))))))
+ (add-to-list 'gnus-parameters `("^nndiscourse"
+ ,@custom-defaults
+ (gnus-summary-make-false-root 'adopt)
+ (gnus-cite-hide-absolute 5)
+ (gnus-cite-hide-percentage 0)
+ (gnus-cited-lines-visible '(2 . 2))
+ (gnus-simplify-subject-functions (quote (gnus-simplify-subject-fuzzy)))
+ (gnus-summary-line-format "%3t%U%R%uS %I%(%*%-10,10f %s%)\n")
+ (gnus-auto-extend-newsgroup nil)
+ (gnus-add-timestamp-to-message t)
+ (gnus-summary-display-article-function
+ (quote ,(symbol-function 'nndiscourse--display-article)))
+ (gnus-header-button-alist
+ (quote ,(nndiscourse--header-button-alist)))
+ (gnus-visible-headers ,(concat gnus-visible-headers "\\|^Score:")))))
+
+(defun nndiscourse-article-mode-activate ()
+ "Augment the `gnus-article-mode-map' conditionally."
+ (when (nndiscourse--gate)
+ (nndiscourse-article-mode)))
+
+(defun nndiscourse-summary-mode-activate ()
+ "Shadow some bindings in `gnus-summary-mode-map' conditionally."
+ (when (nndiscourse--gate)
+ (nndiscourse-summary-mode)))
+
+(nnoo-define-skeleton nndiscourse)
+
+(defsubst nndiscourse--who-am-i ()
+ "Get my Discourse username."
+ "dickmao")
+
+;; I believe I did try buffer-localizing hooks, and it wasn't sufficient
+(add-hook 'gnus-article-mode-hook #'nndiscourse-article-mode-activate)
+(add-hook 'gnus-summary-mode-hook #'nndiscourse-summary-mode-activate)
+
+;; `gnus-newsgroup-p' requires valid method post-mail to return t
+(add-to-list 'gnus-valid-select-methods '("nndiscourse" post-mail) t)
+
+(add-function
+ :filter-return (symbol-function 'message-make-fqdn)
+ (lambda (val)
+ (if (and (nndiscourse--gate)
+ (cl-search "--so-tickle-me" val))
+ "discourse.org" val)))
+
+(add-function
+ :before-until (symbol-function 'message-make-from)
+ (lambda (&rest _args)
+ (when (nndiscourse--gate)
+ (concat (nndiscourse--who-am-i) "@discourse.org"))))
+
+;; the let'ing to nil of `gnus-summary-display-article-function'
+;; in `gnus-summary-select-article' dates back to antiquity.
+(add-function
+ :around (symbol-function 'gnus-summary-display-article)
+ (lambda (f &rest args)
+ (cond ((nndiscourse--gate)
+ (let ((gnus-summary-display-article-function
+ (symbol-function 'nndiscourse--display-article)))
+ (apply f args)))
+ (t (apply f args)))))
+
+;; possible impostors
+(setq gnus-valid-select-methods (cl-remove-if (lambda (method)
+ (equal (car method) "nndiscourse"))
+ gnus-valid-select-methods))
+(gnus-declare-backend "nndiscourse" 'post-mail 'address)
+
+(provide 'nndiscourse)
+
+;;; nndiscourse.el ends here
diff --git a/nndiscourse/.gitignore b/nndiscourse/.gitignore
new file mode 100644
index 0000000..134739b
--- /dev/null
+++ b/nndiscourse/.gitignore
@@ -0,0 +1,10 @@
+/.yardoc
+/_yardoc/
+/coverage/
+/doc/
+/pkg/
+/spec/reports/
+/tmp/
+
+# rspec failure tracking
+.rspec_status
diff --git a/nndiscourse/.rubocop.yml b/nndiscourse/.rubocop.yml
new file mode 100644
index 0000000..e2673a5
--- /dev/null
+++ b/nndiscourse/.rubocop.yml
@@ -0,0 +1,13 @@
+AllCops:
+ Exclude:
+ - 'db/**/*'
+ - 'config/**/*'
+ - 'script/**/*'
+ - 'vendor/**/*'
+ - 'bin/**/*'
+ - !ruby/regexp /old_and_unused\.rb$/
+
+Layout/LineLength:
+ Max: 100
+ Exclude:
+ - !ruby/regexp /.*\.gemspec$/
\ No newline at end of file
diff --git a/nndiscourse/.ruby-version b/nndiscourse/.ruby-version
new file mode 100644
index 0000000..097a15a
--- /dev/null
+++ b/nndiscourse/.ruby-version
@@ -0,0 +1 @@
+2.6.2
diff --git a/nndiscourse/Gemfile b/nndiscourse/Gemfile
new file mode 100644
index 0000000..42ae9bf
--- /dev/null
+++ b/nndiscourse/Gemfile
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+source 'https://rubygems.org'
+
+gem 'discourse_api', github: 'dickmao/discourse_api', branch: 'dev'
+gem 'jimson', github: 'dickmao/jimson', branch: 'next'
+
+# Specify your gem's dependencies in nndiscourse.gemspec
+gemspec
diff --git a/nndiscourse/Gemfile.lock b/nndiscourse/Gemfile.lock
new file mode 100644
index 0000000..01c81ed
--- /dev/null
+++ b/nndiscourse/Gemfile.lock
@@ -0,0 +1,98 @@
+GIT
+ remote: https://github.com/dickmao/discourse_api.git
+ revision: f7e79ed525bc65567eba49a838d02c8f4a595318
+ branch: dev
+ specs:
+ discourse_api (0.38.0.pre.dev)
+ faraday (~> 0.9)
+ faraday_middleware (~> 0.10)
+ rack (>= 1.6)
+
+GIT
+ remote: https://github.com/dickmao/jimson.git
+ revision: 22160cf954fdad3d44c4d597b2f47cc7fe58200e
+ branch: next
+ specs:
+ jimson (0.11.0)
+ blankslate (~> 3.1, >= 3.1.3)
+ multi_json (~> 1, >= 1.11.2)
+ rack (~> 2, >= 2.1.4)
+ rest-client (~> 1, >= 1.7.3)
+
+PATH
+ remote: .
+ specs:
+ nndiscourse (0.1.0)
+ jimson (~> 0.11.0)
+ thor (~> 0.20.3)
+
+GEM
+ remote: https://rubygems.org/
+ specs:
+ ast (2.4.0)
+ blankslate (3.1.3)
+ diff-lcs (1.3)
+ domain_name (0.5.20190701)
+ unf (>= 0.0.5, < 1.0.0)
+ faraday (0.17.3)
+ multipart-post (>= 1.2, < 3)
+ faraday_middleware (0.14.0)
+ faraday (>= 0.7.4, < 1.0)
+ http-cookie (1.0.3)
+ domain_name (~> 0.5)
+ jaro_winkler (1.5.4)
+ mime-types (2.99.3)
+ multi_json (1.15.0)
+ multipart-post (2.1.1)
+ netrc (0.11.0)
+ parallel (1.19.1)
+ parser (2.7.0.2)
+ ast (~> 2.4.0)
+ rack (2.2.3)
+ rainbow (3.0.0)
+ rake (13.0.1)
+ rest-client (1.8.0)
+ http-cookie (>= 1.0.2, < 2.0)
+ mime-types (>= 1.16, < 3.0)
+ netrc (~> 0.7)
+ rspec (3.9.0)
+ rspec-core (~> 3.9.0)
+ rspec-expectations (~> 3.9.0)
+ rspec-mocks (~> 3.9.0)
+ rspec-core (3.9.1)
+ rspec-support (~> 3.9.1)
+ rspec-expectations (3.9.0)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.9.0)
+ rspec-mocks (3.9.1)
+ diff-lcs (>= 1.2.0, < 2.0)
+ rspec-support (~> 3.9.0)
+ rspec-support (3.9.2)
+ rubocop (0.79.0)
+ jaro_winkler (~> 1.5.1)
+ parallel (~> 1.10)
+ parser (>= 2.7.0.1)
+ rainbow (>= 2.2.2, < 4.0)
+ ruby-progressbar (~> 1.7)
+ unicode-display_width (>= 1.4.0, < 1.7)
+ ruby-progressbar (1.10.1)
+ thor (0.20.3)
+ unf (0.1.4)
+ unf_ext
+ unf_ext (0.0.7.7)
+ unicode-display_width (1.6.1)
+
+PLATFORMS
+ ruby
+
+DEPENDENCIES
+ bundler (~> 2.0)
+ discourse_api!
+ jimson!
+ nndiscourse!
+ rake (~> 13.0)
+ rspec (~> 3.4)
+ rubocop (~> 0.69)
+
+BUNDLED WITH
+ 2.0.2
diff --git a/nndiscourse/Makefile b/nndiscourse/Makefile
new file mode 100644
index 0000000..fbae043
--- /dev/null
+++ b/nndiscourse/Makefile
@@ -0,0 +1,11 @@
+SHELL := /bin/bash
+REPO := $(shell git rev-parse --show-toplevel)
+
+.PHONY: test-compile
+test-compile:
+ bundle install --quiet
+ bundle exec rake
+
+.PHONY: clean
+clean:
+ @echo Not running bundle clean
diff --git a/nndiscourse/README.md b/nndiscourse/README.md
new file mode 100644
index 0000000..7419bf7
--- /dev/null
+++ b/nndiscourse/README.md
@@ -0,0 +1,39 @@
+# Nndiscourse
+
+Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/nndiscourse`. To experiment with that code, run `bin/console` for an interactive prompt.
+
+TODO: Delete this and the text above, and describe your gem
+
+## Installation
+
+Add this line to your application's Gemfile:
+
+```ruby
+gem 'nndiscourse'
+```
+
+And then execute:
+
+ $ bundle
+
+Or install it yourself as:
+
+ $ gem install nndiscourse
+
+## Usage
+
+TODO: Write usage instructions here
+
+## Development
+
+After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
+
+To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
+
+## Contributing
+
+Bug reports and pull requests are welcome on GitHub at https://github.com/dickmao/nndiscourse.
+
+## License
+
+The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
diff --git a/nndiscourse/Rakefile b/nndiscourse/Rakefile
new file mode 100644
index 0000000..5c857b3
--- /dev/null
+++ b/nndiscourse/Rakefile
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'bundler'
+
+Bundler::GemHelper.install_tasks
+
+require 'bundler/gem_tasks'
+require 'rspec/core/rake_task'
+RSpec::Core::RakeTask.new(:spec)
+
+require 'rubocop/rake_task'
+RuboCop::RakeTask.new(:rubocop)
+
+task test: :spec
+task lint: :rubocop
+task default: %i[spec lint]
diff --git a/nndiscourse/lib/nndiscourse.rb b/nndiscourse/lib/nndiscourse.rb
new file mode 100644
index 0000000..c7e986b
--- /dev/null
+++ b/nndiscourse/lib/nndiscourse.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+require 'nndiscourse/version'
+require 'jimson'
+require 'discourse_api'
+
+module Nndiscourse
+ # This proxies DiscourseApi::Client
+ class Handler < DiscourseApi::Client
+ extend Jimson::Handler
+
+ def initialize(url)
+ super(url)
+ end
+
+ def send(method_name, *params)
+ params.map! do |param|
+ if param.is_a?(Hash)
+ param.each_with_object({}) { |(k, v), h| h[k.to_sym] = v }
+ else
+ param
+ end
+ end
+ super(method_name, *params)
+ end
+ end
+
+ # Process contains a Jimson Server instance
+ class Process
+ def initialize(url, port = 8999)
+ @server = Jimson::Server.new(Handler.new(url), port: port, show_errors: true)
+ @server.start
+ end
+ end
+end
diff --git a/nndiscourse/lib/nndiscourse/version.rb b/nndiscourse/lib/nndiscourse/version.rb
new file mode 100644
index 0000000..6aa6a5c
--- /dev/null
+++ b/nndiscourse/lib/nndiscourse/version.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+module Nndiscourse
+ VERSION = '0.1.0'
+end
diff --git a/nndiscourse/nndiscourse.gemspec b/nndiscourse/nndiscourse.gemspec
new file mode 100644
index 0000000..d8357db
--- /dev/null
+++ b/nndiscourse/nndiscourse.gemspec
@@ -0,0 +1,27 @@
+# frozen_string_literal: true
+
+require File.expand_path('lib/nndiscourse/version', __dir__)
+
+Gem::Specification.new do |spec|
+ spec.name = 'nndiscourse'
+ spec.version = Nndiscourse::VERSION
+ spec.authors = ['dickmao']
+ spec.email = []
+
+ spec.summary = 'API calls from nndiscourse.el to Discourse'
+ spec.homepage = 'https://github.com/dickmao/nndiscourse'
+ spec.license = 'GPLv3'
+
+ spec.files = Dir['lib/**/*.rb']
+ spec.test_files = spec.files.grep(%r{^spec/})
+ spec.executables = spec.files.grep(%r{^bin/}).map { |f| File.basename(f) }
+ spec.require_path = 'lib'
+
+ spec.add_runtime_dependency 'jimson', '~> 0.11.0'
+ spec.add_runtime_dependency 'thor', '~> 0.20.3'
+
+ spec.add_development_dependency 'bundler', '~> 2.0'
+ spec.add_development_dependency 'rake', '~> 13.0'
+ spec.add_development_dependency 'rspec', '~> 3.4'
+ spec.add_development_dependency 'rubocop', '~> 0.69'
+end
diff --git a/nndiscourse/nndiscourse.thor b/nndiscourse/nndiscourse.thor
new file mode 100644
index 0000000..3b3c3dd
--- /dev/null
+++ b/nndiscourse/nndiscourse.thor
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'thor'
+require 'nndiscourse'
+
+# CLI documentation string
+class CLI < Thor
+ desc 'serve URL', 'Run the jimson server'
+ method_option :port, aliases: '-p', desc: 'Port to listen on'
+ def serve(url)
+ Nndiscourse::Process.new(url, options[:port])
+ end
+end
diff --git a/nndiscourse/spec/nndiscourse_spec.rb b/nndiscourse/spec/nndiscourse_spec.rb
new file mode 100644
index 0000000..775596b
--- /dev/null
+++ b/nndiscourse/spec/nndiscourse_spec.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+require 'nndiscourse'
+
+RSpec.describe Nndiscourse do
+ it 'has a version number' do
+ expect(Nndiscourse::VERSION).not_to be nil
+ end
+
+ it 'does something useful' do
+ expect(true).to eq(true)
+ end
+end
diff --git a/nndiscourse/spec/spec_helper.rb b/nndiscourse/spec/spec_helper.rb
new file mode 100644
index 0000000..de02fec
--- /dev/null
+++ b/nndiscourse/spec/spec_helper.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+require 'bundler/setup'
+require 'nndiscourse'
+
+RSpec.configure do |config|
+ # Enable flags like --only-failures and --next-failure
+ config.example_status_persistence_file_path = '.rspec_status'
+
+ # Disable RSpec exposing methods globally on `Module` and `main`
+ config.disable_monkey_patching!
+
+ config.expect_with :rspec do |c|
+ c.syntax = :expect
+ end
+end
diff --git a/scratch.el b/scratch.el
new file mode 100644
index 0000000..1b73ba7
--- /dev/null
+++ b/scratch.el
@@ -0,0 +1,576 @@
+(require 'request)
+(require 'shr)
+
+(require 'simple-httpd)
+
+(require 'json)
+(require 'json-rpc)
+(require 'shr)
+
+;; poor man's
+(let ((result))
+ (request "localhost:8999"
+ :type "POST"
+ :data (json-encode '(("method" . "category_latest_topics") ("jsonrpc" . "2.0") ("params" :category_slug "emacs") ("id" . 1)))
+ :sync t
+ :parser 'json-read
+ :success (cl-function
+ (lambda (&key data &allow-other-keys)
+ (setq result (cdr (assq 'result data))))))
+ result)
+
+;; rich man's
+(add-to-list 'gnus-secondary-select-methods '(nndiscourse "emacs-china.org" (nndiscourse-scheme "https")))
+
+(let ((server "emacs-china.org"))
+ (nndiscourse-open-server server)
+ (seq-filter (lambda (raw) (cl-search "emacs" (cdr raw)))
+ (seq-map (lambda (x) (cons (plist-get x :raw) (plist-get x :topic_title)))
+ (nndiscourse-get-posts server))))
+
+(let* ((server "emacs-china.org")
+ (group "emacs")
+ (headers (nndiscourse-get-headers server group)))
+ (nndiscourse-open-server server)
+ (cons (nndiscourse--first-article-number server group)
+ (nndiscourse--last-article-number server group))
+ (nndiscourse--get-header server group 72124))
+
+(let ((nntp-server-buffer (get-buffer-create "foo")))
+ (nndiscourse-request-list "emacs-china.org"))
+
+(let ((server "emacs-china.org")
+ result)
+ (nndiscourse-open-server server)
+ (with-current-buffer (nndiscourse--server-buffer server)
+ (mapatoms (lambda (k)
+ (push (nndiscourse-get-category server k) result))
+ nndiscourse--categories-hashtb)
+ result))
+
+(let ((server "emacs-china.org")
+ (nndiscourse--last-id nil)
+ (group "emacs"))
+ (nndiscourse-open-server server)
+ (with-current-buffer (nndiscourse--server-buffer server)
+ (mapatoms (lambda (k)
+ (nndiscourse-set-headers server k nil))
+ nndiscourse--headers-hashtb))
+ (nndiscourse--incoming server)
+ (length (nndiscourse-get-headers server group)))
+
+(nndiscourse-get-headers "emacs-china.org" "emacs")
+
+(let ((server "emacs-china.org"))
+ (nndiscourse-open-server server)
+ (nndiscourse--incoming server)
+ (length (nndiscourse-get-headers server "emacs")))
+
+(let ((server "emacs-china.org"))
+ (nndiscourse-get-ref
+ server
+ (plist-get (car (last (nndiscourse-get-headers server))) :id)))
+
+(let ((server "emacs-china.org")
+ result)
+ (length (nndiscourse-get-headers server))
+ (with-current-buffer (nndiscourse--server-buffer server)
+ (nndiscourse--maphash
+ (lambda (key value)
+ (!cons `(,key ,(nndiscourse-get-ref server key)) result))
+ nndiscourse-refs-hashtb))
+ result)
+
+(let ((foo (lambda (f &rest args)
+ (cl-macrolet ((gnus-active (_group) `(cons 1 10)))
+ (apply f args)))))
+ (add-function :around (symbol-function 'gnus-group-insert-group-line-info)
+ foo)
+ (unwind-protect (gnus-group-insert-group-line-info "nndiscourse:emacs")
+ (remove-function (symbol-function 'gnus-group-insert-group-line-info) foo)))
+
+(gnus-group-entry "nndiscourse:emacs")
+
+(gnus-info-read (gnus-get-info "nndiscourse:emacs"))
+
+(let ((foo (lambda (args)
+ (let ((group (car args)))
+ (if (gnus-group-entry group)
+ args
+ (setf (nthcdr 3 args) 10)
+ args))))
+ (group "nndiscourse:emacs"))
+ (add-function :filter-args (symbol-function 'gnus-group-insert-group-line) foo)
+ (unwind-protect
+ (gnus-group-insert-group-line group
+ gnus-level-killed
+ nil
+ 40
+ (gnus-method-simplify
+ (gnus-find-method-for-group group)))
+ (remove-function (symbol-function 'gnus-group-insert-group-line) foo)))
+
+
+
+(nndiscourse--gethash "emacs-china.org" nndiscourse-headers-hashtb)
+
+(defun nndiscourse--get-header (server group article-number)
+ "Amongst SERVER GROUP headers, binary search ARTICLE-NUMBER."
+ (declare (indent defun))
+ (let ((headers (nndiscourse-get-headers server group)))
+ (cl-flet ((id-of (k) (plist-get (elt headers k) :id)))
+ (cl-do* ((x article-number)
+ (l 0 (if (> x m) (1+ m) l))
+ (r (length headers) (if (< x m) m r))
+ (m (/ (- r l) 2)))
+ ((or (<= (- r l) 1) (= x (id-of m)))
+ (and (< m (length headers)) (>= m 0) (= x (id-of m)) (elt headers m)))))))
+
+(defun bsearch (article-number)
+ (let ((headers '((:id 3) (:id 5) (:id 13) (:id 23) (:id 30))))
+ (cl-flet ((id-of (k) (plist-get (elt headers k) :id)))
+ (cl-do* ((x article-number)
+ (l 0 (if dir (1+ m) l))
+ (r (length headers) (if dir r m))
+ (m (/ (- r l) 2) (+ m (* (if dir 1 -1) (max 1 (/ (- r l) 2)))))
+ (dir (> x (id-of m)) (> x (id-of m))))
+ ((or (<= (- r l) 1) (= x (id-of m)))
+ (and (< m (length headers)) (>= m 0) (= x (id-of m)) (elt headers m)))
+ ))))
+
+(bsearch 29)
+
+
+(setq nndiscourse-headers-hashtb (gnus-make-hashtable))
+(let ((server "emacs-china.org"))
+ (seq-map (-rpartial #'plist-get :post_number)
+ (plist-get (nndiscourse-rpc-request "" "posts" :before 0) :latest_posts)))
+
+(defun nnreddit-rpc-get (&optional server)
+ "Retrieve the PRAW process for SERVER."
+ (setq proc (make-process :name server
+ :buffer (get-buffer-create (format " *%s*" server))
+ :command praw-command
+ :connection-type 'pipe
+ :noquery t
+ :sentinel #'nnreddit-sentinel
+ :stderr (get-buffer-create (format " *%s-stderr*" server))))
+ proc)
+
+
+
+;; run thor on command line, and don't instantiate it in emacs
+(let ((server "emacs-china.org"))
+ (cl-letf (((symbol-function 'nndiscourse-open-server) (lambda (&rest args) t)))
+ ;; (nndiscourse-rpc-request "" "category_latest_topics" '(:category_slug . "emacs"))
+ (nndiscourse-rpc-request server "category_latest_topics" :category_slug "emacs")))
+
+(let ((server "emacs-china.org"))
+ (seq-map (-rpartial #'plist-get :title) (nndiscourse-get-topics server "emacs")))
+
+(let ((server "emacs-china.org"))
+ (seq-map (-rpartial #'plist-get :topic_title) (nndiscourse-get-posts server :before 71000)))
+
+(let ((server "emacs-china.org"))
+ (apply #'min (seq-map (-rpartial #'plist-get :id) (nndiscourse-get-posts server))))
+
+(let ((server "emacs-china.org"))
+ (seq-filter (lambda (raw) (cl-search "org-mode" raw))
+ (seq-map (lambda (x) (plist-get x :raw)) (nndiscourse-get-posts server))))
+
+(let ((server "emacs-china.org"))
+ (seq-filter (lambda (raw) (cl-search "word wrap" (cdr raw)))
+ (seq-map (lambda (x) (cons (plist-get x :raw) (plist-get x :topic_title)))
+ (nndiscourse-get-posts server))))
+
+(let ((server "emacs-china.org"))
+ (seq-map (-rpartial #'plist-get :slug) (nndiscourse-get-categories server)))
+
+(gnus-group-full-name "programming" "nndiscourse:")
+
+(let ((server "emacs-china.org"))
+ (gnus-get-info (gnus-group-full-name "programming" `(nndiscourse ,server))))
+
+(let* ((rpc (json-rpc-connect "localhost" 8999))
+ (cooked (plist-get (json-rpc rpc "get_post" 12) :cooked)))
+ (with-temp-buffer
+ (insert cooked)
+ (shr-render-buffer (current-buffer))))
+
+(json-read-from-string (buffer-string))
+
+(makunbound 'httpd-root)
+(custom-set-default 'httpd-root "/home/dick/nndiscourse")
+(custom-set-default 'httpd-port 9009)
+(httpd-start)
+
+(let ((site "http://localhost:3000/login")
+ result)
+ (request site
+ :parser (lambda ()
+ (let ((foo (make-temp-file "foo")))
+ (write-region (point-min) (point-max) foo)
+ (eww-open-file foo)))
+ :data '(("username" . "priapushk@gmail.com")
+ ("password" . "StT9nyTvyD")
+ ("redirect" . site))
+ :sync t
+ :success (cl-function
+ (lambda (&key data symbol-status response error-thrown
+ &allow-other-keys
+ &aux (response-status (request-response-status-code response)))
+ (setq result (format "SUCCESS: ss=%s r=%s et=%s data=%s"
+ symbol-status response-status error-thrown data))))
+ :error (cl-function
+ (lambda (&key data symbol-status response error-thrown
+ &allow-other-keys
+ &aux (response-status (request-response-status-code response)))
+ (setq result (format "ERROR: ss=%s r=%s et=%s data=%s"
+ symbol-status response-status error-thrown data)))))
+ result)
+
+(let (result)
+ (request "http://localhost:3000/user-api-key/new"
+ :type "GET"
+ ;; :parser (lambda () (shr-render-buffer (current-buffer)))
+ :parser (lambda ()
+ (let ((foo (make-temp-file "foo")))
+ (write-region (point-min) (point-max) foo)
+ (eww-open-file foo)))
+ :data `(("auth_redirect" . "https://localhost:9009")
+ ("application_name" . "nndiscourse")
+ ("client_id" . "nndiscourse-0")
+ ("scopes" . "read,write,message_bus,session_info")
+ ("public_key" . ,(shell-command-to-string "gpg --export-secret-keys 87681210 | openpgp2ssh 87681210 | openssl rsa -pubout"))
+ ("nonce" . ,(shell-command-to-string "head /dev/urandom | tr -dc A-Za-z0-9 | head -c10 -")))
+ :sync t
+ :success (cl-function
+ (lambda (&key data symbol-status response error-thrown
+ &allow-other-keys
+ &aux (response-status (request-response-status-code response)))
+ (setq result (format "SUCCESS: ss=%s r=%s et=%s data=%s"
+ symbol-status response-status error-thrown data))))
+ :error (cl-function
+ (lambda (&key data symbol-status response error-thrown
+ &allow-other-keys
+ &aux (response-status (request-response-status-code response)))
+ (setq result (format "ERROR: ss=%s r=%s et=%s data=%s"
+ symbol-status response-status error-thrown data)))))
+ result)
+
+;; can't use curl because authorization requires javascript
+;; need to go through the browser, but can't auth_redirect, must
+;; copy-paste to emacs sadly.
+
+(defun nndiscourse-first-to-succeed (&rest commands)
+ "Return output of first command among COMMANDS to succeed, NIL if none."
+ (let (conds)
+ (dolist (c
+ (nreverse commands)
+ (eval `(with-temp-buffer
+ ,(cons 'cond conds))))
+ (push `((let ((_ (erase-buffer))
+ (rv (apply #'call-process
+ ,(substring c 0 (search " " c))
+ nil (quote (t nil)) nil
+ (split-string ,(aif (search " " c) (substring c (1+ it)) "")))))
+ (and (numberp rv) (zerop rv)))
+ (buffer-string))
+ conds))))
+
+
+(print-out (cl-macroexpand '(nndiscourse-first-to-succeed "true" "false")))
+(nndiscourse-first-to-succeed "false" "false" "true")
+
+(let* ((nndiscourse-public-keyfile (expand-file-name "~/.ssh/id_rsa.pub"))
+ (nndiscourse-private-keyfile (file-name-sans-extension nndiscourse-public-keyfile)))
+ (nndiscourse-first-to-succeed
+ (format "ssh-keygen -f %s -e -m pkcs8" nndiscourse-public-keyfile)
+ (format "openssl rsa -in %s -pubout" nndiscourse-private-keyfile)))
+
+(defun build-query (&optional site)
+ (unless site
+ (setq site "http://localhost:3000"))
+ (let* (result
+ (shell-command-default-error-buffer "*scratch*")
+ (nndiscourse-public-keyfile (expand-file-name "~/.ssh/id_rsa.pub"))
+ (nndiscourse-private-keyfile (file-name-sans-extension nndiscourse-public-keyfile)))
+ (format "%s/user-api-key/new?%s"
+ site
+ (url-build-query-string
+ `((auth_redirect "https://api.discourse.org/api/auth_redirect")
+ (application_name "nndiscourse")
+ (client_id "nndiscourse-0")
+ (scopes "read,write,message_bus,session_info")
+ ;; (public_key ,(shell-command-to-string (format "gpg --export-secret-keys %s | openpgp2ssh %s | openssl rsa -pubout 2>/dev/null" "87681210" "87681210")))
+ (public_key ,(or (with-temp-buffer
+ (let ((retval
+ (apply #'call-process
+ "ssh-keygen" nil t nil
+ (split-string
+ (format "-f %s -e -m pkcs8"
+ nndiscourse-public-keyfile)))))
+ (when (and (numberp retval) (zerop retval))
+ (buffer-string))))
+ (with-temp-buffer
+ (let ((retval
+ (apply #'call-process
+ "openssl" nil t nil
+ (split-string
+ (format "rsa -in %s -pubout"
+ nndiscourse-private-keyfile)))))
+ (when (and (numberp retval) (zerop retval))
+ (buffer-string))))))
+ (nonce ,(shell-command-to-string "head /dev/urandom | tr -dc A-Za-z0-9 | head -c10 -")))))))
+
+;; eww doesn't fly for lack of javascript
+(build-query "http://localhost:3000")
+
+;; client = DiscourseApi::Client.new("http://localhost:3000")
+;; client = DiscourseApi::Client.new('http://localhost:3000', 'b28f0cea1b4fb749b9a3b8683760388c', 'priapushk', 'User-Api-Key', 'User-Api-Client-Id')
+;; proc = Nndiscourse::Process.new('http://localhost:3000', 'b28f0cea1b4fb749b9a3b8683760388c', 'priapushk')
+;; (let ((user-api-key
+;; (alist-get
+;; 'key
+;; (with-temp-buffer
+;; (shell-command (concat "openssl pkeyutl -decrypt"
+;; " -inkey <(gpg --export-secret-keys 87681210 | "
+;; "openpgp2ssh 87681210 | openssl rsa 2>/dev/null) "
+;; "-in <(cat /tmp/decryptme | base64 --decode)")
+;; t)
+;; (json-read)))))
+;; user-api-key)
+
+(user-api-key "https://api.discourse.org/api/auth_redirect?payload=DiHDYIoM2pzmLfdh2FnZhwTyQfK8bdbebiol2jBouObQGojI5yF%2ByoO00ael%0AO7LstQj1uCBjQnO%2BjrbI03Bvbz1LDvQyVAMYMIPBmwam48JqfCQHm73Z0Qkc%0A%2Bid4LNo8xiP2EiycQKgYRh2KY1y19v%2FXD3Osm6o%2Fn%2BrawpVdJ0fSZTgBkHV%2F%0AcjaCAIRpOOoFzlH1CeSZBUTEl6GhT1ALKR7yurqS2GZ5MW8bIts3MYV5FQss%0A0jSDH6AG2xJENBpRJ9x%2FvM5t5DRbp2jy3105H4d4se2Qfexgf1dfrvDOpaIZ%0Aq5UTqnOatPZ94vIqjBYTnlroh8hlGGU8QKK01k6QhQ%3D%3D%0A")
+
+(defun user-api-key (return-url)
+ (let* ((emacs-china-url "https://api.discourse.org/api/auth_redirect?payload=ZYOTYZz0uM%2B3xBIRTN%2FsOoz3iZvLW%2BdWtPT%2FAOD8Ge1PWJjpGWPijLQlukl7%0AjcD%2Fd2IOTM8fBUUxO9R2P314frGKTHQ1bx%2FLCVxXhcD7CN%2FQxxPUYkr3BEui%0ANAUVW0uIH8el6VbPfPoeUfTp%2BGGYNBNkpqdZJj1sTqi%2FcbrXMlMUSfsYlqKW%0AQLQYr1XuY42vT1B%2FmVUH1i7xad6c3bb6ayQrBoTFJicEG14tEa%2BAtICUu9KI%0Aod%2FlZ2Sq%2Ffid7qnS9q0Z7l4vl6nOkT3T8ngqU2Bajx0Jo6pVcDLw6lcLv8bk%0A%2Bc2HI%2BY4zT98cEzdJjJ2XxvUnEqCPXvs6VvYe9vRcw%3D%3D%0A")
+ (parsed-url (url-generic-parse-url return-url))
+ (encrypted (cl-second
+ (assoc-string "payload" (url-parse-query-string
+ (cdr (url-path-and-query parsed-url))))))
+ (user-api-key
+ (alist-get
+ 'key
+ (with-temp-buffer
+ (shell-command (concat "openssl pkeyutl -decrypt"
+ " -inkey " (expand-file-name "~/.ssh/id_rsa")
+ " -in "
+ (format "<(echo %s | base64 --decode)" (replace-regexp-in-string "\\s-" "" encrypted)))
+ t)
+ (json-read)))))
+ user-api-key))
+
+(let ((foo (gnus-make-hashtable)))
+ (nndiscourse--sethash 1 '(a b c (c . e)) foo))
+
+(let ((foo (gnus-make-hashtable)))
+ (nndiscourse--sethash "froo" '(a b c (c . e)) foo)
+ (let ((posts (nndiscourse--gethash "froo" foo))
+ (index 2))
+ (when (< (length posts) (1+ index))
+ (nndiscourse--sethash "froo"
+ (nconc posts (make-list (- (1+ index)
+ (length posts))
+ nil))
+ foo))
+
+ (setf (elt (nndiscourse--gethash "froo" foo) index) "hi!")
+ (nndiscourse--gethash "froo" foo)))
+
+(let ((id (plist-get plst :id))
+ (group (plist-get plst :creation_id))
+ (topic-title (plist-get plst :topic_title))
+ (post_number (plist-get plst :post_number)))
+ (nndiscourse--sethash
+ id
+ (list group topic-title post_number)
+ nndiscourse-location-hashtb)
+ (nndiscourse--sethash
+ group
+ (let* ((posts-hashtb
+ (or (nndiscourse--gethash group nndiscourse-headers-hashtb)
+ (gnus-make-hashtable)))
+ (posts (nndiscourse--gethash topic-title posts-hashtb)))
+ (when (< (length posts) post-number)
+ (nndiscourse--sethash
+ topic-title
+ (nconc posts (make-list (- post-number (length posts)) nil))
+ posts-hashtb))
+ (setf (elt (nndiscourse--gethash topic-title posts-hashtb)
+ post-number)
+ plst)
+ posts-hashtb)
+ nndiscourse-headers-hashtb))
+
+(setq gnus-server-method-cache nil)
+(gnus-server-to-method "nndiscourse:emacs-china.org")
+
+(setq gnus-secondary-select-methods (cdr gnus-secondary-select-methods))
+
+(gnus-method-to-server-name '(nndiscourse "emacs-china.org" :scheme "https"))
+(gnus-find-method-for-group "nndiscourse+emacs-china.org:emacs-general")
+
+(add-to-list 'gnus-secondary-select-methods '(nndiscourse "emacs-china.org" (nndiscourse-scheme "https")))
+
+(nndiscourse-open-server "emacs-china.org")
+(nndiscourse-proc-info-process (cdr (assoc "emacs-china.org" nndiscourse-processes)))
+(let ((nntp-server-buffer (get-buffer-create "foo")))
+ (nndiscourse-request-list "emacs-china.org"))
+
+(process-contact (alist-get "emacs-china.org" nndiscourse-processes nil nil #'equal))
+
+(mapcan (lambda (b) (let ((foo (buffer-name b)))
+ (and (cl-search "stderr" foo) (list foo))))
+ (buffer-list))
+
+(gnus-find-method-for-group "nndiscourse+emacs-china.org:emacs")
+
+
+(condition-case nil
+ (prog1 t
+ (delete-process (make-network-process :name "test-port"
+ :noquery t
+ :host nndiscourse-localhost
+ :service 37529
+ :buffer nil
+ :stop t)))
+ (file-error nil))
+
+(gnus-compress-sequence (gnus-range-normalize '(72330 . 72367)))
+
+(set (gv-ref (with-current-buffer "scratch.el 最近也想尝试,但是感觉蛮难的,比如不知道如何在"))
+ (let ((handle (mm-make-handle
+ (current-buffer)
+ (rfc2231-parse-qp-string "Content-Type: text/html; charset=UTF-8"))))
+ (cl-assert (not (zerop (length (with-temp-buffer (mm-shr handle)
+ (buffer-string))))))))
+
+(require 'polymode-core)
+(defun ein:markdown-syntax-propertize (start end)
+ "Function used as `syntax-propertize-function'.
+START and END delimit region to propertize."
+ (message "got here %s %s %s" start end (pm-innermost-range start))
+ (with-silent-modifications
+ (save-excursion
+ (remove-text-properties start end ein:markdown--syntax-properties)
+ (ein:markdown-syntax-propertize-fenced-block-constructs start end)
+ (ein:markdown-syntax-propertize-list-items start end)
+ (ein:markdown-syntax-propertize-pre-blocks start end)
+ (ein:markdown-syntax-propertize-blockquotes start end)
+ (ein:markdown-syntax-propertize-headings start end)
+ (ein:markdown-syntax-propertize-hrs start end)
+ (ein:markdown-syntax-propertize-comments start end))))
+
+;; Same boat here, although I am only trying to use User-Api-Key (not Api-Key) to create a topic post and am getting CSRF denial from the actionpack library.
+
+;; Unless the discourse server has turned off CSRF checking, posting from a third-party desktop app seems hard. I’m not about to emulate a browser.
+
+;; which see ~/discourse_api/gem-transcript{,2}
+
+(with-temp-buffer
+ (let ((html " 最近也想尝试,但是感觉蛮难的,比如不知道如何在"))
+ (insert
+ "Subject: " "foo" "\n"
+ "From: " "nobody" "\n"
+ "\n")
+ (mml-insert-multipart "alternative")
+ ;; (mml-insert-empty-tag 'part 'type "text/html")
+ (mml-insert-part "text/html")
+ (insert html)
+ (insert "\n")
+ (when (mml-validate)
+ (message-encode-message-body))
+ (buffer-string)))
diff --git a/screenshot.png b/screenshot.png
new file mode 100644
index 0000000..2e2c879
Binary files /dev/null and b/screenshot.png differ
diff --git a/tests/nndiscourse-test.el b/tests/nndiscourse-test.el
new file mode 100644
index 0000000..be62744
--- /dev/null
+++ b/tests/nndiscourse-test.el
@@ -0,0 +1,59 @@
+;;; nndiscourse-test.el --- Test utilities for nndiscourse -*- lexical-binding: t; coding: utf-8 -*-
+
+;; The following is a derivative work of
+;; https://github.com/millejoh/emacs-ipython-notebook
+;; licensed under GNU General Public License v3.0.
+
+(custom-set-default 'gnus-home-directory (concat default-directory "tests"))
+(custom-set-default 'message-directory (concat default-directory "tests/Mail"))
+(custom-set-variables
+ '(auto-revert-verbose nil)
+ '(auto-revert-stop-on-user-input nil)
+ '(gnus-batch-mode t)
+ '(gnus-use-dribble-file nil)
+ '(gnus-read-newsrc-file nil)
+ '(gnus-save-killed-list nil)
+ '(gnus-save-newsrc-file nil)
+ '(gnus-secondary-select-methods (quote ((nndiscourse "meta.discourse.org"))))
+ '(gnus-select-method (quote (nnnil)))
+ '(gnus-message-highlight-citation nil)
+ '(gnus-verbose 8)
+ '(gnus-large-ephemeral-newsgroup 4000)
+ '(gnus-large-newsgroup 4000)
+ '(gnus-interactive-exit (quote quiet)))
+
+(require 'nndiscourse)
+(require 'ert)
+(require 'message)
+
+(setq ert-runner-profile nil)
+(mapc (lambda (key-params)
+ (when (string-match-p (car key-params) "nndiscourse")
+ (let ((params (cdr key-params)))
+ (setq params (assq-delete-all 'gnus-thread-sort-functions params))
+ (setcdr key-params params))))
+ gnus-parameters)
+
+(defun nndiscourse-test-wait-for (predicate &optional predargs ms interval continue)
+ "Wait until PREDICATE function returns non-`nil'.
+ PREDARGS is argument list for the PREDICATE function.
+ MS is milliseconds to wait. INTERVAL is polling interval in milliseconds."
+ (let* ((int (or interval (if ms (max 300 (/ ms 10)) 300)))
+ (count (max 1 (if ms (truncate (/ ms int)) 25))))
+ (unless (or (cl-loop repeat count
+ when (apply predicate predargs)
+ return t
+ do (sleep-for (/ int 1000.0)))
+ continue)
+ (error "Timeout: %s" predicate))))
+
+;; if yes-or-no-p isn't specially overridden, make it always "yes"
+(let ((original-yes-or-no-p (symbol-function 'yes-or-no-p)))
+ (add-function :around (symbol-function 'message-cancel-news)
+ (lambda (f &rest args)
+ (if (not (eq (symbol-function 'yes-or-no-p) original-yes-or-no-p))
+ (apply f args)
+ (cl-letf (((symbol-function 'yes-or-no-p) (lambda (&rest _args) t)))
+ (apply f args))))))
+
+(provide 'nndiscourse-test)
diff --git a/tests/recordings/install b/tests/recordings/install
new file mode 100644
index 0000000..e412007
--- /dev/null
+++ b/tests/recordings/install
@@ -0,0 +1,218 @@
+((posts-:before-0 (:latest_posts [(:id 699087 :name "Jennifer Alencar Araujo" :username "jenni_alencar" :avatar_template "/user_avatar/meta.discourse.org/jenni_alencar/{size}/169068_2.png" :created_at "2020-02-11T21:02:00.313Z" :cooked " Hi guys, What’s the difference between staff, admin and moderators? And what is this under the name of the group (Owner, automatic, member?) Thank you. I’ve got a fix here that should do the trick The problem is that the digest background color isn’t being dictated by the theme colors (it’s static grey), and the colors of those two links were a theme variable. So light text from dark themes ended up on a light grey background. Something related that may be helpful to someone looking at the digest in the future… the background color of the email is set on the HTML tag… and a lot of email clients (like gmail) will just ignore that style and make the email background white anyway… so even if the background color was defined by a theme, it wouldn’t be reliable:
\n> " (concat "\n" (buffer-string)))
+ 5)
+ (erase-buffer)))
+ (buffer-string))
+ "\n")
+ nil t nil nil nil start end)))
+ (buffer-string)))
+
+(defsubst nndiscourse--citation-wrap (author body)
+ "Cite AUTHOR using `gnus-message-cite-prefix-regexp' before displaying BODY.
+
+Originally written by Paul Issartel."
+ (with-temp-buffer
+ (insert body)
+ (mm-url-remove-markup)
+ (mm-url-decode-entities)
+ (fill-region (point-min) (point-max))
+ (let* ((trimmed-1 (replace-regexp-in-string "\\(\\s-\\|\n\\)+$" "" (buffer-string)))
+ (trimmed (replace-regexp-in-string "^\\(\\s-\\|\n\\)+" "" trimmed-1)))
+ (concat author " wrote:\n"
+ (cl-subseq (replace-regexp-in-string "\n" "\n> " (concat "\n" trimmed)) 1)
+ "\n
Almost certainly this change which was for this issue. I’m not sure exactly what the issue is, but I’ll be looking into it
" :post_number 14 :post_type 1 :updated_at "2020-02-11T20:44:48.368Z" :reply_count 0 :reply_to_post_number 12 :quote_count 1 :incoming_link_count 0 :reads 4 :readers_count 3 :score 60.8 :yours :json-false :topic_id 141232 :topic_slug "recent-changes-breaking-subfolder-setup" :topic_title "Recent Changes Breaking Subfolder Setup?" :topic_html_title "Recent Changes Breaking Subfolder Setup?" :category_id 1 :display_username "David Taylor" :primary_group_name "team" :primary_group_flair_url "https://aws1.discourse-cdn.com/dev/original/2X/e/ebee30bd98aef20357e4a177a5a1e45b877ce088.svg" :primary_group_flair_bg_color "" :primary_group_flair_color "111" :version 1 :can_edit :json-false :can_delete :json-false :can_recover :json-false :can_wiki :json-false :user_title "team" :raw "[quote=\"hartz, post:12, topic:141232\"] +where in the code +[/quote] + +Almost certainly [this change](https://github.com/discourse/discourse/commit/fe0d912b97985a6272e720729aa6197e8e40274f) which was for [this issue](https://meta.discourse.org/t/discourse-docker-blocked-csp-error-with-svg-sprite-when-using-subfolders/139492/9?u=david). I'm not sure exactly what the issue is, but I'll be looking into it :eyes:" :actions_summary [(:id 2 :count 2)] :moderator :json-false :admin t :staff t :user_id 23968 :hidden :json-false :trust_level 4 :deleted_at nil :user_deleted :json-false :edit_reason nil :can_view_edit_history t :wiki :json-false :can_accept_answer :json-false :can_unaccept_answer :json-false :accepted_answer :json-false) (:id 699077 :name "Tobias Eigen" :username "tobiaseigen" :avatar_template "/user_avatar/meta.discourse.org/tobiaseigen/{size}/116107_2.png" :created_at "2020-02-11T20:37:59.472Z" :cooked "If you mouse over that text, it will tell you the precise time. Doesn’t work on mobile, obviously. Personally, I like the way it is now.
" :post_number 2 :post_type 1 :updated_at "2020-02-11T20:37:59.472Z" :reply_count 0 :reply_to_post_number nil :quote_count 0 :incoming_link_count 0 :reads 2 :readers_count 1 :score 15.4 :yours :json-false :topic_id 141321 :topic_slug "make-topic-timers-more-specific" :topic_title "Make Topic Timers More Specific" :topic_html_title "Make Topic Timers More Specific" :category_id 2 :display_username "Tobias Eigen" :primary_group_name nil :primary_group_flair_url nil :primary_group_flair_bg_color nil :primary_group_flair_color nil :version 1 :can_edit :json-false :can_delete :json-false :can_recover :json-false :can_wiki :json-false :user_title "" :raw "If you mouse over that text, it will tell you the precise time. Doesn't work on mobile, obviously. Personally, I like the way it is now." :actions_summary [(:id 2 :count 1)] :moderator :json-false :admin :json-false :staff :json-false :user_id 8571 :hidden :json-false :trust_level 3 :deleted_at nil :user_deleted :json-false :edit_reason nil :can_view_edit_history t :wiki :json-false :can_accept_answer :json-false :can_unaccept_answer :json-false :accepted_answer :json-false) (:id 699072 :name "Kris" :username "awesomerobot" :avatar_template "/user_avatar/meta.discourse.org/awesomerobot/{size}/142900_2.png" :created_at "2020-02-11T20:24:38.134Z" :cooked "Fixed for RTL languages here
+ + +" :post_number 4 :post_type 1 :updated_at "2020-02-11T20:24:38.134Z" :reply_count 0 :reply_to_post_number nil :quote_count 0 :incoming_link_count 0 :reads 2 :readers_count 1 :score 45.4 :yours :json-false :topic_id 125660 :topic_slug "i-couldnt-see-the-setting-menu-bar-in-mobile-rtl-language" :topic_title "I couldn't see the setting menu bar in mobile RTL language" :topic_html_title "I couldn’t see the setting menu bar in mobile RTL language" :category_id 1 :display_username "Kris" :primary_group_name "team" :primary_group_flair_url "https://aws1.discourse-cdn.com/dev/original/2X/e/ebee30bd98aef20357e4a177a5a1e45b877ce088.svg" :primary_group_flair_bg_color "" :primary_group_flair_color "111" :version 1 :can_edit :json-false :can_delete :json-false :can_recover :json-false :can_wiki :json-false :user_title "team" :raw "Fixed for RTL languages here + +https://github.com/discourse/discourse/commit/d73e94bbeb54bc0002123bf879fcd8c3f0c03c7d + +![Screen Shot 2020-02-11 at 3.23.46 PM|229x500](upload://f5cA9GCYpII9Fjo8d1W24qX6quS.png)" :actions_summary [(:id 2 :count 1)] :moderator t :admin t :staff t :user_id 2770 :hidden :json-false :trust_level 4 :deleted_at nil :user_deleted :json-false :edit_reason nil :can_view_edit_history t :wiki :json-false :can_accept_answer :json-false :can_unaccept_answer :json-false :accepted_answer :json-false) (:id 699071 :name "Alex" :username "nexo" :avatar_template "/user_avatar/meta.discourse.org/nexo/{size}/106384_2.png" :created_at "2020-02-11T20:21:53.785Z" :cooked "I saw this on Meta and it seems poll fields are overlapping the border of the poll preview.
+" :post_number 1 :post_type 1 :updated_at "2020-02-11T20:21:53.785Z" :reply_count 0 :reply_to_post_number nil :quote_count 0 :incoming_link_count 0 :reads 5 :readers_count 4 :score 1.0 :yours :json-false :topic_id 141323 :topic_slug "poll-fields-colliding-with-poll-preview" :topic_title "Poll fields colliding with poll preview" :topic_html_title "Poll fields colliding with poll preview" :category_id 9 :display_username "Alex" :primary_group_name nil :primary_group_flair_url nil :primary_group_flair_bg_color nil :primary_group_flair_color nil :version 1 :can_edit :json-false :can_delete :json-false :can_recover :json-false :can_wiki :json-false :user_title "Regular" :raw "I saw this on Meta and it seems poll fields are overlapping the border of the poll preview. + +![image|690x499, 75%](upload://rwcm5sg5F1tQzij6f0Uw3R9B4RE.png)" :actions_summary [] :moderator :json-false :admin :json-false :staff :json-false :user_id 42456 :hidden :json-false :trust_level 3 :deleted_at nil :user_deleted :json-false :edit_reason nil :can_view_edit_history t :wiki :json-false :can_accept_answer :json-false :can_unaccept_answer :json-false :accepted_answer :json-false) (:id 699069 :name "Carson" :username "outofthebox" :avatar_template "/user_avatar/meta.discourse.org/outofthebox/{size}/83708_2.png" :created_at "2020-02-11T20:20:17.544Z" :cooked "Hi,
+I’d like to suggest that when a Topic is scheduled to be posted, that the text be precise about when the Topic will be published.
+Example:
+ +Better:
++" :post_number 1 :post_type 1 :updated_at "2020-02-11T20:20:17.544Z" :reply_count 0 :reply_to_post_number nil :quote_count 0 :incoming_link_count 0 :reads 4 :readers_count 3 :score 0.8 :yours :json-false :topic_id 141321 :topic_slug "make-topic-timers-more-specific" :topic_title "Make Topic Timers More Specific" :topic_html_title "Make Topic Timers More Specific" :category_id 2 :display_username "Carson" :primary_group_name nil :primary_group_flair_url nil :primary_group_flair_bg_color nil :primary_group_flair_color nil :version 1 :can_edit :json-false :can_delete :json-false :can_recover :json-false :can_wiki :json-false :user_title nil :raw "Hi, + +I'd like to suggest that when a Topic is scheduled to be posted, that the text be precise about when the Topic will be published. + +Example: + +![Screen Shot 2020-02-11 at 3.18.08 PM|690x118](upload://h6MTSHaShKYucsoMFnEkVJNanEt.png) + +Better: + +> This topic will be published to XYZ on March 14th at 8am Eastern." :actions_summary [] :moderator :json-false :admin :json-false :staff :json-false :user_id 32674 :hidden :json-false :trust_level 2 :deleted_at nil :user_deleted :json-false :edit_reason nil :can_view_edit_history t :wiki :json-false :can_accept_answer :json-false :can_unaccept_answer :json-false :accepted_answer :json-false) (:id 699063 :name "Jay Pfaffman" :username "pfaffman" :avatar_template "/user_avatar/meta.discourse.org/pfaffman/{size}/120154_2.png" :created_at "2020-02-11T20:12:33.362Z" :cooked " +This topic will be published to XYZ on March 14th at 8am Eastern.
+
It almost certainly isn’t. Make sure that it’s https
. Trailing slash or space could also be a problem.
here is my dashboard
+Did you enable Google logins on your Discourse site? It’s in step 9 of Configuring Google login for Discourse. Try going through that guide again and make sure everything has been setup correctly.
" :post_number 5 :post_type 1 :updated_at "2020-02-11T20:08:04.649Z" :reply_count 0 :reply_to_post_number 4 :quote_count 0 :incoming_link_count 0 :reads 2 :readers_count 1 :score 0.4 :yours :json-false :topic_id 141309 :topic_slug "cannot-signup-with-google" :topic_title "Cannot signup with google?" :topic_html_title "Cannot signup with google?" :category_id 6 :display_username "Simon Cossar" :primary_group_name "team" :primary_group_flair_url "https://aws1.discourse-cdn.com/dev/original/2X/e/ebee30bd98aef20357e4a177a5a1e45b877ce088.svg" :primary_group_flair_bg_color "" :primary_group_flair_color "111" :version 2 :can_edit :json-false :can_delete :json-false :can_recover :json-false :can_wiki :json-false :user_title "team" :raw "Did you enable Google logins on your Discourse site? It's in step 9 of https://meta.discourse.org/t/configuring-google-login-for-discourse/15858. Try going through that guide again and make sure everything has been setup correctly." :actions_summary [] :moderator :json-false :admin t :staff t :user_id 14353 :hidden :json-false :trust_level 4 :deleted_at nil :user_deleted :json-false :edit_reason "Automatically removed quote of whole previous post." :can_view_edit_history t :wiki :json-false :can_accept_answer :json-false :can_unaccept_answer :json-false :accepted_answer :json-false)])) (categories [(:id 17 :name "Uncategorized" :color "AB9364" :text_color "FFFFFF" :slug "uncategorized" :topic_count 11 :post_count 343 :position 16 :description "Topics that don't need a category, or don't fit into any other existing category." :description_text "Topics that don't need a category, or don't fit into any other existing category." :description_excerpt "Topics that don't need a category, or don't fit into any other existing category." :topic_url nil :read_restricted :json-false :permission nil :notification_level 1 :topic_template nil :has_children :json-false :sort_order nil :sort_ascending nil :show_subcategory_list t :num_featured_topics 3 :default_view nil :subcategory_list_style "rows" :default_top_period "all" :minimum_required_tags 0 :navigate_to_first_post_after_read :json-false :topics_day 0 :topics_week 0 :topics_month 1 :topics_year 1 :topics_all_time 11 :is_uncategorized t :uploaded_logo nil :uploaded_background nil) (:id 1 :name "bug" :color "e9dd00" :text_color "000000" :slug "bug" :topic_count 3201 :post_count 22768 :position 1 :description "A bug report means something is broken, preventing normal/typical use of Discourse. Do be sure to search prior to submitting bugs. Include repro steps, and only describe one bug per topic please." :description_text "A bug report means something is broken, preventing normal/typical use of Discourse. Do be sure to search prior to submitting bugs. Include repro steps, and only describe one bug per topic please." :description_excerpt "A bug report means something is broken, preventing normal/typical use of Discourse. Do be sure to search prior to submitting bugs. Include repro steps, and only describe one bug per topic please." :topic_url "/t/category-definition-for-bug/2" :read_restricted :json-false :permission nil :notification_level 1 :topic_template "" :has_children :json-false :sort_order "" :sort_ascending nil :show_subcategory_list t :num_featured_topics 3 :default_view "latest" :subcategory_list_style "rows" :default_top_period "all" :minimum_required_tags 0 :navigate_to_first_post_after_read :json-false :topics_day 2 :topics_week 10 :topics_month 35 :topics_year 418 :topics_all_time 3201 :uploaded_logo nil :uploaded_background nil) (:id 2 :name "feature" :color "0E76BD" :text_color "FFFFFF" :slug "feature" :topic_count 4645 :post_count 41429 :position 2 :description "Discussion about existing Discourse features, how they can be improved or enhanced, and how proposed new features could work." :description_text "Discussion about existing Discourse features, how they can be improved or enhanced, and how proposed new features could work." :description_excerpt "Discussion about existing Discourse features, how they can be improved or enhanced, and how proposed new features could work." :topic_url "/t/category-definition-for-feature/11" :read_restricted :json-false :permission nil :notification_level 1 :topic_template "" :has_children t :sort_order "" :sort_ascending nil :show_subcategory_list t :num_featured_topics 3 :default_view "latest" :subcategory_list_style "rows_with_featured_topics" :default_top_period "all" :minimum_required_tags 0 :navigate_to_first_post_after_read :json-false :topics_day 4 :topics_week 19 :topics_month 53 :topics_year 665 :topics_all_time 4760 :subcategory_ids [67] :uploaded_logo nil :uploaded_background nil) (:id 9 :name "ux" :color "5F497A" :text_color "FFFFFF" :slug "ux" :topic_count 1411 :post_count 10584 :position 3 :description "Discussion about the user interface of Discourse, how features are presented to the user in the client, including language and UI elements." :description_text "Discussion about the user interface of Discourse, how features are presented to the user in the client, including language and UI elements." :description_excerpt "Discussion about the user interface of Discourse, how features are presented to the user in the client, including language and UI elements." :topic_url "/t/category-definition-for-ux/2628" :read_restricted :json-false :permission nil :notification_level 1 :topic_template nil :has_children :json-false :sort_order nil :sort_ascending nil :show_subcategory_list t :num_featured_topics 3 :default_view nil :subcategory_list_style "rows" :default_top_period "all" :minimum_required_tags 0 :navigate_to_first_post_after_read :json-false :topics_day 1 :topics_week 9 :topics_month 30 :topics_year 313 :topics_all_time 1411 :uploaded_logo nil :uploaded_background nil) (:id 6 :name "support" :color "CEA9A9" :text_color "FFFFFF" :slug "support" :topic_count 10246 :post_count 72243 :position 8 :description "Support on configuring and using Discourse after it is up and running. For installation questions, use the install category." :description_text "Support on configuring and using Discourse after it is up and running. For installation questions, use the install category." :description_excerpt "Support on configuring and using Discourse after it is up and running. For installation questions, use the install category." :topic_url "/t/category-definition-for-support/389" :read_restricted :json-false :permission nil :notification_level 1 :topic_template "" :has_children t :sort_order "" :sort_ascending nil :show_subcategory_list t :num_featured_topics 3 :default_view "latest" :subcategory_list_style "rows" :default_top_period "all" :minimum_required_tags 0 :navigate_to_first_post_after_read :json-false :topics_day 17 :topics_week 76 :topics_month 289 :topics_year 3204 :topics_all_time 10738 :subcategory_ids [21] :uploaded_logo nil :uploaded_background nil) (:id 31 :name "installation" :color "997E7E" :text_color "FFFFFF" :slug "installation" :topic_count 978 :post_count 7274 :position 24 :description "Getting Discourse up and running for the first time, and anything you need for installation." :description_text "Getting Discourse up and running for the first time, and anything you need for installation." :description_excerpt "Getting Discourse up and running for the first time, and anything you need for installation." :topic_url "/t/about-the-installation-category/21019" :read_restricted :json-false :permission nil :notification_level 1 :topic_template nil :has_children :json-false :sort_order nil :sort_ascending nil :show_subcategory_list t :num_featured_topics 3 :default_view nil :subcategory_list_style "rows" :default_top_period "all" :minimum_required_tags 0 :navigate_to_first_post_after_read :json-false :topics_day 1 :topics_week 2 :topics_month 14 :topics_year 215 :topics_all_time 978 :uploaded_logo nil :uploaded_background nil) (:id 10 :name "howto" :color "76923C" :text_color "FFFFFF" :slug "howto" :topic_count 27 :post_count 253 :position 0 :description "Tutorial topics that describe how to set up, configure, or install Discourse using a specific platform or environment. Topics in this category may only be created by trust level 2 and up. " :description_text "Tutorial topics that describe how to set up, configure, or install Discourse using a specific platform or environment. Topics in this category may only be created by trust level 2 and up." :description_excerpt "Tutorial topics that describe how to set up, configure, or install Discourse using a specific platform or environment. Topics in this category may only be created by trust level 2 and up." :topic_url "/t/category-definition-for-howto/2629" :read_restricted :json-false :permission nil :notification_level 1 :topic_template "" :has_children t :sort_order "" :sort_ascending nil :show_subcategory_list t :num_featured_topics 3 :default_view "top" :subcategory_list_style "boxes_with_featured_topics" :default_top_period "all" :minimum_required_tags 0 :navigate_to_first_post_after_read t :topics_day 0 :topics_week 1 :topics_month 3 :topics_year 67 :topics_all_time 467 :subcategory_ids [56 55 53 45 4 66] :uploaded_logo nil :uploaded_background nil) (:id 22 :name "plugin" :color "F7941D" :text_color "FFFFFF" :slug "plugin" :topic_count 188 :post_count 7349 :position 19 :description "Plugin directory. To post here, use the Request Access button on the group page with a link to your plugin." :description_text "Plugin directory. To post here, use the Request Access button on the group page with a link to your plugin." :description_excerpt "Plugin directory. To post here, use the Request Access button on the group page with a link to your plugin." :topic_url "/t/about-the-plugin-category/12648" :read_restricted :json-false :permission nil :notification_level 1 :topic_template "" :has_children t :sort_order "" :sort_ascending nil :show_subcategory_list t :num_featured_topics 3 :default_view "latest" :subcategory_list_style "boxes" :default_top_period "all" :minimum_required_tags 0 :navigate_to_first_post_after_read :json-false :topics_day 0 :topics_week 5 :topics_month 18 :topics_year 187 :topics_all_time 638 :subcategory_ids [75 74 78 73 34 60 77 59 84 5 58 76] :uploaded_logo nil :uploaded_background nil) (:id 8 :name "hosting" :color "74CCED" :text_color "FFFFFF" :slug "hosting" :topic_count 356 :post_count 3031 :position 12 :description "Topics about hosting Discourse, either on your own servers, in the cloud, or with specific hosting services." :description_text "Topics about hosting Discourse, either on your own servers, in the cloud, or with specific hosting services." :description_excerpt "Topics about hosting Discourse, either on your own servers, in the cloud, or with specific hosting services." :topic_url "/t/category-definition-for-hosting/2626" :read_restricted :json-false :permission nil :notification_level 1 :topic_template nil :has_children :json-false :sort_order nil :sort_ascending nil :show_subcategory_list t :num_featured_topics 3 :default_view nil :subcategory_list_style "rows" :default_top_period "all" :minimum_required_tags 0 :navigate_to_first_post_after_read :json-false :topics_day 0 :topics_week 2 :topics_month 6 :topics_year 65 :topics_all_time 356 :uploaded_logo nil :uploaded_background nil) (:id 61 :name "theme" :color "BF1E2E" :text_color "FFFFFF" :slug "theme" :topic_count 132 :post_count 3615 :position 40 :description "Themes are reusable CSS/HTML blocks you can use on your site." :description_text "Themes are reusable CSS/HTML blocks you can use on your site." :description_excerpt "Themes are reusable CSS/HTML blocks you can use on your site." :topic_url "/t/about-the-theme-category/60925" :read_restricted :json-false :permission nil :notification_level 1 :topic_template "" :has_children t :sort_order "" :sort_ascending nil :show_subcategory_list :json-false :num_featured_topics 3 :default_view "latest" :subcategory_list_style "rows_with_featured_topics" :default_top_period "all" :minimum_required_tags 1 :navigate_to_first_post_after_read :json-false :topics_day 0 :topics_week 1 :topics_month 2 :topics_year 55 :topics_all_time 136 :subcategory_ids [82] :uploaded_logo nil :uploaded_background nil) (:id 65 :name "community" :color "12A89D" :text_color "FFFFFF" :slug "community" :topic_count 423 :post_count 4015 :position 4 :description "A great platform doesn’t guarantee success. Community building is a science. This category is for discussions about launching, building, growing and managing a thriving community." :description_text "A great platform doesn’t guarantee success. Community building is a science. This category is for discussions about launching, building, growing and managing a thriving community." :description_excerpt "A great platform doesn’t guarantee success. Community building is a science. This category is for discussions about launching, building, growing and managing a thriving community." :topic_url "/t/about-the-community-category/67750" :read_restricted :json-false :permission nil :notification_level 1 :topic_template "" :has_children :json-false :sort_order "" :sort_ascending nil :show_subcategory_list :json-false :num_featured_topics 3 :default_view "latest" :subcategory_list_style "rows_with_featured_topics" :default_top_period "all" :minimum_required_tags 0 :navigate_to_first_post_after_read :json-false :topics_day 0 :topics_week 3 :topics_month 9 :topics_year 113 :topics_all_time 423 :uploaded_logo nil :uploaded_background nil) (:id 7 :name "dev" :color "292929" :text_color "fff" :slug "dev" :topic_count 1581 :post_count 9413 :position 5 :description "Anything related to hacking on Discourse: submitting pull requests, configuring development environments, coding conventions, and so forth." :description_text "Anything related to hacking on Discourse: submitting pull requests, configuring development environments, coding conventions, and so forth." :description_excerpt "Anything related to hacking on Discourse: submitting pull requests, configuring development environments, coding conventions, and so forth." :topic_url "/t/category-definition-for-dev/1026" :read_restricted :json-false :permission nil :notification_level 1 :topic_template "" :has_children t :sort_order "" :sort_ascending nil :show_subcategory_list t :num_featured_topics 3 :default_view "latest" :subcategory_list_style "boxes" :default_top_period "all" :minimum_required_tags 0 :navigate_to_first_post_after_read :json-false :topics_day 1 :topics_week 7 :topics_month 36 :topics_year 427 :topics_all_time 1962 :subcategory_ids [24 27] :uploaded_logo nil :uploaded_background nil) (:id 30 :name "releases" :color "BF1E2E" :text_color "FFFFFF" :slug "releases" :topic_count 17 :post_count 74 :position 22 :description "Outlining each official release of Discourse, and plans for future releases." :description_text "Outlining each official release of Discourse, and plans for future releases." :description_excerpt "Outlining each official release of Discourse, and plans for future releases." :topic_url "/t/about-the-releases-category/20857" :read_restricted :json-false :permission nil :notification_level 1 :topic_template "" :has_children :json-false :sort_order "created" :sort_ascending :json-false :show_subcategory_list t :num_featured_topics 3 :default_view nil :subcategory_list_style "rows" :default_top_period "all" :minimum_required_tags 0 :navigate_to_first_post_after_read :json-false :topics_day 0 :topics_week 0 :topics_month 0 :topics_year 1 :topics_all_time 17 :uploaded_logo nil :uploaded_background nil) (:id 14 :name "marketplace" :color "8C6238" :text_color "FFFFFF" :slug "marketplace" :topic_count 653 :post_count 3670 :position 18 :description "About commercial Discourse related stuff: jobs or paid gigs, plugins, themes, hosting, etc. All soliciting must be in this public category, private solicitations are not permitted here." :description_text "About commercial Discourse related stuff: jobs or paid gigs, plugins, themes, hosting, etc. All soliciting must be in this public category, private solicitations are not permitted here." :description_excerpt "About commercial Discourse related stuff: jobs or paid gigs, plugins, themes, hosting, etc. All soliciting must be in this public category, private solicitations are not permitted here." :topic_url "/t/category-definition-for-marketplace/5425" :read_restricted :json-false :permission nil :notification_level 1 :topic_template "What would you like done? + +When do you need it done? + +What is your budget, in $ USD that you can offer for this task?" :has_children :json-false :sort_order nil :sort_ascending nil :show_subcategory_list t :num_featured_topics 3 :default_view nil :subcategory_list_style "rows" :default_top_period "all" :minimum_required_tags 0 :navigate_to_first_post_after_read :json-false :topics_day 0 :topics_week 0 :topics_month 13 :topics_year 166 :topics_all_time 653 :uploaded_logo nil :uploaded_background nil) (:id 3 :name "Site Feedback" :color "aaa" :text_color "FFFFFF" :slug "site-feedback" :topic_count 277 :post_count 2317 :position 6 :description "Discussion about meta.discourse.org itself, the organization of this forum about Discourse, how it works, and how we can improve this site." :description_text "Discussion about meta.discourse.org itself, the organization of this forum about Discourse, how it works, and how we can improve this site." :description_excerpt "Discussion about meta.discourse.org itself, the organization of this forum about Discourse, how it works, and how we can improve this site." :topic_url "/t/category-definition-for-meta/24" :read_restricted :json-false :permission nil :notification_level 1 :topic_template "" :has_children t :sort_order "" :sort_ascending nil :show_subcategory_list t :num_featured_topics 3 :default_view "" :subcategory_list_style "rows" :default_top_period "all" :minimum_required_tags 0 :navigate_to_first_post_after_read :json-false :topics_day 0 :topics_week 1 :topics_month 3 :topics_year 20 :topics_all_time 364 :subcategory_ids [13] :uploaded_logo nil :uploaded_background nil) (:id 35 :name "praise" :color "9EB83B" :text_color "FFFFFF" :slug "praise" :topic_count 201 :post_count 763 :position 27 :description "Got something nice to say about Discourse?" :description_text "Got something nice to say about Discourse?" :description_excerpt "Got something nice to say about Discourse?" :topic_url "/t/about-the-praise-category/30010" :read_restricted :json-false :permission nil :notification_level 1 :topic_template nil :has_children t :sort_order nil :sort_ascending nil :show_subcategory_list t :num_featured_topics 3 :default_view nil :subcategory_list_style "rows" :default_top_period "all" :minimum_required_tags 0 :navigate_to_first_post_after_read :json-false :topics_day 0 :topics_week 0 :topics_month 1 :topics_year 22 :topics_all_time 208 :subcategory_ids [63] :uploaded_logo nil :uploaded_background nil)] [(:id 17 :name "Uncategorized" :color "AB9364" :text_color "FFFFFF" :slug "uncategorized" :topic_count 11 :post_count 343 :position 16 :description "Topics that don't need a category, or don't fit into any other existing category." :description_text "Topics that don't need a category, or don't fit into any other existing category." :description_excerpt "Topics that don't need a category, or don't fit into any other existing category." :topic_url nil :read_restricted :json-false :permission nil :notification_level 1 :topic_template nil :has_children :json-false :sort_order nil :sort_ascending nil :show_subcategory_list t :num_featured_topics 3 :default_view nil :subcategory_list_style "rows" :default_top_period "all" :minimum_required_tags 0 :navigate_to_first_post_after_read :json-false :topics_day 0 :topics_week 0 :topics_month 1 :topics_year 1 :topics_all_time 11 :is_uncategorized t :uploaded_logo nil :uploaded_background nil) (:id 1 :name "bug" :color "e9dd00" :text_color "000000" :slug "bug" :topic_count 3201 :post_count 22768 :position 1 :description "A bug report means something is broken, preventing normal/typical use of Discourse. Do be sure to search prior to submitting bugs. Include repro steps, and only describe one bug per topic please." :description_text "A bug report means something is broken, preventing normal/typical use of Discourse. Do be sure to search prior to submitting bugs. Include repro steps, and only describe one bug per topic please." :description_excerpt "A bug report means something is broken, preventing normal/typical use of Discourse. Do be sure to search prior to submitting bugs. Include repro steps, and only describe one bug per topic please." :topic_url "/t/category-definition-for-bug/2" :read_restricted :json-false :permission nil :notification_level 1 :topic_template "" :has_children :json-false :sort_order "" :sort_ascending nil :show_subcategory_list t :num_featured_topics 3 :default_view "latest" :subcategory_list_style "rows" :default_top_period "all" :minimum_required_tags 0 :navigate_to_first_post_after_read :json-false :topics_day 2 :topics_week 10 :topics_month 35 :topics_year 418 :topics_all_time 3201 :uploaded_logo nil :uploaded_background nil) (:id 2 :name "feature" :color "0E76BD" :text_color "FFFFFF" :slug "feature" :topic_count 4645 :post_count 41429 :position 2 :description "Discussion about existing Discourse features, how they can be improved or enhanced, and how proposed new features could work." :description_text "Discussion about existing Discourse features, how they can be improved or enhanced, and how proposed new features could work." :description_excerpt "Discussion about existing Discourse features, how they can be improved or enhanced, and how proposed new features could work." :topic_url "/t/category-definition-for-feature/11" :read_restricted :json-false :permission nil :notification_level 1 :topic_template "" :has_children t :sort_order "" :sort_ascending nil :show_subcategory_list t :num_featured_topics 3 :default_view "latest" :subcategory_list_style "rows_with_featured_topics" :default_top_period "all" :minimum_required_tags 0 :navigate_to_first_post_after_read :json-false :topics_day 4 :topics_week 19 :topics_month 53 :topics_year 665 :topics_all_time 4760 :subcategory_ids [67] :uploaded_logo nil :uploaded_background nil) (:id 9 :name "ux" :color "5F497A" :text_color "FFFFFF" :slug "ux" :topic_count 1411 :post_count 10584 :position 3 :description "Discussion about the user interface of Discourse, how features are presented to the user in the client, including language and UI elements." :description_text "Discussion about the user interface of Discourse, how features are presented to the user in the client, including language and UI elements." :description_excerpt "Discussion about the user interface of Discourse, how features are presented to the user in the client, including language and UI elements." :topic_url "/t/category-definition-for-ux/2628" :read_restricted :json-false :permission nil :notification_level 1 :topic_template nil :has_children :json-false :sort_order nil :sort_ascending nil :show_subcategory_list t :num_featured_topics 3 :default_view nil :subcategory_list_style "rows" :default_top_period "all" :minimum_required_tags 0 :navigate_to_first_post_after_read :json-false :topics_day 1 :topics_week 9 :topics_month 30 :topics_year 313 :topics_all_time 1411 :uploaded_logo nil :uploaded_background nil) (:id 6 :name "support" :color "CEA9A9" :text_color "FFFFFF" :slug "support" :topic_count 10246 :post_count 72243 :position 8 :description "Support on configuring and using Discourse after it is up and running. For installation questions, use the install category." :description_text "Support on configuring and using Discourse after it is up and running. For installation questions, use the install category." :description_excerpt "Support on configuring and using Discourse after it is up and running. For installation questions, use the install category." :topic_url "/t/category-definition-for-support/389" :read_restricted :json-false :permission nil :notification_level 1 :topic_template "" :has_children t :sort_order "" :sort_ascending nil :show_subcategory_list t :num_featured_topics 3 :default_view "latest" :subcategory_list_style "rows" :default_top_period "all" :minimum_required_tags 0 :navigate_to_first_post_after_read :json-false :topics_day 17 :topics_week 76 :topics_month 289 :topics_year 3204 :topics_all_time 10738 :subcategory_ids [21] :uploaded_logo nil :uploaded_background nil) (:id 31 :name "installation" :color "997E7E" :text_color "FFFFFF" :slug "installation" :topic_count 978 :post_count 7274 :position 24 :description "Getting Discourse up and running for the first time, and anything you need for installation." :description_text "Getting Discourse up and running for the first time, and anything you need for installation." :description_excerpt "Getting Discourse up and running for the first time, and anything you need for installation." :topic_url "/t/about-the-installation-category/21019" :read_restricted :json-false :permission nil :notification_level 1 :topic_template nil :has_children :json-false :sort_order nil :sort_ascending nil :show_subcategory_list t :num_featured_topics 3 :default_view nil :subcategory_list_style "rows" :default_top_period "all" :minimum_required_tags 0 :navigate_to_first_post_after_read :json-false :topics_day 1 :topics_week 2 :topics_month 14 :topics_year 215 :topics_all_time 978 :uploaded_logo nil :uploaded_background nil) (:id 10 :name "howto" :color "76923C" :text_color "FFFFFF" :slug "howto" :topic_count 27 :post_count 253 :position 0 :description "Tutorial topics that describe how to set up, configure, or install Discourse using a specific platform or environment. Topics in this category may only be created by trust level 2 and up. " :description_text "Tutorial topics that describe how to set up, configure, or install Discourse using a specific platform or environment. Topics in this category may only be created by trust level 2 and up." :description_excerpt "Tutorial topics that describe how to set up, configure, or install Discourse using a specific platform or environment. Topics in this category may only be created by trust level 2 and up." :topic_url "/t/category-definition-for-howto/2629" :read_restricted :json-false :permission nil :notification_level 1 :topic_template "" :has_children t :sort_order "" :sort_ascending nil :show_subcategory_list t :num_featured_topics 3 :default_view "top" :subcategory_list_style "boxes_with_featured_topics" :default_top_period "all" :minimum_required_tags 0 :navigate_to_first_post_after_read t :topics_day 0 :topics_week 1 :topics_month 3 :topics_year 67 :topics_all_time 467 :subcategory_ids [56 55 53 45 4 66] :uploaded_logo nil :uploaded_background nil) (:id 22 :name "plugin" :color "F7941D" :text_color "FFFFFF" :slug "plugin" :topic_count 188 :post_count 7349 :position 19 :description "Plugin directory. To post here, use the Request Access button on the group page with a link to your plugin." :description_text "Plugin directory. To post here, use the Request Access button on the group page with a link to your plugin." :description_excerpt "Plugin directory. To post here, use the Request Access button on the group page with a link to your plugin." :topic_url "/t/about-the-plugin-category/12648" :read_restricted :json-false :permission nil :notification_level 1 :topic_template "" :has_children t :sort_order "" :sort_ascending nil :show_subcategory_list t :num_featured_topics 3 :default_view "latest" :subcategory_list_style "boxes" :default_top_period "all" :minimum_required_tags 0 :navigate_to_first_post_after_read :json-false :topics_day 0 :topics_week 5 :topics_month 18 :topics_year 187 :topics_all_time 638 :subcategory_ids [75 74 78 73 34 60 77 59 84 5 58 76] :uploaded_logo nil :uploaded_background nil) (:id 8 :name "hosting" :color "74CCED" :text_color "FFFFFF" :slug "hosting" :topic_count 356 :post_count 3031 :position 12 :description "Topics about hosting Discourse, either on your own servers, in the cloud, or with specific hosting services." :description_text "Topics about hosting Discourse, either on your own servers, in the cloud, or with specific hosting services." :description_excerpt "Topics about hosting Discourse, either on your own servers, in the cloud, or with specific hosting services." :topic_url "/t/category-definition-for-hosting/2626" :read_restricted :json-false :permission nil :notification_level 1 :topic_template nil :has_children :json-false :sort_order nil :sort_ascending nil :show_subcategory_list t :num_featured_topics 3 :default_view nil :subcategory_list_style "rows" :default_top_period "all" :minimum_required_tags 0 :navigate_to_first_post_after_read :json-false :topics_day 0 :topics_week 2 :topics_month 6 :topics_year 65 :topics_all_time 356 :uploaded_logo nil :uploaded_background nil) (:id 61 :name "theme" :color "BF1E2E" :text_color "FFFFFF" :slug "theme" :topic_count 132 :post_count 3615 :position 40 :description "Themes are reusable CSS/HTML blocks you can use on your site." :description_text "Themes are reusable CSS/HTML blocks you can use on your site." :description_excerpt "Themes are reusable CSS/HTML blocks you can use on your site." :topic_url "/t/about-the-theme-category/60925" :read_restricted :json-false :permission nil :notification_level 1 :topic_template "" :has_children t :sort_order "" :sort_ascending nil :show_subcategory_list :json-false :num_featured_topics 3 :default_view "latest" :subcategory_list_style "rows_with_featured_topics" :default_top_period "all" :minimum_required_tags 1 :navigate_to_first_post_after_read :json-false :topics_day 0 :topics_week 1 :topics_month 2 :topics_year 55 :topics_all_time 136 :subcategory_ids [82] :uploaded_logo nil :uploaded_background nil) (:id 65 :name "community" :color "12A89D" :text_color "FFFFFF" :slug "community" :topic_count 423 :post_count 4015 :position 4 :description "A great platform doesn’t guarantee success. Community building is a science. This category is for discussions about launching, building, growing and managing a thriving community." :description_text "A great platform doesn’t guarantee success. Community building is a science. This category is for discussions about launching, building, growing and managing a thriving community." :description_excerpt "A great platform doesn’t guarantee success. Community building is a science. This category is for discussions about launching, building, growing and managing a thriving community." :topic_url "/t/about-the-community-category/67750" :read_restricted :json-false :permission nil :notification_level 1 :topic_template "" :has_children :json-false :sort_order "" :sort_ascending nil :show_subcategory_list :json-false :num_featured_topics 3 :default_view "latest" :subcategory_list_style "rows_with_featured_topics" :default_top_period "all" :minimum_required_tags 0 :navigate_to_first_post_after_read :json-false :topics_day 0 :topics_week 3 :topics_month 9 :topics_year 113 :topics_all_time 423 :uploaded_logo nil :uploaded_background nil) (:id 7 :name "dev" :color "292929" :text_color "fff" :slug "dev" :topic_count 1581 :post_count 9413 :position 5 :description "Anything related to hacking on Discourse: submitting pull requests, configuring development environments, coding conventions, and so forth." :description_text "Anything related to hacking on Discourse: submitting pull requests, configuring development environments, coding conventions, and so forth." :description_excerpt "Anything related to hacking on Discourse: submitting pull requests, configuring development environments, coding conventions, and so forth." :topic_url "/t/category-definition-for-dev/1026" :read_restricted :json-false :permission nil :notification_level 1 :topic_template "" :has_children t :sort_order "" :sort_ascending nil :show_subcategory_list t :num_featured_topics 3 :default_view "latest" :subcategory_list_style "boxes" :default_top_period "all" :minimum_required_tags 0 :navigate_to_first_post_after_read :json-false :topics_day 1 :topics_week 7 :topics_month 36 :topics_year 427 :topics_all_time 1962 :subcategory_ids [24 27] :uploaded_logo nil :uploaded_background nil) (:id 30 :name "releases" :color "BF1E2E" :text_color "FFFFFF" :slug "releases" :topic_count 17 :post_count 74 :position 22 :description "Outlining each official release of Discourse, and plans for future releases." :description_text "Outlining each official release of Discourse, and plans for future releases." :description_excerpt "Outlining each official release of Discourse, and plans for future releases." :topic_url "/t/about-the-releases-category/20857" :read_restricted :json-false :permission nil :notification_level 1 :topic_template "" :has_children :json-false :sort_order "created" :sort_ascending :json-false :show_subcategory_list t :num_featured_topics 3 :default_view nil :subcategory_list_style "rows" :default_top_period "all" :minimum_required_tags 0 :navigate_to_first_post_after_read :json-false :topics_day 0 :topics_week 0 :topics_month 0 :topics_year 1 :topics_all_time 17 :uploaded_logo nil :uploaded_background nil) (:id 14 :name "marketplace" :color "8C6238" :text_color "FFFFFF" :slug "marketplace" :topic_count 653 :post_count 3670 :position 18 :description "About commercial Discourse related stuff: jobs or paid gigs, plugins, themes, hosting, etc. All soliciting must be in this public category, private solicitations are not permitted here." :description_text "About commercial Discourse related stuff: jobs or paid gigs, plugins, themes, hosting, etc. All soliciting must be in this public category, private solicitations are not permitted here." :description_excerpt "About commercial Discourse related stuff: jobs or paid gigs, plugins, themes, hosting, etc. All soliciting must be in this public category, private solicitations are not permitted here." :topic_url "/t/category-definition-for-marketplace/5425" :read_restricted :json-false :permission nil :notification_level 1 :topic_template "What would you like done? + +When do you need it done? + +What is your budget, in $ USD that you can offer for this task?" :has_children :json-false :sort_order nil :sort_ascending nil :show_subcategory_list t :num_featured_topics 3 :default_view nil :subcategory_list_style "rows" :default_top_period "all" :minimum_required_tags 0 :navigate_to_first_post_after_read :json-false :topics_day 0 :topics_week 0 :topics_month 13 :topics_year 166 :topics_all_time 653 :uploaded_logo nil :uploaded_background nil) (:id 3 :name "Site Feedback" :color "aaa" :text_color "FFFFFF" :slug "site-feedback" :topic_count 277 :post_count 2317 :position 6 :description "Discussion about meta.discourse.org itself, the organization of this forum about Discourse, how it works, and how we can improve this site." :description_text "Discussion about meta.discourse.org itself, the organization of this forum about Discourse, how it works, and how we can improve this site." :description_excerpt "Discussion about meta.discourse.org itself, the organization of this forum about Discourse, how it works, and how we can improve this site." :topic_url "/t/category-definition-for-meta/24" :read_restricted :json-false :permission nil :notification_level 1 :topic_template "" :has_children t :sort_order "" :sort_ascending nil :show_subcategory_list t :num_featured_topics 3 :default_view "" :subcategory_list_style "rows" :default_top_period "all" :minimum_required_tags 0 :navigate_to_first_post_after_read :json-false :topics_day 0 :topics_week 1 :topics_month 3 :topics_year 20 :topics_all_time 364 :subcategory_ids [13] :uploaded_logo nil :uploaded_background nil) (:id 35 :name "praise" :color "9EB83B" :text_color "FFFFFF" :slug "praise" :topic_count 201 :post_count 763 :position 27 :description "Got something nice to say about Discourse?" :description_text "Got something nice to say about Discourse?" :description_excerpt "Got something nice to say about Discourse?" :topic_url "/t/about-the-praise-category/30010" :read_restricted :json-false :permission nil :notification_level 1 :topic_template nil :has_children t :sort_order nil :sort_ascending nil :show_subcategory_list t :num_featured_topics 3 :default_view nil :subcategory_list_style "rows" :default_top_period "all" :minimum_required_tags 0 :navigate_to_first_post_after_read :json-false :topics_day 0 :topics_week 0 :topics_month 1 :topics_year 22 :topics_all_time 208 :subcategory_ids [63] :uploaded_logo nil :uploaded_background nil)])) \ No newline at end of file diff --git a/tests/test-uncacheable.el b/tests/test-uncacheable.el new file mode 100644 index 0000000..c4f95e8 --- /dev/null +++ b/tests/test-uncacheable.el @@ -0,0 +1,7 @@ +;;; -*- lexical-binding: t; coding: utf-8 -*- +(require 'nndiscourse-test) + +;; since nndiscourse has fixed numbering, maybe we *can* use gnus-cache +(ert-deftest nndiscourse-could-cache () + (should (featurep 'gnus-cache)) + (should-not (string-match (or gnus-uncacheable-groups "$a") "nndiscourse+emacs-china.org:emacs"))) diff --git a/tools/package-lint.sh b/tools/package-lint.sh new file mode 100644 index 0000000..9e1a6cd --- /dev/null +++ b/tools/package-lint.sh @@ -0,0 +1,21 @@ +#!/bin/sh -ex + +export EMACS="${EMACS:=emacs}" +export BASENAME=$(basename "$1") + +( cask emacs -Q --batch \ + --visit "$1" \ + --eval "(checkdoc-eval-current-buffer)" \ + --eval "(princ (with-current-buffer checkdoc-diagnostic-buffer \ + (buffer-string)))" \ + 2>&1 | egrep -a "^$BASENAME:" ) && false + +!( cask emacs -Q --batch \ + -l package-lint \ + --eval "(package-initialize)" \ + --eval "(push (quote (\"melpa\" . \"http://melpa.org/packages/\")) \ + package-archives)" \ + --eval "(package-refresh-contents)" \ + --eval "(setq debug-on-error t)" \ + -f package-lint-batch-and-exit "$1" \ + 2>&1 | egrep -a "^$BASENAME:" | egrep -v "non-snapshot" | egrep .) diff --git a/tools/readme-sed.sh b/tools/readme-sed.sh new file mode 100755 index 0000000..991872d --- /dev/null +++ b/tools/readme-sed.sh @@ -0,0 +1,17 @@ +#!/bin/bash + +# by mklement0 https://stackoverflow.com/a/29613573/5132008 + +# Define sample multi-line literal. +input=`cat` +replace="$input" +if [ ! -z "$3" ]; then + replace=$(awk "/$3/,EOF { print \" \" \$0 }" <<<"$input") +fi + +# Escape it for use as a Sed replacement string. +IFS= read -d '' -r < <(sed -e ':a' -e '$!{N;ba' -e '}' -e 's/[&/\]/\\&/g; s/\n/\\&/g' <<<"$replace") +replaceEscaped=${REPLY%$'\n'} + +# If ok, outputs $replace as is. +sed "/$1/c\\$replaceEscaped" $2 diff --git a/tools/recipe b/tools/recipe new file mode 100644 index 0000000..39469ab --- /dev/null +++ b/tools/recipe @@ -0,0 +1,3 @@ +(nndiscourse :repo "dickmao/nndiscourse" + :fetcher github + :files ("nndiscourse.el" ("nndiscourse" "nndiscourse/.ruby-version" "nndiscourse/Gemfile" "nndiscourse/Gemfile.lock" "nndiscourse/nndiscourse.gemspec" "nndiscourse/nndiscourse.thor" "nndiscourse/lib")))