my-emacs-d/elpa/sx-20160125.1601/sx-question.el

237 lines
8.1 KiB
EmacsLisp
Raw Normal View History

2016-09-22 15:58:26 +00:00
;;; sx-question.el --- question logic -*- lexical-binding: t; -*-
;; Copyright (C) 2014 Sean Allred
;; Author: Sean Allred <code@seanallred.com>
;; 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 this program. If not, see <http://www.gnu.org/licenses/>.
;;; Commentary:
;; This file provides an API for retrieving questions and defines
;; additional logic for marking questions as read or hidden.
;;; Code:
(require 'sx)
(require 'sx-filter)
(require 'sx-method)
(defun sx-question-get-questions (site &optional page keywords submethod)
"Get SITE questions. Return page PAGE (the first if nil).
Return a list of question. Each question is an alist of
properties returned by the API with an added (site SITE)
property.
KEYWORDS are added to the method call along with PAGE.
`sx-method-call' is used with `sx-browse-filter'."
(sx-method-call 'questions
:keywords `((page . ,page) ,@keywords)
:site site
:auth t
:submethod submethod
:filter sx-browse-filter))
(defun sx-question-get-question (site question-id)
"Query SITE for a QUESTION-ID and return it.
If QUESTION-ID doesn't exist on SITE, raise an error."
(let ((res (sx-method-call 'questions
:id question-id
:site site
:auth t
:filter sx-browse-filter)))
(if res (elt res 0)
(error "Couldn't find question %S in %S"
question-id site))))
(defun sx-question-get-from-answer (site answer-id)
"Get question from SITE to which ANSWER-ID belongs.
If ANSWER-ID doesn't exist on SITE, raise an error."
(let ((res (sx-method-call 'answers
:id answer-id
:site site
:submethod 'questions
:auth t
:filter sx-browse-filter)))
(if res (elt res 0)
(error "Couldn't find answer %S in %S"
answer-id site))))
(defun sx-question-get-from-comment (site comment-id)
"Get question from SITE to which COMMENT-ID belongs.
If COMMENT-ID doesn't exist on SITE, raise an error.
Note this requires two API requests. One for the comment and one
for the post."
(let ((res (sx-method-call 'comments
:id comment-id
:site site
:auth t
:filter sx-browse-filter)))
(unless res
(error "Couldn't find comment %S in %S" comment-id site))
(sx-assoc-let (elt res 0)
(funcall (if (string= .post_type "answer")
#'sx-question-get-from-answer
#'sx-question-get-question)
.site_par
.post_id))))
;;; Question Properties
;;;; Read/unread
(defvar sx-question--user-read-list nil
"Alist of questions read by the user.
Each element has the form
(SITE . QUESTION-LIST)
where each element in QUESTION-LIST has the form
(QUESTION_ID . LAST-VIEWED-DATE).")
(defun sx-question--ensure-read-list (site)
"Ensure `sx-question--user-read-list' has been read from cache.
If no cache exists for it, initialize one with SITE."
(unless sx-question--user-read-list
(setq sx-question--user-read-list
(sx-cache-get 'read-questions `'((,site))))))
(defun sx-question--read-p (question)
"Non-nil if QUESTION has been read since last updated.
See `sx-question--user-read-list'."
(sx-assoc-let question
(sx-question--ensure-read-list .site_par)
(let ((ql (cdr (assoc .site_par sx-question--user-read-list))))
(and ql
(>= (or (cdr (assoc .question_id ql)) 0)
.last_activity_date)))))
(defmacro sx-sorted-insert-skip-first (newelt list &optional predicate)
"Inserted NEWELT into LIST sorted by PREDICATE.
This is designed for the (site id id ...) lists. So the first car
is intentionally skipped."
`(let ((tail ,list)
(x ,newelt))
(while (and ;; We're not at the end.
(cdr-safe tail)
;; We're not at the right place.
(funcall (or #',predicate #'<) x (cadr tail)))
(setq tail (cdr tail)))
(setcdr tail (cons x (cdr tail)))))
(defun sx-question--mark-read (question)
"Mark QUESTION as being read until it is updated again.
Returns nil if question (in its current state) was already marked
read, i.e., if it was `sx-question--read-p'.
See `sx-question--user-read-list'."
(prog1
(sx-assoc-let question
(sx-question--ensure-read-list .site_par)
(let ((site-cell (assoc .site_par sx-question--user-read-list))
(q-cell (cons .question_id .last_activity_date))
cell)
(cond
;; First question from this site.
((null site-cell)
(push (list .site_par q-cell) sx-question--user-read-list))
;; Question already present.
((setq cell (assoc .question_id site-cell))
;; Current version is newer than cached version.
(when (or (not (numberp (cdr cell)))
(> .last_activity_date (cdr cell)))
(setcdr cell .last_activity_date)))
;; Question wasn't present.
(t
(sx-sorted-insert-skip-first
q-cell site-cell
(lambda (x y) (> (or (car x) -1) (or (car y) -1))))))))
;; Save the results.
;; @TODO This causes a small lag on `j' and `k' as the list gets
;; large. Should we do this on a timer?
(sx-cache-set 'read-questions sx-question--user-read-list)))
;;;; Hidden
(defvar sx-question--user-hidden-list nil
"Alist of questions hidden by the user.
Each element has the form
(SITE QUESTION_ID QUESTION_ID ...)")
(defun sx-question--ensure-hidden-list (site)
"Ensure the `sx-question--user-hidden-list' has been read from cache.
If no cache exists for it, initialize one with SITE."
(unless sx-question--user-hidden-list
(setq sx-question--user-hidden-list
(sx-cache-get 'hidden-questions `'((,site))))))
(defun sx-question--hidden-p (question)
"Non-nil if QUESTION has been hidden."
(sx-assoc-let question
(sx-question--ensure-hidden-list .site_par)
(let ((ql (cdr (assoc .site_par sx-question--user-hidden-list))))
(and ql (memq .question_id ql)))))
(defun sx-question--mark-hidden (question)
"Mark QUESTION as being hidden."
(sx-assoc-let question
(let ((site-cell (assoc .site_par sx-question--user-hidden-list)))
;; If question already hidden, do nothing.
(unless (memq .question_id site-cell)
(if (null site-cell)
;; First question from this site.
(push (list .site_par .question_id) sx-question--user-hidden-list)
;; Not first question and question wasn't present.
;; Add it in, but make sure it's sorted (just in case we
;; decide to rely on it later).
(sx-sorted-insert-skip-first .question_id site-cell >))
;; Save the results.
(sx-cache-set 'hidden-questions sx-question--user-hidden-list)))))
;;;; Other data
(defun sx-question--accepted-answer-id (question)
"Return accepted answer in QUESTION or nil if none exists."
(sx-assoc-let question
(and (integerp .accepted_answer_id)
.accepted_answer_id)))
;;; Question Mode Answer-Sorting Functions
(sx--create-comparator sx-answer-higher-score-p
"Return t if answer A has a higher score than answer B."
#'> (lambda (x) (cdr (assq 'score x))))
(sx--create-comparator sx-answer-newer-p
"Return t if answer A was posted later than answer B."
#'> (lambda (x) (cdr (assq 'creation_date x))))
(sx--create-comparator sx-answer-more-active-p
"Return t if answer A was updated after answer B."
#'> (lambda (x) (cdr (assq 'last_activity_date x))))
(provide 'sx-question)
;;; sx-question.el ends here
;; Local Variables:
;; indent-tabs-mode: nil
;; End: