+(defcustom gnus-sync-lesync-name (system-name)
+ "The LeSync name for this machine."
+ :group 'gnus-sync
+ :type 'string)
+
+(defcustom gnus-sync-lesync-install-topics 'ask
+ "Should LeSync install the recorded topics?"
+ :group 'gnus-sync
+ :type '(choice (const :tag "Never Install" nil)
+ (const :tag "Always Install" t)
+ (const :tag "Ask Me Once" ask)))
+
+(defvar gnus-sync-lesync-props-hash (make-hash-table :test 'equal)
+ "LeSync props, keyed by group name")
+
+(defvar gnus-sync-lesync-design-prefix "/_design/lesync"
+ "The LeSync design prefix for CouchDB")
+
+(defvar gnus-sync-lesync-security-object "/_security"
+ "The LeSync security object for CouchDB")
+
+(defun gnus-sync-lesync-parse ()
+ "Parse the result of a LeSync request."
+ (goto-char (point-min))
+ (condition-case nil
+ (when (search-forward-regexp "^$" nil t)
+ (json-read))
+ (error
+ (gnus-message
+ 1
+ "gnus-sync-lesync-parse: Could not read the LeSync response!")
+ nil)))
+
+(defun gnus-sync-lesync-call (url method headers &optional kvdata)
+ "Make an access request to URL using KVDATA and METHOD.
+KVDATA must be an alist."
+ ;;(debug (json-encode kvdata))
+ ;; (when (string-match-p "gmane.emacs.devel" url) (debug kvdata))
+ (flet ((json-alist-p (list) (gnus-sync-json-alist-p list))) ; temp patch
+ (let ((url-request-method method)
+ (url-request-extra-headers headers)
+ (url-request-data (if kvdata (json-encode kvdata) nil)))
+ (with-current-buffer (url-retrieve-synchronously url)
+ ;;(debug (buffer-string))
+ (let ((data (gnus-sync-lesync-parse)))
+ (gnus-message 12 "gnus-sync-lesync-call: %s URL %s sent %S got %S"
+ method url `((headers . ,headers) (data ,kvdata)) data)
+ (kill-buffer (current-buffer))
+ data)))))
+
+(defun gnus-sync-lesync-PUT (url headers &optional data)
+ (gnus-sync-lesync-call url "PUT" headers data))
+
+(defun gnus-sync-lesync-POST (url headers &optional data)
+ (gnus-sync-lesync-call url "POST" headers data))
+
+(defun gnus-sync-lesync-GET (url headers &optional data)
+ (gnus-sync-lesync-call url "GET" headers data))
+
+(defun gnus-sync-lesync-DELETE (url headers &optional data)
+ (gnus-sync-lesync-call url "DELETE" headers data))
+
+;; this is not necessary with newer versions of json.el but 1.2 or older
+;; (which are in Emacs 24.1 and earlier) need it
+(defun gnus-sync-json-alist-p (list)
+ "Non-null if and only if LIST is an alist."
+ (while (consp list)
+ (setq list (if (consp (car list))
+ (cdr list)
+ 'not-alist)))
+ (null list))
+
+;; this is not necessary with newer versions of json.el but 1.2 or older
+;; (which are in Emacs 24.1 and earlier) need it
+(defun gnus-sync-json-plist-p (list)
+ "Non-null if and only if LIST is a plist."
+ (while (consp list)
+ (setq list (if (and (keywordp (car list))
+ (consp (cdr list)))
+ (cddr list)
+ 'not-plist)))
+ (null list))
+
+; (gnus-sync-lesync-setup "http://lesync.info:5984/tzz" "tzzadmin" "mypassword" "mysalt" t t)
+
+(defun gnus-sync-lesync-setup (url &optional user password salt reader admin)
+ (interactive "sEnter URL to set up: ")
+ "Set up the LeSync database at URL.
+Install USER as a READER and/or an ADMIN in the security object
+under \"_security\", and in the CouchDB \"_users\" table using
+PASSWORD and SALT. Only one USER is thus supported for now.
+When SALT is nil, a random one will be generated using `random'."
+ (let* ((design-url (concat url gnus-sync-lesync-design-prefix))
+ (security-object (concat url "/_security"))
+ (user-record `((names . [,user]) (roles . [])))
+ (couch-user-name (format "org.couchdb.user:%s" user))
+ (salt (or salt (sha1 (random t))))
+ (couch-user-record `((_id . ,couch-user-name)
+ (type . user)
+ (name . ,(format "%s" user))
+ (roles . [])
+ (password_sha . ,(sha1
+ (format "%s%s" password salt)))
+ (salt . ,(format "%s" salt))))
+ (rev (progn
+ (gnus-sync-lesync-find-prop 'rev design-url design-url)
+ (gnus-sync-lesync-get-prop 'rev design-url)))
+ (latest-func "function(head,req)
+{
+ var tosend = [];
+ var row;
+ var ftime = (req.query['ftime'] || 0);
+ while (row = getRow())
+ {
+ if (row.value['float-time'] > ftime)
+ {
+ var s = row.value['_id'];
+ if (s) tosend.push('\"'+s.replace('\"', '\\\"')+'\"');
+ }
+ }
+ send('['+tosend.join(',') + ']');
+}")
+ (subs-func "function(doc){emit([doc._id, doc.source], doc._rev);}")
+ (revs-func "function(doc){emit(doc._id, doc._rev);}")
+ (bytimesubs-func "function(doc)
+{emit([(doc['float-time']||0), doc._id], doc._rev);}")
+ (bytime-func "function(doc)
+{emit([(doc['float-time']||0), doc._id], doc);}")
+ (groups-func "function(doc){emit(doc._id, doc);}"))
+ (and (if user
+ (and (assq 'ok (gnus-sync-lesync-PUT
+ security-object
+ nil
+ (append (and reader
+ (list `(readers . ,user-record)))
+ (and admin
+ (list `(admins . ,user-record))))))
+ (assq 'ok (gnus-sync-lesync-PUT
+ (concat (file-name-directory url)
+ "_users/"
+ couch-user-name)
+ nil
+ couch-user-record)))
+ t)
+ (assq 'ok (gnus-sync-lesync-PUT
+ design-url
+ nil
+ `(,@(when rev (list (cons '_rev rev)))
+ (lists . ((latest . ,latest-func)))
+ (views . ((subs . ((map . ,subs-func)))
+ (revs . ((map . ,revs-func)))
+ (bytimesubs . ((map . ,bytimesubs-func)))
+ (bytime . ((map . ,bytime-func)))
+ (groups . ((map . ,groups-func)))))))))))
+
+(defun gnus-sync-lesync-find-prop (prop url key)
+ "Retrieve a PROPerty of a document KEY at URL.
+Calls `gnus-sync-lesync-set-prop'.
+For the 'rev PROP, uses '_rev against the document."
+ (gnus-sync-lesync-set-prop
+ prop key (cdr (assq (if (eq prop 'rev) '_rev prop)
+ (gnus-sync-lesync-GET url nil)))))
+
+(defun gnus-sync-lesync-set-prop (prop key val)
+ "Update the PROPerty of document KEY at URL to VAL.
+Updates `gnus-sync-lesync-props-hash'."
+ (puthash (format "%s.%s" key prop) val gnus-sync-lesync-props-hash))
+
+(defun gnus-sync-lesync-get-prop (prop key)
+ "Get the PROPerty of KEY from `gnus-sync-lesync-props-hash'."
+ (gethash (format "%s.%s" key prop) gnus-sync-lesync-props-hash))
+
+(defun gnus-sync-deep-print (data)
+ (let* ((print-quoted t)
+ (print-readably t)
+ (print-escape-multibyte nil)
+ (print-escape-nonascii t)
+ (print-length nil)
+ (print-level nil)
+ (print-circle nil)
+ (print-escape-newlines t))
+ (format "%S" data)))
+
+(defun gnus-sync-newsrc-loader-builder (&optional only-modified)
+ (let* ((entries (cdr gnus-newsrc-alist))
+ entry name ret)
+ (while entries
+ (setq entry (pop entries)
+ name (car entry))
+ (when (gnus-grep-in-list name gnus-sync-newsrc-groups)
+ (if only-modified
+ (when (not (equal (gnus-sync-deep-print entry)
+ (gnus-sync-lesync-get-prop 'checksum name)))
+ (gnus-message 9 "%s: add %s, it's modified"
+ "gnus-sync-newsrc-loader-builder" name)
+ (push entry ret))
+ (push entry ret))))
+ ret))
+
+; (json-encode (gnus-sync-range2invlist '((1 . 47137) (47139 . 47714) 48129 48211 49231 49281 49342 49473 49475 49502)))
+(defun gnus-sync-range2invlist (ranges)
+ (append '(invlist)
+ (let ((ranges (delq nil ranges))
+ ret range from to)
+ (while ranges
+ (setq range (pop ranges))
+ (if (atom range)
+ (setq from range
+ to range)
+ (setq from (car range)
+ to (cdr range)))
+ (push from ret)
+ (push (1+ to) ret))
+ (reverse ret))))
+
+; (let* ((d '((1 . 47137) (47139 . 47714) 48129 48211 49231 49281 49342 49473 49475 49502)) (j (format "%S" (gnus-sync-invlist2range (gnus-sync-range2invlist d))))) (or (equal (format "%S" d) j) j))
+(defun gnus-sync-invlist2range (inv)
+ (setq inv (append inv nil))
+ (if (equal (format "%s" (car inv)) "invlist")
+ (let ((i (cdr inv))
+ (start 0)
+ ret cur top flip)
+ (while i
+ (setq cur (pop i))
+ (when flip
+ (setq top (1- cur))
+ (if (= start top)
+ (push start ret)
+ (push (cons start top) ret)))
+ (setq flip (not flip))
+ (setq start cur))
+ (reverse ret))
+ inv))
+
+(defun gnus-sync-position (search list &optional test)
+ "Find the position of SEARCH in LIST using TEST, defaulting to `eq'."
+ (let ((pos 0)
+ (test (or test 'eq)))
+ (while (and list (not (funcall test (car list) search)))
+ (pop list)
+ (incf pos))
+ (if (funcall test (car list) search) pos nil)))
+
+(defun gnus-sync-topic-group-position (group topic-name)
+ (gnus-sync-position
+ group (cdr (assoc topic-name gnus-topic-alist)) 'equal))
+
+(defun gnus-sync-fix-topic-group-position (group topic-name position)
+ (unless (equal position (gnus-sync-topic-group-position group topic-name))
+ (let* ((loc "gnus-sync-fix-topic-group-position")
+ (groups (delete group (cdr (assoc topic-name gnus-topic-alist))))
+ (position (min position (1- (length groups))))
+ (old (nth position groups)))
+ (when (and old (not (equal old group)))
+ (setf (nth position groups) group)
+ (setcdr (assoc topic-name gnus-topic-alist)
+ (append groups (list old)))
+ (gnus-message 9 "%s: %s moved to %d, swap with %s"
+ loc group position old)))))
+
+(defun gnus-sync-lesync-pre-save-group-entry (url nentry &rest passed-props)
+ (let* ((loc "gnus-sync-lesync-save-group-entry")
+ (k (car nentry))
+ (revision (gnus-sync-lesync-get-prop 'rev k))
+ (sname gnus-sync-lesync-name)
+ (topic (gnus-group-topic k))
+ (topic-offset (gnus-sync-topic-group-position k topic))
+ (sources (gnus-sync-lesync-get-prop 'source k)))
+ ;; set the revision so we don't have a conflict
+ `(,@(when revision
+ (list (cons '_rev revision)))
+ (_id . ,k)
+ ;; the time we saved
+ ,@passed-props
+ ;; add our name to the sources list for this key
+ (source ,@(if (member gnus-sync-lesync-name sources)
+ sources
+ (cons gnus-sync-lesync-name sources)))
+ ,(cons 'level (nth 1 nentry))
+ ,@(if topic (list (cons 'topic topic)) nil)
+ ,@(if topic-offset (list (cons 'topic-offset topic-offset)) nil)
+ ;; the read marks
+ ,(cons 'read (gnus-sync-range2invlist (nth 2 nentry)))
+ ;; the other marks
+ ,@(mapcar (lambda (mark-entry)
+ (cons (car mark-entry)
+ (gnus-sync-range2invlist (cdr mark-entry))))
+ (nth 3 nentry)))))
+
+(defun gnus-sync-lesync-post-save-group-entry (url entry)
+ (let* ((loc "gnus-sync-lesync-post-save-group-entry")
+ (k (cdr (assq 'id entry))))
+ (cond
+ ;; success!
+ ((and (assq 'rev entry) (assq 'id entry))
+ (progn
+ (gnus-sync-lesync-set-prop 'rev k (cdr (assq 'rev entry)))
+ (gnus-sync-lesync-set-prop 'checksum
+ k
+ (gnus-sync-deep-print
+ (assoc k gnus-newsrc-alist)))
+ (gnus-message 9 "%s: successfully synced %s to %s"
+ loc k url)))
+ ;; specifically check for document conflicts
+ ((equal "conflict" (format "%s" (cdr-safe (assq 'error entry))))
+ (gnus-error
+ 1
+ "%s: use `%s' to resolve the conflict synchronizing %s to %s: %s"
+ loc "gnus-sync-read" k url (cdr (assq 'reason entry))))
+ ;; generic errors
+ ((assq 'error entry)
+ (gnus-error 1 "%s: got error while synchronizing %s to %s: %s"
+ loc k url (cdr (assq 'reason entry))))
+
+ (t
+ (gnus-message 2 "%s: unknown sync status after %s to %s: %S"
+ loc k url entry)))
+ (assoc 'error entry)))
+
+(defun gnus-sync-lesync-groups-builder (url)
+ (let ((u (concat url gnus-sync-lesync-design-prefix "/_view/groups")))
+ (cdr (assq 'rows (gnus-sync-lesync-GET u nil)))))
+
+(defun gnus-sync-subscribe-group (name)
+ "Subscribe to group NAME. Returns NAME on success, nil otherwise."
+ (gnus-subscribe-newsgroup name))
+
+(defun gnus-sync-lesync-read-group-entry (url name entry &rest passed-props)
+ "Read ENTRY information for NAME. Returns NAME if successful.
+Skips entries whose sources don't contain
+`gnus-sync-lesync-name'. When the alist PASSED-PROPS has a
+`subscribe-all' element that evaluates to true, we attempt to
+subscribe to unknown groups. The user is also allowed to delete
+unwanted groups via the LeSync URL."
+ (let* ((loc "gnus-sync-lesync-read-group-entry")
+ (entry (gnus-sync-lesync-normalize-group-entry entry passed-props))
+ (subscribe-all (cdr (assq 'subscribe-all passed-props)))
+ (sources (cdr (assq 'source entry)))
+ (rev (cdr (assq 'rev entry)))
+ (in-sources (member gnus-sync-lesync-name sources))
+ (known (assoc name gnus-newsrc-alist))
+ cell)
+ (unless known
+ (if (and subscribe-all
+ (y-or-n-p (format "Subscribe to group %s?" name)))
+ (setq known (gnus-sync-subscribe-group name)
+ in-sources t)
+ ;; else...
+ (when (y-or-n-p (format "Delete group %s from server?" name))
+ (if (equal name (gnus-sync-lesync-delete-group url name))
+ (gnus-message 1 "%s: removed group %s from server %s"
+ loc name url)
+ (gnus-error 1 "%s: could not remove group %s from server %s"
+ loc name url)))))
+ (when known
+ (unless in-sources
+ (setq in-sources
+ (y-or-n-p
+ (format "Read group %s even though %s is not in sources %S?"
+ name gnus-sync-lesync-name (or sources ""))))))
+ (when rev
+ (gnus-sync-lesync-set-prop 'rev name rev))
+
+ ;; if the source matches AND we have this group
+ (if (and known in-sources)
+ (progn
+ (gnus-message 10 "%s: reading LeSync entry %s, sources %S"
+ loc name sources)
+ (while entry
+ (setq cell (pop entry))
+ (let ((k (car cell))
+ (val (cdr cell)))
+ (gnus-sync-lesync-set-prop k name val)))
+ name)
+ ;; else...
+ (unless known
+ (gnus-message 5 "%s: ignoring entry %s, it wasn't subscribed. %s"
+ loc name "Call `gnus-sync-read' with C-u to force it."))
+ (unless in-sources
+ (gnus-message 5 "%s: ignoring entry %s, %s not in sources %S"
+ loc name gnus-sync-lesync-name (or sources "")))
+ nil)))
+
+(defun gnus-sync-lesync-install-group-entry (name)
+ (let* ((master (assoc name gnus-newsrc-alist))
+ (old-topic-name (gnus-group-topic name))
+ (old-topic (assoc old-topic-name gnus-topic-alist))
+ (target-topic-name (gnus-sync-lesync-get-prop 'topic name))
+ (target-topic-offset (gnus-sync-lesync-get-prop 'topic-offset name))
+ (target-topic (assoc target-topic-name gnus-topic-alist))
+ (loc "gnus-sync-lesync-install-group-entry"))
+ (if master
+ (progn
+ (when (eq 'ask gnus-sync-lesync-install-topics)
+ (setq gnus-sync-lesync-install-topics
+ (y-or-n-p "Install topics from LeSync?")))
+ (when (and (eq t gnus-sync-lesync-install-topics)
+ target-topic-name)
+ (if (equal old-topic-name target-topic-name)
+ (gnus-message 12 "%s: %s is already in topic %s"
+ loc name target-topic-name)
+ ;; see `gnus-topic-move-group'
+ (when (and old-topic target-topic)
+ (setcdr old-topic (gnus-delete-first name (cdr old-topic)))
+ (gnus-message 5 "%s: removing %s from topic %s"
+ loc name old-topic-name))
+ (unless target-topic
+ (when (y-or-n-p (format "Create missing topic %s?"
+ target-topic-name))
+ (gnus-topic-create-topic target-topic-name nil)
+ (setq target-topic (assoc target-topic-name
+ gnus-topic-alist))))
+ (if target-topic
+ (prog1
+ (nconc target-topic (list name))
+ (gnus-message 5 "%s: adding %s to topic %s"
+ loc name (car target-topic))
+ (gnus-topic-enter-dribble))
+ (gnus-error 2 "%s: LeSync group %s can't go in missing topic %s"
+ loc name target-topic-name)))
+ (when (and target-topic-offset target-topic)
+ (gnus-sync-fix-topic-group-position
+ name target-topic-name target-topic-offset)))
+ ;; install the subscription level
+ (when (gnus-sync-lesync-get-prop 'level name)
+ (setf (nth 1 master) (gnus-sync-lesync-get-prop 'level name)))
+ ;; install the read and other marks
+ (setf (nth 2 master) (gnus-sync-lesync-get-prop 'read name))
+ (setf (nth 3 master) (gnus-sync-lesync-get-prop 'marks name))
+ (gnus-sync-lesync-set-prop 'checksum
+ name
+ (gnus-sync-deep-print master))
+ nil)
+ (gnus-error 1 "%s: invalid LeSync group %s" loc name)
+ 'invalid-name)))
+
+; (gnus-sync-lesync-delete-group (cdr gnus-sync-backend) "nntp+Gmane:gwene.org.slashdot")
+
+(defun gnus-sync-lesync-delete-group (url name)
+ "Returns NAME if successful deleting it from URL, an error otherwise."
+ (interactive "sEnter URL to set up: \rsEnter group name: ")
+ (let* ((u (concat (cadr gnus-sync-backend) "/" (url-hexify-string name)))
+ (del (gnus-sync-lesync-DELETE
+ u
+ `(,@(when (gnus-sync-lesync-get-prop 'rev name)
+ (list (cons "If-Match"
+ (gnus-sync-lesync-get-prop 'rev name))))))))
+ (or (cdr (assq 'id del)) del)))
+
+;;; (gnus-sync-lesync-normalize-group-entry '((subscribe . ["invlist"]) (read . ["invlist"]) (topic-offset . 20) (topic . "news") (level . 6) (source . ["a" "b"]) (float-time . 1319671237.099285) (_rev . "10-edf5107f41e5e6f7f6629d1c0ee172f7") (_id . "nntp+news.net:alt.movies")) '((read-time 1319672156.486414) (subscribe-all nil)))
+
+(defun gnus-sync-lesync-normalize-group-entry (entry &optional passed-props)
+ (let (ret
+ marks
+ cell)
+ (setq entry (append passed-props entry))
+ (while (setq cell (pop entry))
+ (let ((k (car cell))
+ (val (cdr cell)))
+ (cond
+ ((eq k 'read)
+ (push (cons k (gnus-sync-invlist2range val)) ret))
+ ;; we already know the name
+ ((eq k '_id)
+ nil)
+ ((eq k '_rev)
+ (push (cons 'rev val) ret))
+ ((eq k 'source)
+ (push (cons 'source (append val nil)) ret))
+ ((or (eq k 'float-time)
+ (eq k 'level)
+ (eq k 'topic)
+ (eq k 'topic-offset)
+ (eq k 'read-time))
+ (push (cons k val) ret))
+;;; "How often have I said to you that when you have eliminated the
+;;; impossible, whatever remains, however improbable, must be the
+;;; truth?" --Sherlock Holmes
+ ;; everything remaining must be a mark
+ (t (push (cons k (gnus-sync-invlist2range val)) marks)))))
+ (cons (cons 'marks marks) ret)))
+
+(defun gnus-sync-save (&optional force)
+"Save the Gnus sync data to the backend.
+With a prefix, FORCE is set and all groups will be saved."
+ (interactive "P")