1579 lines
68 KiB
EmacsLisp
1579 lines
68 KiB
EmacsLisp
;;; nnreddit.el --- Gnus backend for reddit -*- lexical-binding: t; coding: utf-8 -*-
|
|
|
|
;; Copyright (C) 2019 The Authors of nnreddit.el
|
|
|
|
;; Authors: dickmao <github id: dickmao>
|
|
;; Keywords: news
|
|
;; URL: https://github.com/dickmao/nnreddit
|
|
|
|
;; This file is NOT part of GNU Emacs.
|
|
|
|
;; This program is free software: you can redistribute it and/or modify
|
|
;; it under the terms of the GNU General Public License as published by
|
|
;; the Free Software Foundation, either version 3 of the License, or
|
|
;; (at your option) any later version.
|
|
|
|
;; This program is distributed in the hope that it will be useful,
|
|
;; but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
;; GNU General Public License for more details.
|
|
|
|
;; You should have received a copy of the GNU General Public License
|
|
;; along with nnreddit.el. If not, see <https://www.gnu.org/licenses/>.
|
|
|
|
;;; Commentary:
|
|
|
|
;; A Gnus backend for Reddit.
|
|
|
|
;;; Code:
|
|
|
|
;; Gnus Reddit
|
|
;; ---- ------
|
|
;; list subscribed subreddits
|
|
;; group subreddit
|
|
;; threads threads
|
|
;; root article link or submission
|
|
;; articles {root article, comments}
|
|
|
|
(require 'nnoo)
|
|
(require 'gnus)
|
|
(require 'gnus-start)
|
|
(require 'gnus-art)
|
|
(require 'gnus-sum)
|
|
(require 'gnus-msg)
|
|
(require 'gnus-cite)
|
|
(require 'gnus-srvr)
|
|
(require 'gnus-cache)
|
|
(require 'gnus-bcklg)
|
|
(require 'gnus-score)
|
|
(require 'python)
|
|
(require 'subr-x)
|
|
(require 'json-rpc)
|
|
(require 'mm-url)
|
|
(require 'cl-lib)
|
|
(require 'virtualenvwrapper)
|
|
(require 'anaphora)
|
|
(require 'request)
|
|
(require 'url-http)
|
|
(require 'gnus-topic)
|
|
|
|
(defvar nnreddit--groups nil
|
|
"Someone asked to avoid re-requesting joined subreddits.")
|
|
|
|
(nnoo-declare nnreddit)
|
|
|
|
(eval-when-compile
|
|
(require 'subr-x)
|
|
(unless (fboundp 'libxml-parse-xml-region)
|
|
(display-warning 'nnreddit "nnreddit requires libxml support")))
|
|
|
|
(defalias 'nnreddit-string-trim-right
|
|
(lambda (string &optional regexp)
|
|
"Trim STRING of trailing string matching REGEXP.
|
|
REGEXP defaults to \"[ \\t\\n\\r]+\"."
|
|
(if (string-match (concat "\\(?:" (or regexp "[ \t\n\r]+") "\\)\\'") string)
|
|
(replace-match "" t t string)
|
|
string)))
|
|
|
|
(defgroup nnreddit nil "A Gnus backend for Reddit."
|
|
:group 'gnus)
|
|
|
|
(defvar nnreddit--whoami nil "To populate with reddit login.")
|
|
|
|
(defcustom nnreddit-max-render-bytes 300e3
|
|
"`quoted-printable-encode-region' bogs when spyware gets out of hand."
|
|
:type 'integer
|
|
:group 'nnreddit)
|
|
|
|
(defcustom nnreddit-render-submission t
|
|
"If non-nil, follow link upon `gnus-summary-select-article'.
|
|
|
|
Otherwise, just display link."
|
|
:type 'boolean
|
|
:group 'nnreddit)
|
|
|
|
(defmacro nnreddit--gethash (string hashtable)
|
|
"Get corresponding value of STRING from HASHTABLE.
|
|
Starting in emacs-src commit c1b63af, Gnus moved from obarrays to
|
|
normal hashtables."
|
|
`(,(if (fboundp 'gnus-gethash-safe)
|
|
'gnus-gethash-safe
|
|
'gethash)
|
|
,string ,hashtable))
|
|
|
|
(defmacro nnreddit--sethash (string value hashtable)
|
|
"Set corresponding value of STRING to VALUE in HASHTABLE.
|
|
Starting in emacs-src commit c1b63af, Gnus moved from obarrays to
|
|
normal hashtables."
|
|
`(,(if (fboundp 'gnus-sethash)
|
|
'gnus-sethash
|
|
'puthash)
|
|
,string ,value ,hashtable))
|
|
|
|
(defcustom nnreddit-python-command "python3.8"
|
|
"Python executable name."
|
|
:type (append '(choice)
|
|
(let (result)
|
|
(dolist (py '("python3.7" "python3.8" "python3.9" "python3.10")
|
|
result)
|
|
(setq result (append result `((const :tag ,py ,py))))))
|
|
'((string :tag "Other")))
|
|
:set (lambda (symbol value)
|
|
(set-default symbol value)
|
|
(unless (string-match-p
|
|
"\\b3"
|
|
(shell-command-to-string (format "%s --version" value)))
|
|
;; emacs's arcane custom infra swallows `error' here
|
|
(display-warning 'nnreddit
|
|
"nnreddit-python-command: must customize to python 3.x"
|
|
:error)))
|
|
:group 'nnreddit)
|
|
|
|
(defcustom nnreddit-venv
|
|
(let* ((library-directory (file-name-directory (locate-library "nnreddit")))
|
|
(parent-directory (file-name-directory (directory-file-name library-directory)))
|
|
(requirements-directory
|
|
(file-name-directory (locate-library "requirements.txt" nil
|
|
(list library-directory parent-directory))))
|
|
(defacto-version (file-name-nondirectory
|
|
(directory-file-name requirements-directory)))
|
|
(venv-id (concat defacto-version "-" nnreddit-python-command))
|
|
(result (concat (file-name-as-directory venv-location) venv-id))
|
|
(requirements (concat requirements-directory "requirements.txt"))
|
|
(install-args (if (file-exists-p requirements)
|
|
(list "-r" requirements)
|
|
(list "virtualenv")))
|
|
(already-in-venv
|
|
(not (zerop (apply #'call-process nnreddit-python-command
|
|
nil nil nil
|
|
(list
|
|
"-c"
|
|
"import sys; sys.exit(hasattr(sys, 'real_prefix'))")))))
|
|
(pip-args (append (list "-m" "pip" "install")
|
|
(unless already-in-venv (list "--user"))
|
|
install-args))
|
|
(pip-status
|
|
(apply #'call-process nnreddit-python-command nil nil nil
|
|
pip-args)))
|
|
(gnus-message 7 "nnreddit-venv: %s %s" nnreddit-python-command
|
|
(mapconcat 'identity pip-args " "))
|
|
(cond ((numberp pip-status)
|
|
(unless (zerop pip-status)
|
|
(gnus-message 3 "nnreddit-venv: pip install exit %s" pip-status)))
|
|
(t (gnus-message 3 "nnreddit-venv: pip install signal %s" pip-status)))
|
|
(gnus-message 7 "nnreddit-venv: %s" result)
|
|
(unless (file-exists-p venv-location)
|
|
(make-directory venv-location))
|
|
(cond ((member venv-id (split-string (venv-list-virtualenvs))) result)
|
|
(t (gnus-message 5 "nnreddit-venv: installing venv to %s..." result)
|
|
(condition-case err
|
|
(progn
|
|
(venv-mkvirtualenv-using nnreddit-python-command venv-id)
|
|
(venv-with-virtualenv-shell-command
|
|
venv-id
|
|
;; `python` and not `nnreddit-python-command` because
|
|
;; venv normalizes the executable to `python`.
|
|
(format "cd %s && python setup.py install" requirements-directory))
|
|
(gnus-message 5 "nnreddit-venv: installing venv to %s...done" result)
|
|
result)
|
|
(error (when (venv-is-valid venv-id)
|
|
(condition-case rmerr
|
|
(venv-rmvirtualenv venv-id)
|
|
(error (gnus-message 3 (format "venv-rmvirtualenv: %s"
|
|
(error-message-string rmerr))))))
|
|
(gnus-message 3 (format "nnreddit-venv: %s"
|
|
(error-message-string err)))
|
|
"/dev/null")))))
|
|
"Full path to venv directory.
|
|
|
|
To facilitate upgrades, the name gloms a de facto version (the directory
|
|
name where this file resides) and the `nnreddit-python-command'."
|
|
:type '(choice (string :tag "Directory" (get (quote nnreddit-env) (quote standard-value)))
|
|
(const :tag "Development" nil))
|
|
:group 'nnreddit)
|
|
|
|
(defmacro nnreddit-define-keys (km prefix parent &rest binds)
|
|
"Define keymap KM with prefix PREFIX and parent PARENT with bindings BINDS."
|
|
(declare (indent defun))
|
|
(if (get 'gnus-define-keys 'byte-obsolete-info)
|
|
`(progn
|
|
(setq ,km nil)
|
|
(define-prefix-command ',km)
|
|
(define-key ,parent ,prefix ,km)
|
|
(cl-loop for (k v) on ',binds by (function cddr)
|
|
do (define-key ,km k v)))
|
|
`(gnus-define-keys (,km ,prefix ,parent) ,@binds)))
|
|
|
|
(defvar nnreddit-group-mode-map (make-sparse-keymap))
|
|
|
|
(nnreddit-define-keys nnreddit-group-mode-map "R" gnus-group-mode-map
|
|
"g" nnreddit-goto-group)
|
|
|
|
(defvar nnreddit-summary-voting-map
|
|
(let ((map (make-sparse-keymap)))
|
|
map)
|
|
"Voting map.")
|
|
|
|
(defvar nnreddit-summary-mode-map
|
|
(let ((map (make-sparse-keymap)))
|
|
(define-key map "r" 'gnus-summary-followup)
|
|
(define-prefix-command 'nnreddit-summary-voting-map)
|
|
(define-key map "R" 'nnreddit-summary-voting-map)
|
|
(define-key nnreddit-summary-voting-map "0" 'nnreddit-novote)
|
|
(define-key nnreddit-summary-voting-map "-" 'nnreddit-downvote)
|
|
(define-key nnreddit-summary-voting-map "=" 'nnreddit-upvote)
|
|
(define-key nnreddit-summary-voting-map "+" 'nnreddit-upvote)
|
|
map))
|
|
|
|
(defvar nnreddit-article-mode-map
|
|
(copy-keymap nnreddit-summary-mode-map)) ;; how does Gnus do this?
|
|
|
|
(defcustom nnreddit-log-rpc nil
|
|
"Turn on PRAW logging."
|
|
:type 'boolean
|
|
:group 'nnreddit)
|
|
|
|
(defcustom nnreddit-rpc-request-timeout 60
|
|
"Timeout for talking to PRAW."
|
|
:type 'integer
|
|
:group 'nnreddit)
|
|
|
|
(defcustom nnreddit-localhost "127.0.0.1"
|
|
"Some users keep their browser in a separate domain.
|
|
Do not set this to \"localhost\" as a numeric IP is required
|
|
for the oauth handshake."
|
|
:type 'string
|
|
:group 'nnreddit)
|
|
|
|
(defvar nnreddit-rpc-log-filename nil)
|
|
|
|
(defvar nnreddit--python-module-extra-args nil "Primarily for testing.")
|
|
|
|
(define-minor-mode nnreddit-article-mode
|
|
"Minor mode for nnreddit articles.
|
|
Disallow `gnus-article-reply-with-original'.
|
|
|
|
\\{gnus-article-mode-map}"
|
|
:lighter " Reddit"
|
|
:keymap nnreddit-article-mode-map)
|
|
|
|
(define-minor-mode nnreddit-summary-mode
|
|
"Disallow \"reply\" commands in `gnus-summary-mode-map'.
|
|
|
|
\\{nnreddit-summary-mode-map}"
|
|
:lighter " Reddit"
|
|
:keymap nnreddit-summary-mode-map)
|
|
|
|
(define-minor-mode nnreddit-group-mode
|
|
"Add `R-g' go-to-subreddit binding to *Group*.
|
|
|
|
\\{nnreddit-group-mode-map}"
|
|
:keymap nnreddit-group-mode-map
|
|
:interactive (gnus-group-mode))
|
|
|
|
(cl-defun nnreddit-novote ()
|
|
"Retract vote."
|
|
(interactive)
|
|
(nnreddit-vote-current-article 0))
|
|
|
|
(cl-defun nnreddit-downvote ()
|
|
"Downvote the article in current buffer."
|
|
(interactive)
|
|
(nnreddit-vote-current-article -1))
|
|
|
|
(cl-defun nnreddit-upvote ()
|
|
"Upvote the article in current buffer."
|
|
(interactive)
|
|
(nnreddit-vote-current-article 1))
|
|
|
|
(defvar nnreddit--seq-map-indexed
|
|
(if (fboundp 'seq-map-indexed)
|
|
#'seq-map-indexed
|
|
(lambda (function sequence)
|
|
(let ((index 0))
|
|
(seq-map (lambda (elt)
|
|
(prog1
|
|
(funcall function elt index)
|
|
(setq index (1+ index))))
|
|
sequence)))))
|
|
|
|
(defmacro nnreddit--normalize-server ()
|
|
"Disallow \"server\" from being empty string, which is unsettling.
|
|
Normalize it to \"nnreddit-default\"."
|
|
`(let ((canonical "nnreddit-default"))
|
|
(when (equal server "")
|
|
(setq server nil))
|
|
(unless server
|
|
(setq server canonical))
|
|
(unless (string= server canonical)
|
|
(error "`nnreddit--normalize-server': multiple servers unsupported!"))))
|
|
|
|
(defvar nnreddit-headers-hashtb (gnus-make-hashtable)
|
|
"Group -> merged submissions and comments sorted by created time.")
|
|
|
|
(defvar nnreddit-refs-hashtb (gnus-make-hashtable)
|
|
"Who replied to whom (global over all entries).")
|
|
|
|
(defvar nnreddit-authors-hashtb (gnus-make-hashtable)
|
|
"For fast lookup of parent-author (global over all entries).")
|
|
|
|
(defsubst nnreddit-get-headers (group)
|
|
"List headers from GROUP."
|
|
(nnreddit--gethash group nnreddit-headers-hashtb))
|
|
|
|
(defun nnreddit-find-header (group id)
|
|
"O(n) search of GROUP headers for ID."
|
|
(-when-let* ((headers (nnreddit-get-headers group))
|
|
(found (seq-position headers id
|
|
(lambda (plst id)
|
|
(equal id (plist-get plst :id))))))
|
|
(nnreddit--get-header (1+ found) group)))
|
|
|
|
(defsubst nnreddit-refs-for (name &optional depth)
|
|
"Get message ancestry for NAME up to DEPTH."
|
|
(unless depth
|
|
(setq depth most-positive-fixnum))
|
|
(when (> depth 0)
|
|
(nreverse (cl-loop with parent-id = (nnreddit--gethash name nnreddit-refs-hashtb)
|
|
for level = 0 then level
|
|
for name = parent-id then
|
|
(nnreddit--gethash name nnreddit-refs-hashtb)
|
|
until (null name)
|
|
collect name
|
|
until (>= (cl-incf level) depth)))))
|
|
|
|
(defsubst nnreddit-sort-append-headers (group &rest lvp)
|
|
"Append to hashed headers of GROUP the LVP (list of vector of plists)."
|
|
(nnreddit--sethash group (nconc (nnreddit-get-headers group)
|
|
(apply #'nnreddit--sort-headers lvp))
|
|
nnreddit-headers-hashtb))
|
|
|
|
(defvar nnreddit-directory (nnheader-concat gnus-directory "reddit")
|
|
"Where to retrieve last read state.")
|
|
|
|
(defvar nnreddit-processes nil
|
|
"Garbage collect PRAW processes.")
|
|
|
|
(nnoo-define-basics nnreddit)
|
|
|
|
(defsubst nnreddit-rpc-call (server generator_kwargs method &rest args)
|
|
"Make jsonrpc call to SERVER with GENERATOR_KWARGS using METHOD ARGS.
|
|
Process stays the same, but the jsonrpc connection (a cheap struct) gets
|
|
reinstantiated with every call."
|
|
(nnreddit--normalize-server)
|
|
(-when-let* ((proc (nnreddit-rpc-get server))
|
|
(connection (json-rpc--create :process proc
|
|
:host nnreddit-localhost
|
|
:id-counter 0)))
|
|
(condition-case-unless-debug err
|
|
(apply #'nnreddit-rpc-request connection generator_kwargs method args)
|
|
(error (gnus-message 3 "nnreddit-rpc-call: %s" (error-message-string err))
|
|
nil))))
|
|
|
|
(defsubst nnreddit--populate-whoami ()
|
|
"Get login name from PRAW user_attr."
|
|
(unless nnreddit--whoami
|
|
(setq nnreddit--whoami
|
|
(aand (nnreddit-rpc-call nil nil "user_attr" "name")
|
|
(and (stringp it) (not (zerop (length it))) it))))
|
|
nnreddit--whoami)
|
|
|
|
(defvar nnreddit--current-feature)
|
|
(defmacro nnreddit--test-supports-inbox (&rest body)
|
|
"Run BODY if not testing or testfile later than 20201124."
|
|
`(when (or (not (boundp 'nnreddit--current-feature))
|
|
(>= (string-to-number nnreddit--current-feature) 20201124))
|
|
,@body))
|
|
|
|
(defun nnreddit--inbox-realname ()
|
|
"Return /u/[nnreddit--whoami]."
|
|
(nnreddit--test-supports-inbox (nnreddit--populate-whoami))
|
|
(when (stringp nnreddit--whoami) (concat "/u/" nnreddit--whoami)))
|
|
|
|
(defun nnreddit-goto-group (realname)
|
|
"Jump to the REALNAME subreddit."
|
|
(interactive (list (read-no-blanks-input "Subreddit: r/")))
|
|
(let* ((canonical (nnreddit-rpc-call nil nil "canonical_spelling" realname))
|
|
(group (gnus-group-full-name canonical (list "nnreddit"))))
|
|
(if group
|
|
(progn (gnus-activate-group group t)
|
|
(gnus-group-read-group t t group))
|
|
(gnus-message 3 "nnreddit-goto-group: failed canonical_spelling of %s" realname))))
|
|
|
|
(defsubst nnreddit--current-article-number ()
|
|
"`gnus-article-current' is a global variable that gets clobbered."
|
|
(or (cdr gnus-message-group-art)
|
|
(and (gnus-buffer-live-p gnus-summary-buffer)
|
|
(with-current-buffer gnus-summary-buffer
|
|
(cdr gnus-article-current)))))
|
|
|
|
(defsubst nnreddit--current-group ()
|
|
"`gnus-article-current' is a global variable that gets clobbered."
|
|
(or (car gnus-message-group-art)
|
|
(with-current-buffer gnus-summary-buffer
|
|
(car gnus-article-current))))
|
|
|
|
(defun nnreddit-vote-current-article (vote)
|
|
"VOTE is +1, -1, 0."
|
|
(unless gnus-newsgroup-name
|
|
(error "No current newgroup"))
|
|
(if-let ((article-number (or (nnreddit--current-article-number)
|
|
(with-current-buffer gnus-summary-buffer
|
|
(gnus-summary-article-number)))))
|
|
(let* ((header (nnreddit--get-header
|
|
article-number
|
|
(gnus-group-real-name (or (nnreddit--current-group)
|
|
gnus-newsgroup-name))))
|
|
(orig-score (format "%s" (plist-get header :score)))
|
|
(new-score (if (zerop vote) orig-score
|
|
(concat orig-score " "
|
|
(if (> vote 0) "+" "")
|
|
(format "%s" vote))))
|
|
(article-name (plist-get header :name)))
|
|
(save-excursion
|
|
(save-window-excursion
|
|
(with-current-buffer gnus-summary-buffer
|
|
(if (eq (gnus-summary-article-number) (cdr gnus-article-current))
|
|
(progn (with-current-buffer gnus-article-buffer
|
|
(let ((inhibit-read-only t))
|
|
(nnheader-replace-header "Score" new-score)))
|
|
(nnreddit-rpc-call nil nil "vote" article-name vote))
|
|
(message "Open the article before voting"))))))
|
|
(error "No current article")))
|
|
|
|
(defsubst nnreddit--gate (&optional group)
|
|
"Apply our minor modes only when the following conditions hold for GROUP."
|
|
(unless group
|
|
(setq group gnus-newsgroup-name))
|
|
(and (stringp group)
|
|
(listp (gnus-group-method group))
|
|
(eq 'nnreddit (car (gnus-group-method group)))))
|
|
|
|
(defsubst nnreddit--message-gate ()
|
|
"In `message-mode', `gnus-newsgroup-name' could be anything.
|
|
So we cannot use `nnreddit--gate'."
|
|
(nnreddit--gate (car-safe gnus-message-group-art)))
|
|
|
|
(defun nnreddit-update-subscription (group level oldlevel &optional _previous)
|
|
"Nnreddit `gnus-group-change-level' callback of GROUP to LEVEL from OLDLEVEL."
|
|
(when (nnreddit--gate group)
|
|
(let ((old-subbed-p (<= oldlevel gnus-level-subscribed))
|
|
(new-subbed-p (<= level gnus-level-subscribed)))
|
|
(unless (eq old-subbed-p new-subbed-p)
|
|
;; afaict, praw post() doesn't return status
|
|
(setq nnreddit--groups nil)
|
|
(if new-subbed-p
|
|
(nnreddit-rpc-call nil nil "subscribe" (gnus-group-real-name group))
|
|
(nnreddit-rpc-call nil nil "unsubscribe" (gnus-group-real-name group)))))))
|
|
|
|
(defun nnreddit-rpc-kill (&optional server)
|
|
"Kill the jsonrpc process named SERVER."
|
|
(interactive (list nil))
|
|
(nnreddit--normalize-server)
|
|
(let (new-processes)
|
|
(mapc (lambda (proc) (if (and server (not (string= server (process-name proc))))
|
|
(push proc new-processes)
|
|
(delete-process proc)))
|
|
nnreddit-processes)
|
|
(setq nnreddit-processes new-processes)))
|
|
|
|
(deffoo nnreddit-request-close ()
|
|
(nnreddit-close-server)
|
|
t)
|
|
|
|
(deffoo nnreddit-request-type (_group &optional _article)
|
|
'news)
|
|
|
|
(deffoo nnreddit-server-opened (&optional server)
|
|
(nnreddit--normalize-server)
|
|
(setq nnreddit-processes
|
|
(cl-remove-if-not (lambda (proc) (string= server (process-name proc)))
|
|
nnreddit-processes)))
|
|
|
|
(deffoo nnreddit-status-message (&optional server)
|
|
(nnreddit--normalize-server)
|
|
"")
|
|
|
|
(deffoo nnreddit-open-server (_server &optional _defs)
|
|
t)
|
|
|
|
(deffoo nnreddit-close-group (_group &optional server)
|
|
(nnreddit--normalize-server)
|
|
t)
|
|
|
|
(defmacro nnreddit--with-group (group &rest body)
|
|
"Disambiguate GROUP if it's empty and execute BODY."
|
|
(declare (debug (form &rest form))
|
|
(indent 1))
|
|
`(when-let ((group (or ,group (gnus-group-real-name gnus-newsgroup-name)))
|
|
(gnus-newsgroup-name (unless (zerop (length group))
|
|
(gnus-group-full-name group "nnreddit:"))))
|
|
,@body))
|
|
|
|
(defun nnreddit--get-header (article-number &optional group)
|
|
"Get header indexed ARTICLE-NUMBER for GROUP."
|
|
(nnreddit--with-group group
|
|
(let ((headers (nnreddit-get-headers group)))
|
|
(elt headers (1- article-number)))))
|
|
|
|
(defun nnreddit--get-body (name &optional group server)
|
|
"Get full text of submission or comment NAME for GROUP at SERVER."
|
|
(nnreddit--normalize-server)
|
|
(if name
|
|
(nnreddit--with-group group
|
|
(nnreddit-rpc-call server nil "body" group name))
|
|
(gnus-message 3 "nnreddit--get-body: null name\n%s"
|
|
(with-temp-buffer
|
|
(backtrace)
|
|
(buffer-string)))))
|
|
|
|
(defsubst nnreddit-hack-name-to-id (name)
|
|
"Get x from t1_x (NAME)."
|
|
(cl-subseq name 3))
|
|
|
|
(defsubst nnreddit--br-tagify (body)
|
|
"Reddit-html BODY shies away from <BR>. Should it?"
|
|
(replace-regexp-in-string "\n" "<br>" body))
|
|
|
|
(defsubst nnreddit--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:<br>\n"
|
|
"<pre>\n"
|
|
(cl-subseq (replace-regexp-in-string "\n" "\n> " (concat "\n" trimmed)) 1)
|
|
"\n</pre>\n\n"))))
|
|
|
|
(defun nnreddit-add-entry (hashtb e field)
|
|
"Add to HASHTB the pair consisting of entry E's name to its FIELD."
|
|
(nnreddit--sethash (plist-get e :name) (plist-get e field) hashtb))
|
|
|
|
(defun nnreddit--filter-after (after-this vop)
|
|
"Get elements created AFTER-THIS in VOP (vector of plists)."
|
|
(cl-loop for elt-idx in (funcall nnreddit--seq-map-indexed
|
|
(lambda (elt idx) (cons elt idx)) vop)
|
|
until (>= (plist-get (car elt-idx) :created_utc) after-this)
|
|
finally return (seq-drop vop (or (cdr elt-idx) 0))))
|
|
|
|
(defsubst nnreddit--base10 (base36)
|
|
"Convert BASE36 reddit name encoding to a base10 integer."
|
|
(apply #'+ (funcall nnreddit--seq-map-indexed
|
|
(lambda (elt idx)
|
|
(* (expt 36 idx)
|
|
(if (>= elt ?a) (+ 10 (- elt ?a)) (- elt ?0))))
|
|
(reverse base36))))
|
|
|
|
(deffoo nnreddit-request-group-scan (group &optional server _info)
|
|
"\\[gnus-group-get-new-news-this-group] from *Group* calls this.
|
|
Set flag for the ensuing `nnreddit-request-group' to avoid going out
|
|
to PRAW yet again."
|
|
(nnreddit--normalize-server)
|
|
(nnreddit--with-group group
|
|
(gnus-message 5 "nnreddit-request-group-scan: scanning %s..." group)
|
|
(gnus-activate-group gnus-newsgroup-name t)
|
|
(gnus-message 5 "nnreddit-request-group-scan: scanning %s...done" group)
|
|
t))
|
|
|
|
(defsubst nnreddit--shift-ranges (delta ranges)
|
|
"Shift back by DELTA the elements of RANGES, removing any negative entries."
|
|
(cl-remove-if-not (lambda (e)
|
|
(cond ((numberp e) (> e 0))
|
|
(t (> (cdr e) 0))))
|
|
(mapcar (lambda (e)
|
|
(cond ((numberp e) (- e delta))
|
|
(t `(,(max 1 (- (car e) delta)) .
|
|
,(- (cdr e) delta)))))
|
|
ranges)))
|
|
|
|
;; 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
|
|
;; nnreddit-request-group
|
|
(deffoo nnreddit-request-group (group &optional server _fast info)
|
|
(nnreddit--normalize-server)
|
|
(nnreddit--with-group group
|
|
(let* ((info
|
|
(or info
|
|
(gnus-get-info gnus-newsgroup-name)
|
|
(list gnus-newsgroup-name
|
|
gnus-level-default-subscribed
|
|
nil nil
|
|
(gnus-method-simplify (gnus-group-method gnus-newsgroup-name)))))
|
|
(params (gnus-info-params info))
|
|
(newsrc-read-ranges (gnus-info-read info))
|
|
(newsrc-mark-ranges (gnus-info-marks info))
|
|
(newsrc-seen-cons (gnus-group-parameter-value params 'last-seen t))
|
|
(newsrc-seen-index (car newsrc-seen-cons))
|
|
(newsrc-seen-id (cdr newsrc-seen-cons))
|
|
(headers (nnreddit-get-headers group))
|
|
(num-headers (length headers))
|
|
(status (format "211 %d %d %d %s" num-headers 1 num-headers group)))
|
|
(gnus-message 7 "nnreddit-request-group: %s" status)
|
|
(nnheader-insert "%s\n" status)
|
|
|
|
;; remind myself how this works:
|
|
;; old-praw (1 - 20=emkdjrx)
|
|
;; read-ranges (1 - 10) (15 - 20)
|
|
;; unread-ranges (11, 12, 13, 14)
|
|
;; new-praw (12 13 14 15 16 17 18 19 20 - 100)
|
|
;; 20=emkdjrx in old-praw is 9=emkdjrx in new-praw. index shift is 20-9=+11
|
|
;; new-unread-ranges (0, 1, 2, 3)
|
|
;; new-read-ranges (4 - 9)
|
|
(when (gnus-group-entry gnus-newsgroup-name)
|
|
;; seen-indices are one-indexed !
|
|
(let* ((newsrc-seen-index-now
|
|
(if (or (not (stringp newsrc-seen-id))
|
|
(zerop (nnreddit--base10 newsrc-seen-id)))
|
|
1
|
|
(cl-loop with cand
|
|
for plst in (reverse headers)
|
|
for i = (length headers) then (1- i)
|
|
if (= (nnreddit--base10 (plist-get plst :id))
|
|
(nnreddit--base10 newsrc-seen-id))
|
|
do (gnus-message 7 "nnreddit-request-group: exact=%s" i)
|
|
and return i ;; do not go to finally
|
|
end
|
|
if (> (nnreddit--base10 (plist-get plst :id))
|
|
(nnreddit--base10 newsrc-seen-id))
|
|
do (gnus-message 7 "nnreddit-request-group: cand=%s"
|
|
(setq cand i))
|
|
end
|
|
finally return (or cand 0))))
|
|
(updated-seen-index (- num-headers
|
|
(aif (seq-position
|
|
(reverse headers) nil
|
|
(lambda (plst _e)
|
|
(not (plist-get plst :title))))
|
|
it
|
|
-1)))
|
|
(updated-seen-id (awhen (nth (1- updated-seen-index) headers)
|
|
(plist-get it :id)))
|
|
(delta (if newsrc-seen-index
|
|
(max 0 (- newsrc-seen-index newsrc-seen-index-now))
|
|
0))
|
|
(newsrc-read-ranges-shifted
|
|
(nnreddit--shift-ranges delta newsrc-read-ranges))
|
|
(newsrc-mark-ranges-shifted
|
|
(mapcar (lambda (what-ranges)
|
|
(cl-case (car what-ranges)
|
|
(seen `(seen (1 . ,num-headers)))
|
|
(t (cons (car what-ranges)
|
|
(nnreddit--shift-ranges delta (cdr what-ranges))))))
|
|
newsrc-mark-ranges)))
|
|
(gnus-message 7 "nnreddit-request-group: seen-id=%s seen-index=%s -> %s"
|
|
newsrc-seen-id newsrc-seen-index newsrc-seen-index-now)
|
|
(gnus-message 7 "nnreddit-request-group: seen-id-to-be=%s seen-index-to-be=%s delta=%d"
|
|
updated-seen-id updated-seen-index delta)
|
|
(gnus-message 7 "nnreddit-request-group: read-ranges=%s shifted-read-ranges=%s"
|
|
newsrc-read-ranges newsrc-read-ranges-shifted)
|
|
(gnus-message 7 "nnreddit-request-group: mark-ranges=%s shifted-mark-ranges=%s"
|
|
newsrc-mark-ranges newsrc-mark-ranges-shifted)
|
|
(setf (gnus-info-read info) newsrc-read-ranges-shifted)
|
|
(gnus-info-set-marks info newsrc-mark-ranges-shifted)
|
|
(when updated-seen-id
|
|
(while (assq 'last-seen params)
|
|
(gnus-alist-pull 'last-seen params))
|
|
(gnus-info-set-params
|
|
info
|
|
(cons `(last-seen ,updated-seen-index . ,updated-seen-id) params)
|
|
t))
|
|
(unless (listp (gnus-info-method info))
|
|
(gnus-info-set-method info (gnus-group-method gnus-newsgroup-name) t))
|
|
(gnus-set-info gnus-newsgroup-name info)
|
|
(gnus-message 7 "nnreddit-request-group: new info=%s" info))))
|
|
t))
|
|
|
|
(deffoo nnreddit-request-scan (&optional group server)
|
|
(nnreddit--normalize-server)
|
|
(when group
|
|
(nnreddit--with-group group
|
|
(cond ((string= group (nnreddit--inbox-realname))
|
|
(let ((inbox (nnreddit-rpc-call server nil "inboxes" nnreddit--whoami)))
|
|
(gnus-message 5 "nnreddit-request-scan: %s: +%s inbox"
|
|
group (length inbox))
|
|
(seq-doseq (e inbox)
|
|
(nnreddit-add-entry nnreddit-refs-hashtb e :parent_id)
|
|
(nnreddit-add-entry nnreddit-authors-hashtb e :author))
|
|
(nnreddit-sort-append-headers group inbox)))
|
|
(t
|
|
(let* ((comments (nnreddit-rpc-call server nil "comments" group))
|
|
(raw-submissions (nnreddit-rpc-call server nil "submissions" group))
|
|
(submissions (if (zerop (length comments))
|
|
raw-submissions
|
|
(nnreddit--filter-after
|
|
(- (plist-get (aref comments 0) :created_utc) 7200)
|
|
raw-submissions))))
|
|
(seq-doseq (e comments)
|
|
(nnreddit-add-entry nnreddit-refs-hashtb e :parent_id)) ;; :parent_id is fullname
|
|
(seq-doseq (e (vconcat submissions comments))
|
|
(nnreddit-add-entry nnreddit-authors-hashtb e :author))
|
|
(gnus-message 5 "nnreddit-request-scan: %s: +%s comments +%s submissions"
|
|
group (length comments) (length submissions))
|
|
(nnreddit-sort-append-headers group submissions comments)))))))
|
|
|
|
(defsubst nnreddit--make-message-id (fullname)
|
|
"Construct a valid Gnus message id from FULLNAME."
|
|
(format "<%s@reddit.com>" fullname))
|
|
|
|
(defsubst nnreddit--make-references (fullname)
|
|
"Construct a space delimited string of message ancestors of FULLNAME."
|
|
(mapconcat (lambda (ref) (nnreddit--make-message-id ref))
|
|
(nnreddit-refs-for fullname) " "))
|
|
|
|
(defsubst nnreddit--make-header (article-number &optional group)
|
|
"Construct full headers of articled indexed ARTICLE-NUMBER in GROUP."
|
|
(let* ((header (nnreddit--get-header article-number group))
|
|
(score (plist-get header :score))
|
|
(num-comments (plist-get header :num_comments)))
|
|
(make-full-mail-header
|
|
article-number
|
|
(or (plist-get header :title)
|
|
(concat "Re: " (plist-get header :link_title)))
|
|
(plist-get header :author)
|
|
(format-time-string "%a, %d %h %Y %T %z (%Z)" (plist-get header :created_utc))
|
|
(nnreddit--make-message-id (plist-get header :name))
|
|
(nnreddit--make-references (plist-get header :name))
|
|
0 0 nil
|
|
(append `((X-Reddit-Name . ,(plist-get header :name)))
|
|
`((X-Reddit-ID . ,(plist-get header :id)))
|
|
(awhen (plist-get header :permalink)
|
|
`((X-Reddit-Permalink . ,it)))
|
|
(and (integerp score)
|
|
`((X-Reddit-Score . ,(number-to-string score))))
|
|
(and (integerp num-comments)
|
|
`((X-Reddit-Comments . ,(number-to-string num-comments))))))))
|
|
|
|
(cl-defun nnreddit--request-error (caller
|
|
&key response symbol-status error-thrown
|
|
&allow-other-keys
|
|
&aux (response-status
|
|
(request-response-status-code response)))
|
|
"Refer to CALLER when reporting a submit error.
|
|
Also report http code of RESPONSE, which is distinct from SYMBOL-STATUS,
|
|
and ERROR-THROWN. The http code is stored in RESPONSE-STATUS."
|
|
(gnus-message 3 "%s %s: http status %s, %s"
|
|
caller symbol-status response-status
|
|
(error-message-string error-thrown)))
|
|
|
|
(cl-defun nnreddit--request (caller
|
|
url
|
|
&rest attributes &key parser (backend 'url-retrieve)
|
|
&allow-other-keys)
|
|
"Prefix errors with CALLER when executing synchronous request to URL.
|
|
Request shall contain ATTRIBUTES, one of which is PARSER of the response,
|
|
if provided (shall default to verbatim dump of response, if not).
|
|
BACKEND can be curl (defaults to `url-retrieve')."
|
|
(unless parser
|
|
(setq attributes (nconc attributes (list :parser #'buffer-string))))
|
|
(setq attributes (cl-loop for (k v) on attributes by (function cddr)
|
|
unless (eq k :backend)
|
|
collect k and collect v))
|
|
(let ((request-backend backend))
|
|
(apply #'request url
|
|
:sync t
|
|
:error (apply-partially #'nnreddit--request-error caller)
|
|
attributes)))
|
|
|
|
(cl-defun nnreddit--content-handler
|
|
(&key data response &allow-other-keys
|
|
&aux (header (request-response--raw-header response)))
|
|
"Wrap DATA in uri if RESPONSE has HEADER that is image."
|
|
(let* ((_ (string-match "Content-Type:\\s-*\\([[:graph:]]+\\)" header))
|
|
(content-type (match-string 1 header)))
|
|
(cl-destructuring-bind (type _subtype) (split-string content-type "/")
|
|
(cond ((string= type "image")
|
|
(format "<img src=\"data:%s;base64,%s\" />"
|
|
content-type
|
|
(base64-encode-string (encode-coding-string data 'binary) t)))
|
|
((string= type "text") data)
|
|
(t (error "`nnreddit--content-handler': passing on %s" content-type))))))
|
|
|
|
(defmacro nnreddit--concat (thus-far &rest add)
|
|
"Assign to THUS-FAR the catenation of itself and ADD."
|
|
`(setq ,thus-far (apply #'concat ,thus-far (list ,@add))))
|
|
|
|
(deffoo nnreddit-request-article (article-number &optional group server buffer)
|
|
(nnreddit--normalize-server)
|
|
(nnreddit--with-group group
|
|
(with-current-buffer (or buffer nntp-server-buffer)
|
|
(erase-buffer)
|
|
(let* ((header (nnreddit--get-header article-number group))
|
|
(mail-header (nnreddit--make-header article-number))
|
|
(score (cdr (assq 'X-Reddit-Score (mail-header-extra mail-header))))
|
|
(permalink (cdr (assq 'X-Reddit-Permalink (mail-header-extra mail-header))))
|
|
(body (awhen (plist-get header :name)
|
|
(nnreddit--get-body it group server))))
|
|
(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"
|
|
(if permalink
|
|
(format "Archived-at: <https://www.reddit.com%s>\n"
|
|
permalink)
|
|
"")
|
|
"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-name (plist-get header :parent_id)) ;; parent_id is full
|
|
(parent-author (or (nnreddit--gethash parent-name nnreddit-authors-hashtb)
|
|
"Someone"))
|
|
(parent-body (nnreddit--get-body parent-name group server)))
|
|
(insert (nnreddit--citation-wrap parent-author parent-body)))
|
|
(aif (and nnreddit-render-submission
|
|
(eq (plist-get header :is_self) :json-false)
|
|
(plist-get header :url))
|
|
(condition-case err
|
|
(nnreddit--request
|
|
"nnreddit-request-article" it
|
|
:success
|
|
(lambda (&rest args)
|
|
(let ((data (apply #'nnreddit--content-handler args)))
|
|
(if (> (length data) nnreddit-max-render-bytes)
|
|
(insert (nnreddit--br-tagify body))
|
|
(insert data)))))
|
|
(error (gnus-message 5 "nnreddit-request-article: %s %s"
|
|
it (error-message-string err))
|
|
(insert (nnreddit--br-tagify body))))
|
|
(insert (nnreddit--br-tagify body)))
|
|
(insert "\n")
|
|
(if (mml-validate)
|
|
(message-encode-message-body)
|
|
(gnus-message 2 "nnreddit-request-article: Invalid mml:\n%s"
|
|
(buffer-string)))
|
|
(cons group article-number))))))
|
|
|
|
(deffoo nnreddit-request-head (_id &optional _group _server)
|
|
"Demur.
|
|
Since `gnus-summary-refer-article' calls
|
|
`gnus-summary-select-article' on an on-demand retrieval,
|
|
`nnreddit--get-body' won't cut it."
|
|
nil)
|
|
|
|
(deffoo nnreddit-retrieve-headers (article-numbers &optional group server _fetch-old)
|
|
(nnreddit--normalize-server)
|
|
(nnreddit--with-group group
|
|
(with-current-buffer nntp-server-buffer
|
|
(erase-buffer)
|
|
(dolist (i article-numbers)
|
|
(nnheader-insert-nov (nnreddit--make-header i group)))
|
|
'nov)))
|
|
|
|
(defsubst nnreddit--earliest-among (indices lvp)
|
|
"Return (list-to-iterate . next-earliest) from INDICES.
|
|
INDICES are thus far iterators.
|
|
LVP is a list of vectors of plists.
|
|
Used in the interleaving of submissions and comments."
|
|
(let (earliest next-earliest)
|
|
(dolist (plst-idx
|
|
(cl-remove-if-not #'car
|
|
(funcall nnreddit--seq-map-indexed
|
|
(lambda (plst idx) (cons plst idx))
|
|
(seq-mapn
|
|
(lambda (v i)
|
|
(if (< i (length v)) (aref v i)))
|
|
lvp indices)))
|
|
(list (cdr earliest)
|
|
(awhen next-earliest
|
|
(plist-get (car it) :created_utc))))
|
|
(cond ((null earliest)
|
|
(setq earliest plst-idx))
|
|
((< (plist-get (car plst-idx) :created_utc)
|
|
(plist-get (car earliest) :created_utc))
|
|
(setq next-earliest earliest)
|
|
(setq earliest plst-idx))
|
|
((null next-earliest)
|
|
(setq next-earliest plst-idx))))))
|
|
|
|
(defun nnreddit--sort-headers (&rest lvp)
|
|
"Sort headers for LVP (list of vectors of plists)."
|
|
(let* ((indices (make-list (length lvp) 0))
|
|
result)
|
|
(while (not (equal indices (mapcar #'length lvp)))
|
|
(cl-destructuring-bind (to-iterate bogey-created)
|
|
(nnreddit--earliest-among indices lvp)
|
|
(cl-loop with arr = (elt lvp to-iterate)
|
|
for j in (number-sequence (elt indices to-iterate) (1- (length arr)))
|
|
for plst = (aref arr j)
|
|
for created = (plist-get plst :created_utc)
|
|
until (> created (or bogey-created most-positive-fixnum))
|
|
do (cl-incf (elt indices to-iterate))
|
|
do (push plst result))))
|
|
(nreverse result)))
|
|
|
|
(deffoo nnreddit-close-server (&optional server _defs)
|
|
(nnreddit--normalize-server)
|
|
(condition-case err
|
|
(progn (nnreddit-rpc-kill server) t)
|
|
(error
|
|
(gnus-message 2 "nnreddit-close-server: %s" (error-message-string err))
|
|
nil)))
|
|
|
|
(deffoo nnreddit-request-list (&optional server)
|
|
(nnreddit--normalize-server)
|
|
(with-current-buffer nntp-server-buffer
|
|
(let ((groups (delq
|
|
nil
|
|
(or nnreddit--groups
|
|
(setq nnreddit--groups
|
|
(append (nnreddit-rpc-call server nil "user_subreddits")
|
|
(nnreddit--test-supports-inbox
|
|
(list (nnreddit--inbox-realname))))))))
|
|
(newsrc (cl-mapcan (lambda (info)
|
|
(when (and (equal "nnreddit:" (gnus-info-method info))
|
|
(<= (gnus-info-level info)
|
|
gnus-level-subscribed))
|
|
(list (gnus-info-group info))))
|
|
gnus-newsrc-alist)))
|
|
(mapc (lambda (realname)
|
|
(let ((group (gnus-group-full-name realname '("nnreddit" (or server "")))))
|
|
(erase-buffer)
|
|
(gnus-message 5 "nnreddit-request-list: scanning %s..." realname)
|
|
(gnus-activate-group group t)
|
|
(gnus-message 5 "nnreddit-request-list: scanning %s...done" realname)
|
|
(when (> (gnus-group-level group) gnus-level-subscribed)
|
|
(gnus-group-unsubscribe-group group gnus-level-default-subscribed t))
|
|
(setq newsrc (cl-remove group newsrc :test #'string=))))
|
|
groups)
|
|
(mapc (lambda (fullname)
|
|
(gnus-message 4 "nnreddit-request-list: missing subscription %s" fullname)
|
|
(nnreddit-rpc-call nil nil "subscribe" (gnus-group-real-name fullname))
|
|
(gnus-activate-group fullname t))
|
|
newsrc)
|
|
(erase-buffer)
|
|
(mapc (lambda (group)
|
|
(insert (format "%s %d 1 y\n" group
|
|
(length (nnreddit-get-headers group)))))
|
|
groups)))
|
|
t)
|
|
|
|
(defun nnreddit-sentinel (process event)
|
|
"Wipe headers state when PROCESS dies from EVENT."
|
|
(unless (string= "open" (substring event 0 4))
|
|
(gnus-message 2 "nnreddit-sentinel: process %s %s"
|
|
(car (process-command process))
|
|
(replace-regexp-in-string "\n$" "" event))
|
|
(setq nnreddit-headers-hashtb (gnus-make-hashtable))
|
|
(setq nnreddit-processes (cl-remove-if (lambda (other) (string= (process-name process)
|
|
(process-name other)))
|
|
nnreddit-processes))
|
|
(gnus-backlog-shutdown)))
|
|
|
|
(defun nnreddit--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 (nnreddit-string-trim-right
|
|
(substring string (length magic))
|
|
"\n")))))
|
|
|
|
(defsubst nnreddit--install-failed ()
|
|
"If we can't install the virtualenv then all bets are off."
|
|
(string= nnreddit-venv "/dev/null"))
|
|
|
|
(defun nnreddit-dump-diagnostics (&optional server)
|
|
"Makefile recipe test-run. SERVER is usually nnreddit-default."
|
|
(nnreddit--normalize-server)
|
|
(dolist (b `(,byte-compile-log-buffer
|
|
,gnus-group-buffer
|
|
"*Messages*"
|
|
,(format " *%s*" server)
|
|
,(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))))
|
|
|
|
(defsubst nnreddit--cripple-fight-p ()
|
|
"The albatross of a single-threaded event loop hangs heavy on gnus and exwm.
|
|
As a result, each deadlocks the other in a race to the bottom between two
|
|
relics of the 1990s."
|
|
(and (boundp 'exwm--connection) exwm--connection))
|
|
|
|
(defun nnreddit-rpc-get (&optional server)
|
|
"Retrieve the PRAW process for SERVER."
|
|
(nnreddit--normalize-server)
|
|
(unless (nnreddit--install-failed)
|
|
(let ((proc (get-buffer-process (get-buffer-create (format " *%s*" server)))))
|
|
(unless proc
|
|
(let* ((nnreddit-el-dir (directory-file-name (file-name-directory (locate-library "nnreddit"))))
|
|
(nnreddit-py-dir (directory-file-name
|
|
(if (string= "lisp" (file-name-base nnreddit-el-dir))
|
|
(file-name-directory nnreddit-el-dir)
|
|
nnreddit-el-dir)))
|
|
(python-shell-extra-pythonpaths (list nnreddit-py-dir))
|
|
(process-environment
|
|
(funcall (if (fboundp 'python-shell--calculate-process-environment)
|
|
(symbol-function 'python-shell--calculate-process-environment)
|
|
(symbol-function 'python-shell-calculate-process-environment))))
|
|
(python-executable (if nnreddit-venv
|
|
(format "%s/bin/python" nnreddit-venv)
|
|
(executable-find nnreddit-python-command)))
|
|
(python-module (if (featurep 'nnreddit-test) "tests" "nnreddit"))
|
|
(praw-command (append (list python-executable "-m" python-module)
|
|
nnreddit--python-module-extra-args)))
|
|
(unless (featurep 'nnreddit-test)
|
|
(setq praw-command (append praw-command (list "--localhost" nnreddit-localhost)))
|
|
(when nnreddit-log-rpc
|
|
(setq nnreddit-rpc-log-filename
|
|
(concat (file-name-as-directory temporary-file-directory)
|
|
"nnreddit-rpc-log."))
|
|
(setq praw-command (append praw-command
|
|
(list "--log" nnreddit-rpc-log-filename)))))
|
|
(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))))
|
|
(with-current-buffer (get-buffer-create (format " *%s-stderr*" server))
|
|
(add-hook 'after-change-functions
|
|
(apply-partially 'nnreddit--message-user server)
|
|
nil t)))
|
|
(push proc nnreddit-processes)
|
|
(when (nnreddit--cripple-fight-p)
|
|
(error "`nnreddit-rpc-get': Under EXWM, authentication must be out-of-band")))
|
|
proc)))
|
|
|
|
(defmacro nnreddit--with-mutex (mtx &rest body)
|
|
"If capable of threading, lock with MTX and execute BODY."
|
|
(declare (indent 1))
|
|
(if (fboundp 'with-mutex)
|
|
`(with-mutex ,mtx ,@body)
|
|
`(progn ,@body)))
|
|
|
|
(defvar nnreddit--mutex-rpc-request (when (fboundp 'make-mutex)
|
|
(make-mutex "nnreddit--mutex-rpc-request"))
|
|
"Only one jsonrpc output buffer, so avoid two requests using at the same time.")
|
|
|
|
(defun nnreddit-rpc-request (connection kwargs method &rest args)
|
|
"Send to CONNECTION a request with generator KWARGS calling METHOD ARGS.
|
|
Library `json-rpc--request' assumes HTTP transport which jsonrpyc does not,
|
|
so we make our own."
|
|
(unless (hash-table-p kwargs)
|
|
(setq kwargs #s(hash-table)))
|
|
(let* ((id (cl-incf (json-rpc-id-counter connection)))
|
|
(request `(:method ,method
|
|
:id ,id
|
|
:params (:args ,(apply json-array-type args) :kwargs ,kwargs)))
|
|
(proc (json-rpc-process (json-rpc-ensure connection)))
|
|
(encoded (json-encode (append '(:jsonrpc "2.0") request)))
|
|
(json-object-type 'plist)
|
|
(json-key-type 'keyword)
|
|
(iteration-seconds 6))
|
|
(nnreddit--with-mutex nnreddit--mutex-rpc-request
|
|
(with-current-buffer (process-buffer proc)
|
|
(erase-buffer)
|
|
(gnus-message 7 "nnreddit-rpc-request: send %s" encoded)
|
|
(process-send-string proc (concat encoded "\n"))
|
|
(cl-loop repeat (/ nnreddit-rpc-request-timeout iteration-seconds)
|
|
with result
|
|
until (or (not (json-rpc-live-p connection))
|
|
(and (not (zerop (length (buffer-string))))
|
|
(condition-case err
|
|
(setq result (json-read-from-string (buffer-string)))
|
|
(error
|
|
(let* ((resp (if (< (length (buffer-string)) 100)
|
|
(buffer-string)
|
|
(format "%s...%s"
|
|
(cl-subseq (buffer-string) 0 50)
|
|
(cl-subseq (buffer-string) -50)))))
|
|
(setq result
|
|
`(:error ,(format "%s on %s"
|
|
(error-message-string err)
|
|
resp))))
|
|
nil))))
|
|
do (when (fboundp 'set-process-thread)
|
|
(set-process-thread proc nil))
|
|
do (accept-process-output proc iteration-seconds 0)
|
|
finally return
|
|
(cond ((null result)
|
|
(error "`nnreddit-rpc-request': response timed out"))
|
|
((plist-get result :error)
|
|
(error "`nnreddit-rpc-request': %s" (plist-get result :error)))
|
|
(t
|
|
(gnus-message 7 "`nnreddit-rpc-request': recv ...%s"
|
|
(cl-subseq (buffer-string)
|
|
(- (min (length (buffer-string)) 50))))
|
|
(plist-get result :result))))))))
|
|
|
|
(defsubst nnreddit--extract-name (from)
|
|
"String match on something looking like t1_es076hd in FROM."
|
|
(and (stringp from) (string-match "\\(t[0-9]+_[a-z0-9]+\\)" from) (match-string 1 from)))
|
|
|
|
;; 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
|
|
;; nnreddit-request-post
|
|
(deffoo nnreddit-request-post (&optional server)
|
|
(nnreddit--normalize-server)
|
|
(let* ((ret t)
|
|
(kwargs (make-hash-table))
|
|
(title (or (message-fetch-field "Subject")
|
|
(error "`nnreddit-request-post': no subject field")))
|
|
(link (message-fetch-field "Link"))
|
|
(reply-p (not (null message-reply-headers)))
|
|
(edit-name (nnreddit--extract-name (message-fetch-field "Supersedes")))
|
|
(cancel-name (nnreddit--extract-name (message-fetch-field "Control")))
|
|
(root-p (message-fetch-field "Reply-Root"))
|
|
(article-number (nnreddit--current-article-number))
|
|
(group (if (numberp article-number)
|
|
(gnus-group-real-name (nnreddit--current-group))
|
|
(or (message-fetch-field "Newsgroups")
|
|
(error "`nnreddit-request-post': no newsgroups field"))))
|
|
(header (when (numberp article-number)
|
|
(nnreddit--get-header article-number group)))
|
|
(body
|
|
(save-excursion
|
|
(save-restriction
|
|
(message-goto-body)
|
|
(narrow-to-region (point) (point-max))
|
|
(buffer-string)))))
|
|
(cond (cancel-name (nnreddit-rpc-call server nil "remove" cancel-name))
|
|
(edit-name (nnreddit-rpc-call server nil "edit" edit-name body))
|
|
(reply-p (if (and header (plist-get header :name))
|
|
(nnreddit-rpc-call server nil "reply"
|
|
(plist-get header :name)
|
|
body (stringp root-p))
|
|
(backtrace)
|
|
(error "`nnreddit-request-post': no current article, header=%s name=%s"
|
|
header
|
|
(when header (plist-get header :name)))))
|
|
(link (let* ((parsed-url (url-generic-parse-url link))
|
|
(host (url-host parsed-url)))
|
|
(if (and (stringp host) (not (zerop (length host))))
|
|
(progn
|
|
(puthash 'url link kwargs)
|
|
(nnreddit-rpc-call server kwargs "submit" group title))
|
|
;; gnus-error might be better here
|
|
(error "`nnreddit-request-post': invalid url \"%s\"" link)
|
|
(setq ret nil))))
|
|
(t (puthash 'selftext body kwargs)
|
|
(nnreddit-rpc-call server kwargs "submit" group title)))
|
|
ret))
|
|
|
|
(defun nnreddit--browse-root (&rest _args)
|
|
"What happens when I click on Subject."
|
|
(-when-let* ((article-number (nnreddit--current-article-number))
|
|
(group (gnus-group-real-name (nnreddit--current-group)))
|
|
(header (nnreddit--get-header article-number group))
|
|
(permalink (plist-get header :permalink)))
|
|
(cl-loop for name in (nnreddit-refs-for (plist-get header :name))
|
|
for header1 = (nnreddit-find-header
|
|
group (nnreddit-hack-name-to-id name))
|
|
for permalink1 = (plist-get header1 :permalink)
|
|
until permalink1
|
|
finally (browse-url (format "https://www.reddit.com%s"
|
|
(or permalink1 permalink ""))))))
|
|
|
|
(defun nnreddit--header-button-alist ()
|
|
"Construct a buffer-local `gnus-header-button-alist' for nnreddit."
|
|
(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:" ".+" 0 (>= gnus-button-browse-level 0)
|
|
nnreddit--browse-root 0)
|
|
result)
|
|
result))
|
|
|
|
(defun nnreddit-sort-by-number-of-articles-in-thread (t1 t2)
|
|
"Whichever of the T1 or T2 has the most articles."
|
|
(> (gnus-summary-number-of-articles-in-thread t1)
|
|
(gnus-summary-number-of-articles-in-thread t2)))
|
|
|
|
(defun nnreddit-gather-threads-by-references (threads)
|
|
"Gather THREADS by root reference, and don't be incomprehensible or buggy.
|
|
The built-in `gnus-gather-threads-by-references' is both."
|
|
(cl-flet ((special-case
|
|
(thread)
|
|
(let ((header (cl-first thread)))
|
|
(if (stringp header)
|
|
thread
|
|
(list (mail-header-subject header) thread))))
|
|
(has-refs
|
|
(thread)
|
|
(let ((header (cl-first thread)))
|
|
(gnus-split-references (mail-header-references header)))))
|
|
(let ((threads-by-ref (gnus-make-hashtable))
|
|
(separated (-separate #'has-refs threads))
|
|
result)
|
|
(dolist (thread (cl-second separated))
|
|
(let* ((header (cl-first thread))
|
|
(id (mail-header-id header))
|
|
(thread-special (special-case thread)))
|
|
(push thread-special result)
|
|
(nnreddit--sethash id thread-special threads-by-ref)))
|
|
(dolist (thread (cl-first separated))
|
|
(let* ((header (cl-first thread))
|
|
(refs (gnus-split-references (mail-header-references header)))
|
|
(ref-thread (cl-some (lambda (ref)
|
|
(nnreddit--gethash ref threads-by-ref))
|
|
refs)))
|
|
(if ref-thread
|
|
(setcdr ref-thread (nconc (cdr ref-thread) (list thread)))
|
|
(setq ref-thread (special-case thread))
|
|
(push ref-thread result)
|
|
(nnreddit--sethash (car refs) ref-thread threads-by-ref))))
|
|
(nreverse result))))
|
|
|
|
(defun nnreddit--fallback-link ()
|
|
"Cannot render submission."
|
|
(when-let ((current-group (nnreddit--current-group))
|
|
(current-article (nnreddit--current-article-number)))
|
|
(let* ((group (gnus-group-real-name current-group))
|
|
(header (nnreddit--get-header current-article group))
|
|
(body (awhen (plist-get header :name) (nnreddit--get-body it group))))
|
|
(with-current-buffer gnus-original-article-buffer
|
|
(article-goto-body)
|
|
(delete-region (point) (point-max))
|
|
(when body
|
|
(insert (nnreddit--br-tagify body)))))))
|
|
|
|
(defalias 'nnreddit--display-article
|
|
(lambda (article &optional all-headers _header)
|
|
(condition-case err
|
|
(gnus-article-prepare article all-headers)
|
|
(error
|
|
(if nnreddit-render-submission
|
|
(progn
|
|
(gnus-message 7 "nnreddit--display-article: '%s' (falling back...)"
|
|
(error-message-string err))
|
|
(nnreddit--fallback-link)
|
|
(gnus-article-prepare article all-headers))
|
|
(error (error-message-string err))))))
|
|
"In case of shr failures, dump original link.")
|
|
|
|
(defsubst nnreddit--dense-time (time*)
|
|
"Convert TIME to a floating point number.
|
|
Written by John Wiegley (https://github.com/jwiegley/dot-emacs)."
|
|
(let ((time (if (fboundp 'time-convert)
|
|
(funcall #'time-convert time* 'list)
|
|
(identity time*))))
|
|
(+ (* (car time) 65536.0)
|
|
(cadr time)
|
|
(/ (or (car (cdr (cdr time))) 0) 1000000.0))))
|
|
|
|
(defalias 'nnreddit--format-time-elapsed
|
|
(lambda (header)
|
|
(condition-case nil
|
|
(let ((date (mail-header-date header)))
|
|
(if (> (length date) 0)
|
|
(let*
|
|
((then (nnreddit--dense-time
|
|
(apply #'encode-time (parse-time-string date))))
|
|
(now (nnreddit--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 package-lint!
|
|
(fset 'gnus-user-format-function-S
|
|
(symbol-function 'nnreddit--format-time-elapsed))
|
|
|
|
(add-to-list
|
|
'gnus-parameters
|
|
`("^nnreddit"
|
|
(gnus-refer-article-method 'current)
|
|
(gnus-summary-make-false-root 'adopt)
|
|
(gnus-cite-hide-absolute 5)
|
|
(gnus-cite-hide-percentage 0)
|
|
(gnus-cited-lines-visible '(2 . 2))
|
|
(gnus-auto-extend-newsgroup nil)
|
|
(gnus-add-timestamp-to-message t)
|
|
(gnus-summary-line-format "%3t%U%R%uS %I%(%*%-10,10f %s%)\n")
|
|
(gnus-thread-sort-functions (quote (nnreddit-sort-by-number-of-articles-in-thread)))
|
|
(gnus-subthread-sort-functions (quote (gnus-thread-sort-by-number)))
|
|
(gnus-summary-display-article-function
|
|
(quote ,(symbol-function 'nnreddit--display-article)))
|
|
(gnus-header-button-alist
|
|
(quote ,(nnreddit--header-button-alist)))
|
|
(gnus-visible-headers ,(concat gnus-visible-headers "\\|^Score:"))))
|
|
|
|
(nnoo-define-skeleton nnreddit)
|
|
|
|
(defun nnreddit-article-mode-activate ()
|
|
"Augment the `gnus-article-mode-map' conditionally."
|
|
(when (nnreddit--gate)
|
|
(nnreddit-article-mode)))
|
|
|
|
(defun nnreddit-summary-mode-activate ()
|
|
"Shadow some bindings in `gnus-summary-mode-map' conditionally."
|
|
(when (nnreddit--gate)
|
|
(nnreddit-summary-mode)))
|
|
|
|
(defun nnreddit-group-mode-activate ()
|
|
"Augment the variable `gnus-group-mode-map' unconditionally."
|
|
(if gnus-group-change-level-function
|
|
(add-function :after gnus-group-change-level-function
|
|
#'nnreddit-update-subscription)
|
|
(setq gnus-group-change-level-function #'nnreddit-update-subscription)))
|
|
|
|
(defun nnreddit--who-am-i ()
|
|
"User@reddit.com for matching in `message-is-yours-p'."
|
|
(concat (if (and noninteractive (not nnreddit--whoami))
|
|
"nnreddit-user"
|
|
nnreddit--whoami)
|
|
"@reddit.com"))
|
|
|
|
(defun nnreddit--fix-from ()
|
|
"Must fix the From header, always."
|
|
(when (nnreddit--message-gate)
|
|
(save-excursion
|
|
(message-replace-header
|
|
"From"
|
|
(nnreddit--who-am-i)))))
|
|
|
|
;; I believe I did try buffer-localizing hooks, and it wasn't sufficient
|
|
(add-hook 'gnus-article-mode-hook 'nnreddit-article-mode-activate)
|
|
(add-hook 'gnus-group-mode-hook 'nnreddit-group-mode-activate)
|
|
(add-hook 'gnus-summary-mode-hook 'nnreddit-summary-mode-activate)
|
|
(add-hook 'gnus-message-setup-hook #'nnreddit--fix-from)
|
|
|
|
;; `gnus-newsgroup-p' requires valid method post-mail to return t
|
|
(add-to-list 'gnus-valid-select-methods '("nnreddit" post-mail) t)
|
|
|
|
;; Add prompting for replying to thread root to gnus-summary-followup.
|
|
;; The interactive spec of gnus-summary-followup is putatively preserved.
|
|
(let* ((prompt-loose
|
|
(lambda (f &rest args)
|
|
(cond ((nnreddit--gate)
|
|
(nnreddit--with-group nil
|
|
(when (string= group (nnreddit--inbox-realname))
|
|
(error "Followup from inbox not implemented")))
|
|
(or (-when-let*
|
|
((article-number (gnus-summary-article-number))
|
|
(header (nnreddit--get-header article-number))
|
|
(root-name (car (nnreddit-refs-for (plist-get header :name))))
|
|
(rootless (or (not (stringp root-name))
|
|
(not (string-prefix-p "t3_" root-name))
|
|
(not (nnreddit-find-header
|
|
(gnus-group-real-name gnus-newsgroup-name)
|
|
(nnreddit-hack-name-to-id root-name)))))
|
|
(reply-root (read-char-choice
|
|
"Reply loose thread [m]essage or [r]oot: " '(?m ?r)))
|
|
(q-root (eq reply-root ?r)))
|
|
(let* ((link-header (apply-partially #'message-add-header
|
|
"Reply-Root: yes"))
|
|
(add-link-header (apply-partially #'add-hook
|
|
'message-header-setup-hook
|
|
link-header))
|
|
(remove-link-header (apply-partially #'remove-hook
|
|
'message-header-setup-hook
|
|
link-header)))
|
|
(funcall add-link-header)
|
|
(condition-case err
|
|
(progn
|
|
(apply f args)
|
|
(funcall remove-link-header))
|
|
(error (funcall remove-link-header)
|
|
(error (error-message-string err)))))
|
|
t)
|
|
(apply f args)))
|
|
(t (apply f args)))))
|
|
(advise-gnus-summary-followup
|
|
(lambda ()
|
|
(add-function :around (symbol-function 'gnus-summary-followup) prompt-loose)))
|
|
(suspend-prompt-loose
|
|
(lambda (f &rest args)
|
|
(cond ((nnreddit--gate)
|
|
(remove-function (symbol-function 'gnus-summary-followup) prompt-loose)
|
|
(unwind-protect
|
|
(apply f args)
|
|
(funcall advise-gnus-summary-followup)))
|
|
(t (apply f args)))))
|
|
(advise-gnus-summary-cancel-article
|
|
(lambda ()
|
|
(add-function :around (symbol-function 'gnus-summary-cancel-article)
|
|
suspend-prompt-loose))))
|
|
(funcall advise-gnus-summary-cancel-article)
|
|
(funcall advise-gnus-summary-followup))
|
|
|
|
(add-function
|
|
:around (symbol-function 'message-supersede)
|
|
(lambda (f &rest args)
|
|
(cond ((nnreddit--message-gate)
|
|
(add-function :override
|
|
(symbol-function 'mml-insert-mml-markup)
|
|
'ignore)
|
|
(unwind-protect
|
|
(prog1 (apply f args)
|
|
(remove-function (symbol-function 'mml-insert-mml-markup) 'ignore)
|
|
(save-excursion
|
|
(save-restriction
|
|
(nnreddit--fix-from)
|
|
(message-goto-body)
|
|
(narrow-to-region (point) (point-max))
|
|
(goto-char (point-max))
|
|
(mm-inline-text-html nil)
|
|
(delete-region (point-min) (point)))))
|
|
(remove-function (symbol-function 'mml-insert-mml-markup) 'ignore)))
|
|
(t (apply f args)))))
|
|
|
|
(add-function
|
|
:around (symbol-function 'message-send-news)
|
|
(lambda (f &rest args)
|
|
(cond ((nnreddit--message-gate)
|
|
(let* ((dont-ask (lambda (prompt)
|
|
(when (cl-search "mpty article" prompt) t)))
|
|
(link-p (message-fetch-field "Link"))
|
|
(message-shoot-gnksa-feet (if link-p t message-shoot-gnksa-feet))
|
|
(message-inhibit-body-encoding t))
|
|
(unwind-protect
|
|
(progn
|
|
(when link-p
|
|
(add-function :before-until (symbol-function 'y-or-n-p) dont-ask))
|
|
(apply f args))
|
|
(remove-function (symbol-function 'y-or-n-p) dont-ask))))
|
|
(t (apply f args)))))
|
|
|
|
(add-function
|
|
:around (symbol-function 'gnus-summary-post-news)
|
|
(lambda (f &rest args)
|
|
(cond ((nnreddit--gate)
|
|
(let* ((nnreddit-post-type (read-char-choice "[l]ink / [t]ext: " '(?l ?t)))
|
|
(link-header (apply-partially #'message-add-header "Link: https://"))
|
|
(add-link-header (apply-partially #'add-hook
|
|
'message-header-setup-hook
|
|
link-header))
|
|
(remove-link-header (apply-partially #'remove-hook
|
|
'message-header-setup-hook
|
|
link-header)))
|
|
(cl-case nnreddit-post-type
|
|
(?l (funcall add-link-header)))
|
|
(unwind-protect
|
|
(prog1 (apply f args))
|
|
(funcall remove-link-header))))
|
|
(t (apply f args)))))
|
|
|
|
(add-function
|
|
:filter-return (symbol-function 'message-make-fqdn)
|
|
(lambda (val)
|
|
(if (and (nnreddit--message-gate)
|
|
(cl-search "--so-tickle-me" val))
|
|
"reddit.com"
|
|
val)))
|
|
|
|
(add-function
|
|
:around (symbol-function 'message-is-yours-p)
|
|
(lambda (f &rest args)
|
|
(let ((concat-func (lambda (f &rest args)
|
|
(let ((fetched (apply f args)))
|
|
(if (string= (car args) "from")
|
|
(concat fetched "@reddit.com")
|
|
fetched)))))
|
|
(when (nnreddit--message-gate)
|
|
(add-function :around
|
|
(symbol-function 'message-fetch-field)
|
|
concat-func)
|
|
(add-function :override
|
|
(symbol-function 'message-make-from)
|
|
#'nnreddit--who-am-i))
|
|
(unwind-protect
|
|
(apply f args)
|
|
(remove-function (symbol-function 'message-fetch-field) concat-func)
|
|
(remove-function (symbol-function 'message-make-from) #'nnreddit--who-am-i)))))
|
|
|
|
(add-function
|
|
:around (symbol-function 'url-http-generic-filter)
|
|
(lambda (f &rest args)
|
|
(cond ((nnreddit--gate)
|
|
(condition-case err
|
|
(apply f args)
|
|
(error (gnus-message 7 "url-http-generic-filter: %s"
|
|
(error-message-string err)))))
|
|
(t (apply f args)))))
|
|
|
|
;; 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 ((nnreddit--gate)
|
|
(let ((gnus-summary-display-article-function
|
|
(symbol-function 'nnreddit--display-article)))
|
|
(apply f args)))
|
|
(t (apply f args)))))
|
|
|
|
;; Lars rejected my change for vectorizing `gnus-group-change-level-functions'
|
|
(add-function
|
|
:after (symbol-function 'gnus-topic-change-level)
|
|
(lambda (&rest args)
|
|
;; nnreddit-update-subscription calls nnreddit--gate
|
|
(apply #'nnreddit-update-subscription args)))
|
|
|
|
;; disallow caching as the article numbering is wont to change
|
|
;; after PRAW restarts!
|
|
(setq gnus-uncacheable-groups
|
|
(aif gnus-uncacheable-groups
|
|
(format "\\(%s\\)\\|\\(^nnreddit\\)" it)
|
|
"^nnreddit"))
|
|
|
|
(provide 'nnreddit)
|
|
|
|
;;; nnreddit.el ends here
|