2012-07-25 01:49:02 +02:00
|
|
|
;;; avandu.el --- Gateway to Tiny Tiny RSS
|
|
|
|
|
|
|
|
;; Copyright (C) 2012 Tom Willemsen <tom@ryuslash.org>
|
|
|
|
|
|
|
|
;; Author: Tom Willemsen <tom@ryuslash.org>
|
|
|
|
;; Created: Jul 22, 2012
|
|
|
|
;; Version: 0
|
|
|
|
;; Keywords: net
|
|
|
|
|
|
|
|
;; Permission to use, copy, modify, and distribute this software for any
|
|
|
|
;; purpose with or without fee is hereby granted, provided that the
|
|
|
|
;; above copyright notice and this permission notice appear in all
|
|
|
|
;; copies.
|
|
|
|
|
|
|
|
;; THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL
|
|
|
|
;; WARRANTIES WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED
|
|
|
|
;; WARRANTIES OF MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE
|
|
|
|
;; AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, INDIRECT, OR
|
|
|
|
;; CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS
|
|
|
|
;; OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT,
|
|
|
|
;; NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF OR IN
|
|
|
|
;; CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
|
|
|
|
|
|
|
;;; Commentary:
|
|
|
|
|
2012-08-22 12:44:50 +02:00
|
|
|
;; Avandu is an emacs mode that connects to a Tiny Tiny RSS
|
|
|
|
;; (http://tt-rss.org) instance and allows you to read the feeds it
|
|
|
|
;; has gathered.
|
2012-08-02 22:22:34 +02:00
|
|
|
|
|
|
|
;; The simplest way to install it is to use package.el:
|
|
|
|
|
|
|
|
;; (package-install-file "/path/to/avandu.el")
|
|
|
|
|
|
|
|
;; For further information I would like to refer you to the avandu
|
|
|
|
;; info file.
|
|
|
|
|
|
|
|
;; Once installation is out of the way, it should get a value for
|
|
|
|
;; `avandu-tt-rss-api-url' (for example: http://tt-rss.org/demo/api/)
|
2012-08-04 16:10:42 +02:00
|
|
|
;; and then run `avandu-overview'.
|
2012-08-02 22:22:34 +02:00
|
|
|
|
|
|
|
;; Once in avandu:overview mode some key bindings will be:
|
|
|
|
|
|
|
|
;; - `r' :: Mark article at point as read.
|
|
|
|
;; - `o' :: Open article at point in a browser. Uses `browse-url'.
|
|
|
|
;; - `n' :: Next article.
|
|
|
|
;; - `p' :: Previous article.
|
2012-07-25 01:49:02 +02:00
|
|
|
|
|
|
|
;;; Code:
|
2012-08-07 09:47:43 +02:00
|
|
|
(require 'auth-source)
|
2012-07-25 01:49:02 +02:00
|
|
|
(require 'json)
|
2012-08-02 22:24:45 +02:00
|
|
|
(require 'simple)
|
2012-08-07 09:47:43 +02:00
|
|
|
(require 'url)
|
2012-07-25 01:49:02 +02:00
|
|
|
|
2012-08-03 22:46:15 +02:00
|
|
|
(defconst avandu-entity-replacement-alist
|
|
|
|
'(("hellip" . 8230)
|
|
|
|
("qout" . 34)
|
|
|
|
("amp" . 38)
|
|
|
|
("nbsp" . 32))
|
|
|
|
"What to replace the part between & and ; of HTML entities with
|
|
|
|
names.")
|
|
|
|
|
2012-08-04 00:30:15 +02:00
|
|
|
(defconst avandu-overview-mode-name "Avandu:Overview"
|
|
|
|
"The default name for `avandu-overview-mode'.")
|
|
|
|
|
2012-08-03 23:58:15 +02:00
|
|
|
(defconst avandu-version 0
|
|
|
|
"The current version of avandu.")
|
|
|
|
|
2012-08-03 22:46:15 +02:00
|
|
|
;; Customization
|
2012-07-25 01:49:02 +02:00
|
|
|
(defgroup avandu nil
|
2012-07-26 00:43:30 +02:00
|
|
|
"Tiny Tiny RSS interface for emacs."
|
2012-07-25 01:49:02 +02:00
|
|
|
:group 'applications)
|
|
|
|
|
2012-08-03 22:46:15 +02:00
|
|
|
;; Faces
|
|
|
|
(defface avandu-overview-excerpt
|
|
|
|
'((t (:inherit shadow :slant italic)))
|
|
|
|
"Face for article excerpts in avandu overview."
|
|
|
|
:group 'avandu)
|
|
|
|
|
2012-07-25 01:49:02 +02:00
|
|
|
(defface avandu-overview-feed
|
|
|
|
'((((class color)
|
|
|
|
(background dark))
|
|
|
|
(:foreground "white" :height 1.2 :bold t :family "sans"))
|
|
|
|
(((class color)
|
|
|
|
(background light))
|
|
|
|
(:foreground "black" :height 1.2 :bold t :family "sans")))
|
|
|
|
"Face for feed titles in avandu overview."
|
|
|
|
:group 'avandu)
|
|
|
|
|
|
|
|
(defface avandu-overview-read-article
|
|
|
|
'((((class color)
|
|
|
|
(background dark))
|
|
|
|
(:foreground "white" :weight normal :family "sans"))
|
|
|
|
(((class color)
|
|
|
|
(background light))
|
|
|
|
(:foreground "black" :weight normal :family "sans")))
|
|
|
|
"Face for read article titles in avandu overview."
|
|
|
|
:group 'avandu)
|
|
|
|
|
2012-08-03 22:46:15 +02:00
|
|
|
(defface avandu-overview-unread-article
|
|
|
|
'((((class color)
|
|
|
|
(background dark))
|
|
|
|
(:foreground "orange3" :weight bold :family "sans"))
|
|
|
|
(((class color)
|
|
|
|
(background light))
|
|
|
|
(:foregroung "red4" :weight bold :family "sans")))
|
|
|
|
"Face for unread article titles in avandu overview."
|
2012-07-26 00:49:18 +02:00
|
|
|
:group 'avandu)
|
|
|
|
|
2012-08-22 12:44:50 +02:00
|
|
|
(defface avandu-article-title
|
|
|
|
'((((class color)
|
|
|
|
(background dark))
|
|
|
|
(:foreground "orange3" :weight bold :family "sans"))
|
|
|
|
(((class color)
|
|
|
|
(background light))
|
|
|
|
(:foreground "red4" :weight bold :family "sans")))
|
|
|
|
"Face for titles in avandu article view."
|
|
|
|
:group 'avandu)
|
|
|
|
|
|
|
|
(defface avandu-article-author
|
|
|
|
'((t (:inherit shadow :slant italic :height 0.9)))
|
|
|
|
"Face for the author's name in avandu article view."
|
|
|
|
:group 'avandu)
|
|
|
|
|
2012-08-03 22:46:15 +02:00
|
|
|
;; User options
|
2012-08-22 23:08:42 +02:00
|
|
|
(defcustom avandu-article-render-function nil
|
|
|
|
"A function to call that will render the content of an article."
|
|
|
|
:group 'avandu
|
|
|
|
:type 'function)
|
|
|
|
|
2012-08-02 22:24:11 +02:00
|
|
|
(defcustom avandu-tt-rss-api-url nil
|
|
|
|
"URL of your Tiny Tiny RSS instance. For example:
|
|
|
|
http://tt-rss.org/demo/api/"
|
|
|
|
:group 'avandu
|
|
|
|
:type 'string)
|
|
|
|
|
2012-08-22 23:08:42 +02:00
|
|
|
(defcustom avandu-html2text-command nil
|
|
|
|
"Shell command to call to change HTML to plain text."
|
2012-08-03 23:37:27 +02:00
|
|
|
:group 'avandu
|
|
|
|
:type 'string)
|
|
|
|
|
2012-08-22 23:08:42 +02:00
|
|
|
(defcustom avandu-user nil
|
|
|
|
"Username of your Tiny Tiny RSS account."
|
2012-08-22 12:44:50 +02:00
|
|
|
:group 'avandu
|
|
|
|
:type 'string)
|
|
|
|
|
2012-08-03 22:46:15 +02:00
|
|
|
;; Variables
|
2012-07-25 01:49:02 +02:00
|
|
|
(defvar avandu--session-id nil
|
|
|
|
"*internal* Session id for avandu.")
|
|
|
|
|
|
|
|
(defvar avandu-article-button-map
|
|
|
|
(let ((map (make-sparse-keymap)))
|
|
|
|
(set-keymap-parent map button-map)
|
|
|
|
(define-key map "o" 'avandu-browse-article)
|
2012-08-22 12:44:50 +02:00
|
|
|
(define-key map "r" #'(lambda ()
|
|
|
|
(interactive)
|
|
|
|
(let ((button (button-at (point))))
|
|
|
|
(avandu-mark-article-read
|
|
|
|
(button-get button 'article-id))
|
|
|
|
(avandu-ui-mark-article-read button))))
|
2012-07-25 01:49:02 +02:00
|
|
|
map)
|
2012-07-26 00:43:30 +02:00
|
|
|
"Keymap for articles in `avandu-overview-mode'.")
|
2012-07-25 01:49:02 +02:00
|
|
|
|
2012-08-03 22:46:15 +02:00
|
|
|
(defvar avandu-feed-button-map
|
|
|
|
(let ((map (make-sparse-keymap)))
|
|
|
|
(set-keymap-parent map button-map)
|
|
|
|
(define-key map "c" 'avandu-feed-catchup)
|
|
|
|
map)
|
|
|
|
"Keymap for feeds in `avandu-overview-mode'.")
|
|
|
|
|
2012-07-25 01:49:02 +02:00
|
|
|
(defvar avandu-overview-map
|
|
|
|
(let ((map (make-sparse-keymap)))
|
|
|
|
(set-keymap-parent map special-mode-map)
|
|
|
|
(define-key map "n" 'avandu-next-article)
|
|
|
|
(define-key map "N" 'avandu-next-feed)
|
|
|
|
(define-key map "p" 'avandu-previous-article)
|
|
|
|
(define-key map "P" 'avandu-previous-feed)
|
|
|
|
map)
|
2012-07-26 00:43:30 +02:00
|
|
|
"Keymap for `avandu-overview-mode'.")
|
2012-07-25 01:49:02 +02:00
|
|
|
|
2012-08-03 22:46:15 +02:00
|
|
|
(defvar avandu-password nil
|
|
|
|
"Password for your Tiny Tiny RSS account.")
|
2012-07-27 01:44:14 +02:00
|
|
|
|
2012-08-03 22:46:15 +02:00
|
|
|
;; Macros
|
|
|
|
(defmacro avandu--next-button-of-type (direction type)
|
|
|
|
"Go DIRECTION and find the next button of a TYPE."
|
|
|
|
(let ((prop (case type
|
|
|
|
(feed 'feed-id)
|
|
|
|
(article 'article-id)
|
|
|
|
(t (error "Invalid type"))))
|
|
|
|
(next-point-function (case direction
|
|
|
|
(forward 'point-min)
|
|
|
|
(backward 'point-max)
|
|
|
|
(t (error "Invalid direction"))))
|
|
|
|
(next-button-function (case direction
|
|
|
|
(forward 'next-button)
|
|
|
|
(backward 'previous-button)
|
|
|
|
(t (error "Invalid direction")))))
|
|
|
|
`(let ((pos (point))
|
|
|
|
found-value)
|
|
|
|
(while (not found-value)
|
|
|
|
(let ((button (,next-button-function pos)))
|
|
|
|
(unless button
|
|
|
|
(setq pos (,next-point-function)
|
|
|
|
button (or (button-at pos)
|
|
|
|
(,next-button-function pos))))
|
|
|
|
(setq found-value (button-get button ',prop)
|
|
|
|
pos (overlay-start button))))
|
|
|
|
(goto-char pos))))
|
2012-07-26 00:43:30 +02:00
|
|
|
|
2012-08-03 22:46:15 +02:00
|
|
|
(defmacro avandu-getset (var prompt &optional passwdp)
|
|
|
|
"Ask the user for, and then save, VAR with PROMPT. Use
|
|
|
|
`read-passwd' if PASSWDP and `read-string' otherwise."
|
|
|
|
`(or ,var (setq ,var (,(if passwdp 'read-passwd 'read-string)
|
|
|
|
,prompt))))
|
|
|
|
|
2012-08-22 12:44:50 +02:00
|
|
|
(defmacro avu-prop (element property)
|
|
|
|
"Get PROPERTY from ELEMENT."
|
|
|
|
`(cdr (assq (quote ,property) ,element)))
|
|
|
|
|
2012-08-03 22:46:15 +02:00
|
|
|
;; Internal
|
|
|
|
(defun avandu--check-login ()
|
|
|
|
"Check to see if we're (still) logged in, try to login
|
|
|
|
otherwise. Signals an error if we're not logged in *and* login
|
|
|
|
was unsuccesful."
|
|
|
|
(unless (or (and avandu--session-id (avandu-logged-in-p))
|
|
|
|
(avandu-login))
|
|
|
|
(avandu--clear-data)
|
|
|
|
(error "Could not log in to tt-rss")))
|
|
|
|
|
|
|
|
(defun avandu--clean-text (text)
|
|
|
|
"Go through TEXT and remove any trailing and leading whitespace
|
|
|
|
from it, then look for any HTML entities and either replace them
|
|
|
|
with their char value or with the value in
|
|
|
|
`avandu-entity-replacement-alist'."
|
|
|
|
(with-temp-buffer
|
|
|
|
(insert text)
|
|
|
|
(while (re-search-forward
|
|
|
|
"\\`[[:space:][:cntrl:]]+\\|[[:space:][:cntrl:]]+\\'" nil t)
|
|
|
|
(replace-match ""))
|
|
|
|
|
|
|
|
(goto-char (point-min))
|
|
|
|
(while (search-forward "&" nil t)
|
|
|
|
(let ((pos (point)))
|
|
|
|
(save-excursion
|
|
|
|
(when (search-forward ";" nil t)
|
|
|
|
(let* ((sstring (buffer-substring pos (1- (point))))
|
|
|
|
(char-code
|
|
|
|
(if (= (char-after pos) ?#)
|
|
|
|
(unless (string-match-p "[^[:digit:]]"
|
|
|
|
(substring sstring 1))
|
|
|
|
(string-to-number (substring sstring 1)))
|
|
|
|
(assoc sstring avandu-entity-replacement-alist))))
|
|
|
|
(when char-code
|
|
|
|
(delete-region (1- pos) (point))
|
|
|
|
(insert-char (if (consp char-code)
|
|
|
|
(cdr char-code)
|
|
|
|
char-code) 1)))))))
|
|
|
|
|
|
|
|
(setq text (buffer-string)))
|
|
|
|
text)
|
2012-07-25 01:49:02 +02:00
|
|
|
|
2012-07-28 22:49:18 +02:00
|
|
|
(defun avandu--clear-data ()
|
|
|
|
"Clean up login data. This makes for a clean slate next time."
|
|
|
|
(setq avandu-user nil
|
2012-08-03 22:46:15 +02:00
|
|
|
avandu--session-id nil)
|
2012-08-06 21:07:33 +02:00
|
|
|
|
|
|
|
(if (stringp avandu-password)
|
|
|
|
(clear-string avandu-password)
|
|
|
|
(setq avandu-password nil)))
|
|
|
|
|
|
|
|
(defun avandu--get-credentials ()
|
|
|
|
"Get a username and password for Tiny Tiny RSS. Try it first
|
|
|
|
with `auth-source-search' and then by asking the user."
|
|
|
|
(let ((credentials (auth-source-search :max 1
|
|
|
|
:host avandu-tt-rss-api-url
|
|
|
|
:type 'netrc
|
|
|
|
:require '(:user :secret)
|
|
|
|
:user avandu-user)))
|
|
|
|
(if credentials
|
|
|
|
(setq avandu-user (plist-get (car credentials) :user)
|
|
|
|
avandu-password (plist-get (car credentials) :secret))
|
|
|
|
(avandu-getset avandu-user "Username: ")
|
|
|
|
(avandu-getset avandu-password "Password: " t))))
|
2012-08-03 22:46:15 +02:00
|
|
|
|
|
|
|
(defun avandu--get-session-id (results)
|
|
|
|
"Get the session id from RESULTS."
|
2012-08-22 12:44:50 +02:00
|
|
|
(avu-prop (assq 'content results) session_id))
|
2012-07-28 22:49:18 +02:00
|
|
|
|
2012-07-25 01:49:02 +02:00
|
|
|
(defun avandu--get-status-id (results)
|
|
|
|
"Get the status id from RESULTS."
|
2012-08-22 12:44:50 +02:00
|
|
|
(avu-prop results status))
|
2012-07-25 01:49:02 +02:00
|
|
|
|
2012-08-03 22:46:15 +02:00
|
|
|
(defun avandu--insert-article-excerpt (excerpt)
|
|
|
|
"Insert the excerpt of an article."
|
|
|
|
(let ((start-pos (point))
|
|
|
|
end-pos
|
2012-08-07 09:49:08 +02:00
|
|
|
(text (avandu--oneline excerpt)))
|
2012-08-03 22:46:15 +02:00
|
|
|
(unless (or (not text) (string= text ""))
|
|
|
|
(insert
|
|
|
|
(propertize
|
|
|
|
text
|
|
|
|
'face 'avandu-overview-excerpt))
|
|
|
|
(indent-region start-pos (point) tab-width)
|
|
|
|
(fill-region start-pos (point))
|
|
|
|
(insert-char ?\n 1))))
|
2012-07-25 01:49:02 +02:00
|
|
|
|
2012-08-03 22:46:15 +02:00
|
|
|
(defun avandu--insert-article-title (id link title)
|
|
|
|
"Insert a button with the label TITLE and store ID and LINK in
|
|
|
|
the article-id and link properties, respectively."
|
2012-08-07 10:57:23 +02:00
|
|
|
(let ((pos (point)))
|
|
|
|
(insert-button
|
|
|
|
(avandu--oneline title)
|
|
|
|
'face 'avandu-overview-unread-article
|
|
|
|
'article-id id
|
|
|
|
'link link
|
|
|
|
'keymap avandu-article-button-map
|
|
|
|
'action #'(lambda (button)
|
2012-08-22 12:44:50 +02:00
|
|
|
(avandu-view-article (button-get button 'article-id))))
|
|
|
|
|
2012-08-07 10:57:23 +02:00
|
|
|
(fill-region pos (point))
|
|
|
|
(insert-char ?\n 1)))
|
2012-07-25 01:49:02 +02:00
|
|
|
|
2012-08-03 22:46:15 +02:00
|
|
|
(defun avandu--insert-feed-title (id title)
|
|
|
|
"Insert a button with the label TITLE and store ID in the
|
|
|
|
feed-id property."
|
|
|
|
(unless (eq (point) (point-min)) (insert-char ?\n 1))
|
2012-08-07 10:57:23 +02:00
|
|
|
|
|
|
|
(let ((pos (point)))
|
|
|
|
(insert-button
|
|
|
|
(avandu--oneline title)
|
|
|
|
'face 'avandu-overview-feed
|
|
|
|
'feed-id id
|
|
|
|
'keymap avandu-feed-button-map
|
|
|
|
'action #'(lambda (button)
|
|
|
|
(message "%s" (button-label button))))
|
|
|
|
(fill-region pos (point))
|
|
|
|
(insert-char ?\n 2)))
|
2012-07-25 01:49:02 +02:00
|
|
|
|
2012-08-07 09:49:08 +02:00
|
|
|
(defun avandu--oneline (text)
|
|
|
|
"Make a single line out of and clean up TEXT."
|
|
|
|
(replace-regexp-in-string "[ \n\t]*$" "" (avandu--clean-text text)))
|
|
|
|
|
2012-08-06 21:07:33 +02:00
|
|
|
(defun avandu--password ()
|
|
|
|
"Get the password. This means either return `avandu-password'
|
|
|
|
as-is, or if it's a function return the result of that function."
|
|
|
|
(if (functionp avandu-password)
|
|
|
|
(funcall avandu-password)
|
|
|
|
avandu-password))
|
|
|
|
|
2012-08-22 22:05:16 +02:00
|
|
|
(defun avandu--prep-params (data)
|
|
|
|
"Prepare DATA to be sent to Tiny Tiny RSS."
|
|
|
|
(json-encode (if avandu--session-id
|
|
|
|
(append `((sid . ,avandu--session-id))
|
|
|
|
data)
|
|
|
|
data)))
|
|
|
|
|
|
|
|
(defun avandu--send-command-async (data func)
|
|
|
|
"Send a command with parameters DATA to tt-rss asynchronously.
|
|
|
|
The current session-id is added to the request and then DATA is
|
|
|
|
passed on to `json-encode'.
|
|
|
|
|
|
|
|
DATA should be an association list with at least an OP value.
|
|
|
|
|
|
|
|
FUNC should be a callback function as defined by
|
|
|
|
`url-retrieve'."
|
|
|
|
(let* ((url-request-data (avandu--prep-params data))
|
|
|
|
(url-request-method "POST"))
|
|
|
|
(unless (url-retrieve avandu-tt-rss-api-url func)
|
|
|
|
(message "Complete."))))
|
|
|
|
|
|
|
|
(defun avandu--send-command-sync (data)
|
2012-07-26 00:43:30 +02:00
|
|
|
"Send a command with parameters DATA to tt-rss. The current
|
|
|
|
session-id is added to the request and then DATA is passed on to
|
|
|
|
`json-encode'.
|
|
|
|
|
|
|
|
DATA should be an association list with at least an OP value.
|
|
|
|
For example:
|
|
|
|
|
2012-08-22 22:05:16 +02:00
|
|
|
(avandu--send-command-sync '((op . \"isLoggedIn\")))
|
2012-07-26 00:43:30 +02:00
|
|
|
|
|
|
|
This function returns the result of `json-read' passed over the
|
|
|
|
returned json."
|
2012-08-22 22:05:16 +02:00
|
|
|
(let* ((url-request-data (avandu--prep-params data))
|
2012-07-25 01:49:02 +02:00
|
|
|
(url-request-method "POST")
|
|
|
|
(buffer (url-retrieve-synchronously avandu-tt-rss-api-url))
|
|
|
|
result)
|
|
|
|
(with-current-buffer buffer
|
|
|
|
(goto-char (point-min))
|
|
|
|
(search-forward "\n\n")
|
|
|
|
(setq result (json-read)))
|
|
|
|
(kill-buffer buffer)
|
2012-08-22 12:44:50 +02:00
|
|
|
(avu-prop result content)))
|
2012-07-25 01:49:02 +02:00
|
|
|
|
2012-08-04 02:28:18 +02:00
|
|
|
(defun avandu-categories (&optional unread)
|
|
|
|
"Get the created categories. If UNREAD is non-nil only get
|
|
|
|
categories with feeds with unread articles in them."
|
2012-08-22 22:05:16 +02:00
|
|
|
(avandu--send-command-sync
|
2012-08-22 12:44:50 +02:00
|
|
|
`((op . "getCategories")
|
|
|
|
,@(when unread `((unread_only . ,unread))))))
|
2012-07-25 01:49:02 +02:00
|
|
|
|
2012-08-04 01:31:55 +02:00
|
|
|
(defun avandu-feeds (&optional category unread limit offset)
|
|
|
|
"Get the subscribed feeds. If CATEGORY has been specified show
|
|
|
|
only the feeds in CATEGORY. If UNREAD has been specified only
|
|
|
|
show feeds with unread articles in them. Only fets LIMIT
|
|
|
|
number of feeds, starting from OFFSET.
|
|
|
|
|
|
|
|
There are a number of special category IDs:
|
|
|
|
0 -- Uncategorized feeds
|
|
|
|
-1 -- Special (e.g. Starred, Published, Archived, etc.) feeds
|
|
|
|
-2 -- Labels
|
|
|
|
-3 -- All feeds, excluding virtual feeds (e.g. Labels and such)
|
|
|
|
-4 -- All feeds, including virtual feeds"
|
2012-08-22 22:05:16 +02:00
|
|
|
(avandu--send-command-sync
|
2012-08-22 12:44:50 +02:00
|
|
|
`((op . "getFeeds")
|
|
|
|
,@(when category `((cat_id . ,category)))
|
|
|
|
,@(when unread `((unread_only . ,unread)))
|
|
|
|
,@(when limit `((limit . ,limit)))
|
|
|
|
,@(when offset `((offset . ,offset))))))
|
2012-08-04 01:31:55 +02:00
|
|
|
|
2012-08-04 18:07:23 +02:00
|
|
|
(defun avandu-headlines (feed-id &rest plist)
|
2012-08-04 03:14:54 +02:00
|
|
|
"Get a list of headlines from Tiny Tiny RSS from the feed
|
2012-08-04 18:07:23 +02:00
|
|
|
identified by FEED-ID. Options about what to get can be
|
|
|
|
specified in the form of a property list PLIST.
|
|
|
|
|
|
|
|
If `:limit' is specified only get that many headlines, and if
|
|
|
|
`:skip' has been specified skip that many headlines first.
|
|
|
|
|
|
|
|
If `:is-cat' is non-nil, that means FEED-ID is actually the ID of
|
|
|
|
a category.
|
|
|
|
|
|
|
|
When `:show-excerpt' is non-nil, send back an excerpt along with
|
|
|
|
the headline and if `:show-content' is non-nil send along the
|
|
|
|
entire article.
|
|
|
|
|
|
|
|
`:view-mode' determines what type of headlines are sent back:
|
|
|
|
|
2012-08-04 03:14:54 +02:00
|
|
|
all_articles -- All articles found are sent back.
|
|
|
|
unread -- Only unread articles are sent back.
|
|
|
|
adaptive -- ?
|
|
|
|
marked -- ?
|
|
|
|
updated -- ?
|
|
|
|
|
2012-08-04 18:07:23 +02:00
|
|
|
If `:include-attatchments' is non-nil, send along any files
|
|
|
|
enclosed in the articles.
|
2012-08-04 03:14:54 +02:00
|
|
|
|
2012-08-04 18:07:23 +02:00
|
|
|
If `:since-id' is specified, send only articles with a FEED-ID
|
2012-08-04 03:14:54 +02:00
|
|
|
greater than this.
|
|
|
|
|
2012-08-04 18:07:23 +02:00
|
|
|
There are some special feed IDs:
|
2012-08-04 03:14:54 +02:00
|
|
|
-1 -- Starred feeds
|
|
|
|
-2 -- Published feeds
|
|
|
|
-3 -- Fresh feeds (less than X hours old)
|
|
|
|
-4 -- All articles
|
|
|
|
0 -- Archived articles
|
|
|
|
IDs < -10 -- Labels"
|
2012-08-04 18:07:23 +02:00
|
|
|
(let ((limit (plist-get plist :limit))
|
|
|
|
(skip (plist-get plist :skip))
|
|
|
|
(is-cat (plist-get plist :is-cat))
|
|
|
|
(show-excerpt (plist-get plist :show-excerpt))
|
|
|
|
(show-content (plist-get plist :show-content))
|
|
|
|
(view-mode (plist-get plist :view-mode))
|
|
|
|
(include-attachments (plist-get plist :include-attachments))
|
|
|
|
(since-id (plist-get plist :since-id)))
|
2012-08-22 22:05:16 +02:00
|
|
|
(avandu--send-command-sync
|
2012-08-22 12:44:50 +02:00
|
|
|
`((op . "getHeadlines")
|
|
|
|
(feed_id . ,feed-id)
|
|
|
|
,@(when limit `((limit . ,limit)))
|
|
|
|
,@(when skip `((skip . ,skip)))
|
|
|
|
,@(when is-cat `((is_cat . ,is-cat)))
|
|
|
|
,@(when show-excerpt `((show_excerpt . ,show-excerpt)))
|
|
|
|
,@(when show-content `((show_content . ,show-content)))
|
|
|
|
,@(when view-mode `((view_mode . ,view-mode)))
|
|
|
|
,@(when include-attachments `((include_attachments
|
|
|
|
. ,include-attachments)))
|
|
|
|
,@(when since-id `((since_id . ,since-id)))))))
|
2012-08-04 03:14:54 +02:00
|
|
|
|
2012-08-04 20:39:59 +02:00
|
|
|
(defun avandu-update-article (article-ids mode field &optional data)
|
|
|
|
"Update the status of FIELD to MODE for the articles identified
|
|
|
|
by ARTICLE-IDS.
|
|
|
|
|
|
|
|
ARTICLE-IDS should either be a single integer or a
|
|
|
|
comma-separated list of integers.
|
|
|
|
|
|
|
|
MODE should be one of:
|
|
|
|
0 -- Set to false
|
|
|
|
1 -- Set to true
|
|
|
|
2 -- Toggle
|
|
|
|
|
|
|
|
FIELD should be one of:
|
|
|
|
0 -- Starred
|
|
|
|
1 -- Published
|
|
|
|
2 -- Unread
|
|
|
|
3 -- Article Note
|
|
|
|
|
|
|
|
When updating FIELD 3 DATA functions as the note's contents."
|
2012-08-22 22:05:16 +02:00
|
|
|
(avandu--send-command-async `((op . "updateArticle")
|
|
|
|
(article_ids . ,article-ids)
|
|
|
|
(mode . ,mode)
|
|
|
|
(field . ,field)
|
|
|
|
,@(when data `((data . ,data))))
|
|
|
|
(lambda (status)
|
|
|
|
(message "Update done."))))
|
2012-08-04 20:39:59 +02:00
|
|
|
|
2012-08-22 12:44:50 +02:00
|
|
|
(defun avandu-get-article (article-ids)
|
|
|
|
"Get one or more articles from Tiny Tiny RSS with ARTICLE-IDS,
|
|
|
|
if you're using version 1.5.0 or higher this can also be a
|
|
|
|
comma-separated list of ids."
|
2012-08-22 22:05:16 +02:00
|
|
|
(avandu--send-command-sync `((op . "getArticle")
|
|
|
|
(article_id . ,article-ids))))
|
2012-08-22 12:44:50 +02:00
|
|
|
|
2012-08-04 02:28:18 +02:00
|
|
|
;; Commands
|
|
|
|
(defun avandu-browse-article ()
|
|
|
|
"Browse the current button's article url."
|
|
|
|
(interactive)
|
2012-08-07 10:17:54 +02:00
|
|
|
(let ((button (button-at (point)))
|
|
|
|
(message-truncate-lines t))
|
2012-08-04 02:28:18 +02:00
|
|
|
(browse-url (button-get button 'link))
|
2012-08-22 12:44:50 +02:00
|
|
|
(avandu-mark-article-read (button-get button 'article-id))
|
|
|
|
(avandu-ui-mark-article-read button)
|
2012-08-07 10:17:54 +02:00
|
|
|
(message "Opened: %s" (button-label button))))
|
2012-08-04 02:28:18 +02:00
|
|
|
|
|
|
|
(defun avandu-feed-catchup ()
|
|
|
|
"Send a request to tt-rss to \"Catch up\" with a feed. This
|
|
|
|
means that all the (unread) articles in a feed will be marked
|
|
|
|
as read. After having completed this request the overview is
|
|
|
|
reloaded."
|
|
|
|
(interactive)
|
|
|
|
(let* ((button (button-at (point)))
|
|
|
|
(id (button-get button 'feed-id)))
|
2012-08-22 22:05:16 +02:00
|
|
|
(avandu--send-command-async `((op . "catchupFeed")
|
|
|
|
(feed_id . ,id))
|
|
|
|
(lambda (status)
|
|
|
|
(message "Catch-up complete."))))
|
2012-08-04 02:28:18 +02:00
|
|
|
(revert-buffer))
|
|
|
|
|
2012-07-25 01:49:02 +02:00
|
|
|
(defun avandu-logged-in-p ()
|
2012-07-26 00:43:30 +02:00
|
|
|
"Send a request to tt-rss to see if we're (still) logged
|
|
|
|
in. This function returns t if we are, or nil if we're not."
|
2012-08-22 22:05:16 +02:00
|
|
|
(let* ((response (avandu--send-command-sync '((op . "isLoggedIn"))))
|
2012-08-22 12:44:50 +02:00
|
|
|
(result (avu-prop response status)))
|
2012-07-25 01:49:02 +02:00
|
|
|
(if (eq result :json-false)
|
|
|
|
nil
|
|
|
|
result)))
|
|
|
|
|
|
|
|
(defun avandu-login ()
|
2012-07-28 22:49:18 +02:00
|
|
|
"Send a request to log in to tt-rss. If `avandu-user' or
|
2012-07-26 00:43:30 +02:00
|
|
|
`avandu-password' have not been specified they will be asked for
|
|
|
|
and saved in memory. This function returns t on succes, nil
|
|
|
|
otherwise."
|
2012-07-25 01:49:02 +02:00
|
|
|
(interactive)
|
2012-08-06 21:07:33 +02:00
|
|
|
(unless (and avandu-user avandu-password)
|
|
|
|
(avandu--get-credentials))
|
|
|
|
|
2012-08-22 22:05:16 +02:00
|
|
|
(let ((result (avandu--send-command-sync
|
2012-07-25 01:49:02 +02:00
|
|
|
`((op . "login")
|
2012-08-06 21:07:33 +02:00
|
|
|
(user . ,avandu-user)
|
|
|
|
(password . ,(avandu--password))))))
|
2012-07-25 01:49:02 +02:00
|
|
|
(if (eq (avandu--get-status-id result) 0)
|
|
|
|
(progn
|
|
|
|
(setq avandu--session-id (avandu--get-session-id result))
|
|
|
|
t)
|
|
|
|
nil)))
|
|
|
|
|
2012-08-03 22:46:15 +02:00
|
|
|
(defun avandu-logout ()
|
|
|
|
"Logout from Tiny Tiny RSS."
|
|
|
|
(interactive)
|
2012-08-22 22:05:16 +02:00
|
|
|
(avandu--send-command-async '((op . "logout"))
|
|
|
|
(lambda (status)
|
|
|
|
(message "Logged out.")))
|
2012-08-03 22:46:15 +02:00
|
|
|
(avandu--clear-data))
|
|
|
|
|
2012-08-22 12:44:50 +02:00
|
|
|
(defun avandu-mark-article-read (id)
|
2012-08-03 22:46:15 +02:00
|
|
|
"Send a request to tt-rss to mark an article as read.
|
|
|
|
|
|
|
|
BUTTON, if given, should be a button widget, as created by
|
|
|
|
`button-insert' and such, which contains FEED-ID. If BUTTON is
|
|
|
|
nil, it will be assumed that `point' is currently within the
|
|
|
|
bounds of a button."
|
|
|
|
(interactive)
|
2012-08-22 12:44:50 +02:00
|
|
|
(let* ((message-truncate-lines t))
|
|
|
|
(avandu-update-article id 0 2)))
|
|
|
|
|
|
|
|
(defun avandu-ui-mark-article-read (&optional button)
|
|
|
|
"Try to change the state of BUTTON to a read article button, if
|
|
|
|
BUTTON is nil, try to use a button at `point'."
|
|
|
|
(let ((button (or button (button-at (point)))))
|
|
|
|
(if button
|
|
|
|
(progn
|
|
|
|
(button-put button 'face 'avandu-overview-read-article)
|
|
|
|
(avandu-next-article))
|
|
|
|
(error "No button found."))))
|
2012-08-03 22:46:15 +02:00
|
|
|
|
2012-07-25 01:49:02 +02:00
|
|
|
(defun avandu-new-articles-count ()
|
2012-07-26 00:43:30 +02:00
|
|
|
"Send a request to tt-rss for the total number of unread
|
|
|
|
feeds."
|
2012-07-25 01:49:02 +02:00
|
|
|
(interactive)
|
|
|
|
(avandu--check-login)
|
2012-08-22 22:05:16 +02:00
|
|
|
(let* ((result (avandu--send-command-sync '((op . "getUnread"))))
|
2012-08-22 12:44:50 +02:00
|
|
|
(count (avu-prop result unread)))
|
2012-08-04 00:30:15 +02:00
|
|
|
|
|
|
|
(when (called-interactively-p 'any)
|
|
|
|
(message "There are %s unread articles" count))
|
|
|
|
|
|
|
|
count))
|
2012-07-25 01:49:02 +02:00
|
|
|
|
|
|
|
(defun avandu-next-article ()
|
2012-07-26 00:43:30 +02:00
|
|
|
"Search forward for the next article."
|
2012-07-25 01:49:02 +02:00
|
|
|
(interactive)
|
|
|
|
(avandu--next-button-of-type forward article))
|
|
|
|
|
|
|
|
(defun avandu-next-feed ()
|
2012-07-26 00:43:30 +02:00
|
|
|
"Go forward and find the next feed."
|
2012-07-25 01:49:02 +02:00
|
|
|
(interactive)
|
|
|
|
(avandu--next-button-of-type forward feed))
|
|
|
|
|
2012-08-03 22:46:15 +02:00
|
|
|
(defun avandu-previous-article ()
|
|
|
|
"Go backward and find the next article."
|
|
|
|
(interactive)
|
|
|
|
(avandu--next-button-of-type backward article))
|
|
|
|
|
2012-07-25 01:49:02 +02:00
|
|
|
(defun avandu-previous-feed ()
|
2012-07-26 00:43:30 +02:00
|
|
|
"Go backward and find the next feed."
|
2012-07-25 01:49:02 +02:00
|
|
|
(interactive)
|
|
|
|
(avandu--next-button-of-type backward feed))
|
|
|
|
|
2012-08-03 23:54:35 +02:00
|
|
|
(defun avandu-tt-rss-api-level ()
|
2012-08-04 00:05:24 +02:00
|
|
|
"Get the API level of your Tiny Tiny RSS instance."
|
2012-08-03 23:54:35 +02:00
|
|
|
(interactive)
|
2012-08-22 22:05:16 +02:00
|
|
|
(let ((level (avu-prop (avandu--send-command-sync
|
|
|
|
'((op . "getApiLevel")))
|
2012-08-22 12:44:50 +02:00
|
|
|
level)))
|
2012-08-03 23:54:35 +02:00
|
|
|
(when (called-interactively-p 'any)
|
|
|
|
(message "API Level: %d" level))
|
|
|
|
|
|
|
|
level))
|
|
|
|
|
2012-08-04 00:05:24 +02:00
|
|
|
(defun avandu-tt-rss-version ()
|
|
|
|
"Get the version of your Tiny Tiny RSS instance."
|
|
|
|
(interactive)
|
2012-08-22 22:05:16 +02:00
|
|
|
(let ((version (avu-prop (avandu--send-command-sync
|
|
|
|
'((op . "getVersion")))
|
2012-08-22 12:44:50 +02:00
|
|
|
version)))
|
2012-08-04 00:05:24 +02:00
|
|
|
(when (called-interactively-p 'any)
|
|
|
|
(message "Tiny Tiny RSS Version: %s" version))
|
|
|
|
|
|
|
|
version))
|
|
|
|
|
2012-08-22 23:08:42 +02:00
|
|
|
(defun avandu-view-possibly-external (start end)
|
|
|
|
"If `avandu-html2text-command' has been specified use that on
|
|
|
|
the given region, otherwise just leave it alone."
|
|
|
|
(when avandu-html2text-command
|
|
|
|
(shell-command-on-region
|
|
|
|
start end avandu-html2text-command t t)))
|
|
|
|
|
|
|
|
(defun avandu-view-w3m (start end)
|
|
|
|
"Use w3m to view an article."
|
|
|
|
(when (require 'w3m nil t)
|
|
|
|
(w3m-region start end)
|
|
|
|
(w3m-minor-mode)))
|
|
|
|
|
2012-08-03 22:46:15 +02:00
|
|
|
;; Overview
|
2012-08-04 00:30:15 +02:00
|
|
|
(define-derived-mode avandu-overview-mode special-mode
|
|
|
|
avandu-overview-mode-name
|
2012-08-07 09:49:18 +02:00
|
|
|
"Major mode for the avandu overview screen.
|
2012-07-27 01:44:14 +02:00
|
|
|
|
2012-08-03 22:46:15 +02:00
|
|
|
This screen shows the articles categorized by feed as a list. It
|
|
|
|
doesn't sort the list, so you'll have to set that up in tt-rss.
|
2012-07-27 01:44:14 +02:00
|
|
|
|
2012-08-03 22:46:15 +02:00
|
|
|
\\{avandu-overview-map}
|
|
|
|
\\<avandu-overview-map>"
|
|
|
|
(use-local-map avandu-overview-map)
|
|
|
|
(set (make-local-variable 'revert-buffer-function)
|
2012-08-04 16:10:42 +02:00
|
|
|
#'(lambda (ignore-auto noconfirm) (avandu-overview)))
|
2012-08-04 00:30:15 +02:00
|
|
|
(setq mode-name (format "%s[%s]"
|
|
|
|
avandu-overview-mode-name
|
|
|
|
(avandu-new-articles-count))))
|
2012-07-25 01:49:02 +02:00
|
|
|
|
2012-08-22 12:44:50 +02:00
|
|
|
(define-derived-mode avandu-article-mode special-mode
|
|
|
|
"Avandu:Article"
|
|
|
|
"Major mode for the avandu article screen.
|
|
|
|
|
|
|
|
This screen shows the contents of an article.
|
|
|
|
|
|
|
|
\\{avandu-overview-map}
|
|
|
|
\\<avandu-overview-map>")
|
|
|
|
|
2012-07-25 01:49:02 +02:00
|
|
|
;;;###autoload
|
2012-08-04 16:10:42 +02:00
|
|
|
(defun avandu-overview ()
|
2012-07-26 00:43:30 +02:00
|
|
|
"Request the headlines of unread articles and list them grouped
|
|
|
|
by feed."
|
2012-07-25 01:49:02 +02:00
|
|
|
(interactive)
|
|
|
|
(avandu--check-login)
|
|
|
|
(let ((buffer (get-buffer-create "*avandu-overview*"))
|
2012-08-04 18:07:23 +02:00
|
|
|
(result (avandu-headlines -4 :show-excerpt t :view-mode "unread"))
|
2012-07-25 01:49:02 +02:00
|
|
|
feed-id)
|
|
|
|
(with-current-buffer buffer
|
|
|
|
(setq buffer-read-only nil)
|
|
|
|
(erase-buffer)
|
|
|
|
(goto-char (point-min))
|
|
|
|
(mapc #'(lambda (elt)
|
|
|
|
(unless (equal feed-id (assq 'feed_id elt))
|
2012-08-22 12:44:50 +02:00
|
|
|
(avandu--insert-feed-title (avu-prop elt feed_id)
|
|
|
|
(avu-prop elt feed_title)))
|
2012-07-25 01:49:02 +02:00
|
|
|
(setq feed-id (assq 'feed_id elt))
|
2012-08-22 12:44:50 +02:00
|
|
|
(avandu--insert-article-title (avu-prop elt id)
|
|
|
|
(avu-prop elt link)
|
|
|
|
(avu-prop elt title))
|
|
|
|
(avandu--insert-article-excerpt (avu-prop elt excerpt)))
|
2012-08-04 03:14:54 +02:00
|
|
|
result)
|
2012-07-25 01:49:02 +02:00
|
|
|
(setq buffer-read-only t)
|
|
|
|
(goto-char (point-min))
|
|
|
|
(avandu-overview-mode))
|
|
|
|
(switch-to-buffer buffer)))
|
|
|
|
|
2012-08-22 12:44:50 +02:00
|
|
|
(defun avandu-view-article (id)
|
|
|
|
"Show a single article in a new buffer."
|
|
|
|
(interactive "nArticle id: ")
|
|
|
|
(let* ((data (avandu-get-article id))
|
|
|
|
(buffer (get-buffer-create "*avandu-article*"))
|
2012-08-22 23:08:42 +02:00
|
|
|
(inhibit-read-only t)
|
|
|
|
content-start
|
|
|
|
content-end)
|
2012-08-22 12:44:50 +02:00
|
|
|
(with-current-buffer buffer
|
|
|
|
(erase-buffer)
|
|
|
|
(mapc #'(lambda (item)
|
|
|
|
(insert
|
|
|
|
(propertize (avu-prop item title)
|
|
|
|
'face 'avandu-article-title))
|
|
|
|
(newline)
|
|
|
|
(insert
|
|
|
|
(propertize (concat "by: " (avu-prop item author))
|
|
|
|
'face 'avandu-article-author))
|
|
|
|
(newline)(newline)
|
2012-08-22 23:08:42 +02:00
|
|
|
(setq content-start (point))
|
|
|
|
(insert (avu-prop item content))
|
|
|
|
(setq content-end (point))
|
2012-08-22 12:44:50 +02:00
|
|
|
(newline)(newline))
|
|
|
|
data)
|
|
|
|
(setq buffer-read-only t)
|
|
|
|
(goto-char (point-min))
|
|
|
|
(avandu-article-mode))
|
|
|
|
(avandu-mark-article-read id)
|
|
|
|
(avandu-ui-mark-article-read)
|
2012-08-22 23:08:42 +02:00
|
|
|
(switch-to-buffer buffer)
|
|
|
|
(when avandu-article-render-function
|
|
|
|
(funcall
|
|
|
|
avandu-article-render-function content-start
|
|
|
|
(min content-end (point-max))))))
|
2012-08-22 12:44:50 +02:00
|
|
|
|
2012-07-25 01:49:02 +02:00
|
|
|
(provide 'avandu)
|
|
|
|
|
|
|
|
;;; avandu.el ends here
|
|
|
|
;; ((feed_title . "Identity at Mozilla")
|
|
|
|
;; (labels . [])
|
|
|
|
;; (tags . [""])
|
|
|
|
;; (feed_id . "6")
|
|
|
|
;; (link . "http://identity.mozilla.com/post/13619011637")
|
|
|
|
;; (title . "BrowserID this week: better, faster, more secure.")
|
|
|
|
;; (is_updated . :json-false)
|
|
|
|
;; (updated . 1322795401)
|
|
|
|
;; (published . :json-false)
|
|
|
|
;; (marked . :json-false)
|
|
|
|
;; (unread . t)
|
|
|
|
;; (id . 109))
|
2012-08-03 22:46:15 +02:00
|
|
|
|
|
|
|
;; (login user password)
|
|
|
|
;; (get-article article-id)
|
|
|
|
;; (get-config icons-dir icons-url daemon-is-running num-feeds)
|
|
|
|
;; (update-feed feed-id)
|
|
|
|
;; (get-pref pref-name)
|
|
|
|
;; (catchup-feed feed-id categoryp)
|
|
|
|
;; (get-counters output-mode)
|
|
|
|
;; (get-labels article-id)
|
|
|
|
;; (set-article-label article-ids label-id assingp)
|