Merge changes made in Emacs trunk
[gnus] / lisp / gnus-sync.el
1 ;;; gnus-sync.el --- synchronization facility for Gnus
2
3 ;; Copyright (C) 2010-2012  Free Software Foundation, Inc.
4
5 ;; Author: Ted Zlatanov <tzz@lifelogs.com>
6 ;; Keywords: news synchronization nntp nnrss
7
8 ;; This file is part of GNU Emacs.
9
10 ;; GNU Emacs is free software: you can redistribute it and/or modify
11 ;; it under the terms of the GNU General Public License as published by
12 ;; the Free Software Foundation, either version 3 of the License, or
13 ;; (at your option) any later version.
14
15 ;; GNU Emacs is distributed in the hope that it will be useful,
16 ;; but WITHOUT ANY WARRANTY; without even the implied warranty of
17 ;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
18 ;; GNU General Public License for more details.
19
20 ;; You should have received a copy of the GNU General Public License
21 ;; along with GNU Emacs.  If not, see <http://www.gnu.org/licenses/>.
22
23 ;;; Commentary:
24
25 ;; This is the gnus-sync.el package.
26
27 ;; Put this in your startup file (~/.gnus.el for instance)
28
29 ;; possibilities for gnus-sync-backend:
30 ;; Tramp over SSH: /ssh:user@host:/path/to/filename
31 ;; ...or any other file Tramp and Emacs can handle...
32
33 ;; (setq gnus-sync-backend "/remote:/path.gpg" ; will use Tramp+EPA if loaded
34 ;;       gnus-sync-global-vars '(gnus-newsrc-last-checked-date)
35 ;;       gnus-sync-newsrc-groups '("nntp" "nnrss"))
36 ;;       gnus-sync-newsrc-offsets '(2 3))
37 ;; against a LeSync server (beware the vampire LeSync, who knows your newsrc)
38
39 ;; (setq gnus-sync-backend '(lesync "http://lesync.info:5984/tzz")
40 ;;       gnus-sync-newsrc-groups '("nntp" "nnrss"))
41
42 ;; What's a LeSync server?
43
44 ;; 1. install CouchDB, set up a real server admin user, and create a
45 ;; database, e.g. "tzz" and save the URL,
46 ;; e.g. http://lesync.info:5984/tzz
47
48 ;; 2. run `M-: (gnus-sync-lesync-setup "http://lesync.info:5984/tzz" "tzzadmin" "mypassword" "mysalt" t t)'
49
50 ;;    (If you run it more than once, you have to remove the entry from
51 ;;    _users yourself.  This is intentional.  This sets up a database
52 ;;    admin for the "tzz" database, distinct from the server admin
53 ;;    user in (1) above.)
54
55 ;; That's it, you can start using http://lesync.info:5984/tzz in your
56 ;; gnus-sync-backend as a LeSync backend.  Fan fiction about the
57 ;; vampire LeSync is welcome.
58
59 ;; You may not want to expose a CouchDB install to the Big Bad
60 ;; Internet, especially if your love of all things furry would be thus
61 ;; revealed.  Make sure it's not accessible by unauthorized users and
62 ;; guests, at least.
63
64 ;; If you want to try it out, I will create a test DB for you under
65 ;; http://lesync.info:5984/yourfavoritedbname
66
67 ;; TODO:
68
69 ;; - after gnus-sync-read, the message counts look wrong until you do
70 ;;   `g'.  So it's not run automatically, you have to call it with M-x
71 ;;   gnus-sync-read
72
73 ;; - use gnus-after-set-mark-hook and gnus-before-update-mark-hook to
74 ;;   catch the mark updates
75
76 ;; - repositioning of groups within topic after a LeSync sync is a
77 ;;   weird sort of bubble sort ("buttle" sort: the old entry ends up
78 ;;   at the rear of the list); you will eventually end up with the
79 ;;   right order after calling `gnus-sync-read' a bunch of times.
80
81 ;; - installing topics and groups is inefficient and annoying, lots of
82 ;;   prompts could be avoided
83
84 ;;; Code:
85
86 (eval-when-compile (require 'cl))
87 (eval-and-compile
88   (or (ignore-errors (progn
89                        (require 'json)))
90       ;; gnus-fallback-lib/ from gnus/lisp/gnus-fallback-lib
91       (ignore-errors
92         (let ((load-path (cons (expand-file-name
93                                 "gnus-fallback-lib"
94                                 (file-name-directory (locate-library "gnus")))
95                                load-path)))
96           (require 'json)))
97       (error
98        "json not found in `load-path' or gnus-fallback-lib/ directory.")))
99 (require 'gnus)
100 (require 'gnus-start)
101 (require 'gnus-util)
102
103 (defvar gnus-topic-alist) ;; gnus-group.el
104 (eval-when-compile
105   (autoload 'gnus-group-topic "gnus-topic")
106   (autoload 'gnus-topic-create-topic "gnus-topic" nil t)
107   (autoload 'gnus-topic-enter-dribble "gnus-topic"))
108
109 (defgroup gnus-sync nil
110   "The Gnus synchronization facility."
111   :version "24.1"
112   :group 'gnus)
113
114 (defcustom gnus-sync-newsrc-groups '("nntp" "nnrss")
115   "List of groups to be synchronized in the gnus-newsrc-alist.
116 The group names are matched, they don't have to be fully
117 qualified.  Typically you would choose all of these.  That's the
118 default because there is no active sync backend by default, so
119 this setting is harmless until the user chooses a sync backend."
120   :group 'gnus-sync
121   :type '(repeat regexp))
122
123 (defcustom gnus-sync-newsrc-offsets '(2 3)
124   "List of per-group data to be synchronized."
125   :group 'gnus-sync
126   :type '(set (const :tag "Read ranges" 2)
127               (const :tag "Marks" 3)))
128
129 (defcustom gnus-sync-global-vars nil
130   "List of global variables to be synchronized.
131 You may want to sync `gnus-newsrc-last-checked-date' but pretty
132 much any symbol is fair game.  You could additionally sync
133 `gnus-newsrc-alist', `gnus-server-alist', `gnus-topic-topology',
134 and `gnus-topic-alist'.  Also see `gnus-variable-list'."
135   :group 'gnus-sync
136   :type '(repeat (choice (variable :tag "A known variable")
137                          (symbol :tag "Any symbol"))))
138
139 (defcustom gnus-sync-backend nil
140   "The synchronization backend."
141   :group 'gnus-sync
142   :type '(radio (const :format "None" nil)
143                 (list :tag "Sync server"
144                       (const :format "LeSync Server API" lesync)
145                       (string :tag "URL of a CouchDB database for API access"))
146                 (string :tag "Sync to a file")))
147
148 (defvar gnus-sync-newsrc-loader nil
149   "Carrier for newsrc data")
150
151 (defcustom gnus-sync-lesync-name (system-name)
152   "The LeSync name for this machine."
153   :group 'gnus-sync
154   :version "24.3"
155   :type 'string)
156
157 (defcustom gnus-sync-lesync-install-topics 'ask
158   "Should LeSync install the recorded topics?"
159   :group 'gnus-sync
160   :version "24.3"
161   :type '(choice (const :tag "Never Install" nil)
162                  (const :tag "Always Install" t)
163                  (const :tag "Ask Me Once" ask)))
164
165 (defvar gnus-sync-lesync-props-hash (make-hash-table :test 'equal)
166   "LeSync props, keyed by group name")
167
168 (defvar gnus-sync-lesync-design-prefix "/_design/lesync"
169   "The LeSync design prefix for CouchDB")
170
171 (defvar gnus-sync-lesync-security-object "/_security"
172   "The LeSync security object for CouchDB")
173
174 (defun gnus-sync-lesync-parse ()
175   "Parse the result of a LeSync request."
176   (goto-char (point-min))
177   (condition-case nil
178       (when (search-forward-regexp "^$" nil t)
179         (json-read))
180     (error
181      (gnus-message
182       1
183       "gnus-sync-lesync-parse: Could not read the LeSync response!")
184      nil)))
185
186 (defun gnus-sync-lesync-call (url method headers &optional kvdata)
187   "Make an access request to URL using KVDATA and METHOD.
188 KVDATA must be an alist."
189   (flet ((json-alist-p (list) (gnus-sync-json-alist-p list))) ; temp patch
190     (let ((url-request-method method)
191           (url-request-extra-headers headers)
192           (url-request-data (if kvdata (json-encode kvdata) nil)))
193       (with-current-buffer (url-retrieve-synchronously url)
194         (let ((data (gnus-sync-lesync-parse)))
195           (gnus-message 12 "gnus-sync-lesync-call: %s URL %s sent %S got %S"
196                         method url `((headers . ,headers) (data ,kvdata)) data)
197           (kill-buffer (current-buffer))
198           data)))))
199
200 (defun gnus-sync-lesync-PUT (url headers &optional data)
201   (gnus-sync-lesync-call url "PUT" headers data))
202
203 (defun gnus-sync-lesync-POST (url headers &optional data)
204   (gnus-sync-lesync-call url "POST" headers data))
205
206 (defun gnus-sync-lesync-GET (url headers &optional data)
207   (gnus-sync-lesync-call url "GET" headers data))
208
209 (defun gnus-sync-lesync-DELETE (url headers &optional data)
210   (gnus-sync-lesync-call url "DELETE" headers data))
211
212 ;; this is not necessary with newer versions of json.el but 1.2 or older
213 ;; (which are in Emacs 24.1 and earlier) need it
214 (defun gnus-sync-json-alist-p (list)
215   "Non-null if and only if LIST is an alist."
216   (while (consp list)
217     (setq list (if (consp (car list))
218                    (cdr list)
219                  'not-alist)))
220   (null list))
221
222 ;; this is not necessary with newer versions of json.el but 1.2 or older
223 ;; (which are in Emacs 24.1 and earlier) need it
224 (defun gnus-sync-json-plist-p (list)
225   "Non-null if and only if LIST is a plist."
226   (while (consp list)
227     (setq list (if (and (keywordp (car list))
228                         (consp (cdr list)))
229                    (cddr list)
230                  'not-plist)))
231   (null list))
232
233 ; (gnus-sync-lesync-setup "http://lesync.info:5984/tzz" "tzzadmin" "mypassword" "mysalt" t t)
234 ; (gnus-sync-lesync-setup "http://lesync.info:5984/tzz")
235
236 (defun gnus-sync-lesync-setup (url &optional user password salt reader admin)
237   (interactive "sEnter URL to set up: ")
238   "Set up the LeSync database at URL.
239 Install USER as a READER and/or an ADMIN in the security object
240 under \"_security\", and in the CouchDB \"_users\" table using
241 PASSWORD and SALT.  Only one USER is thus supported for now.
242 When SALT is nil, a random one will be generated using `random'."
243   (let* ((design-url (concat url gnus-sync-lesync-design-prefix))
244          (security-object (concat url "/_security"))
245          (user-record `((names . [,user]) (roles . [])))
246          (couch-user-name (format "org.couchdb.user:%s" user))
247          (salt (or salt (sha1 (format "%s" (random)))))
248          (couch-user-record
249           `((_id . ,couch-user-name)
250             (type . user)
251             (name . ,(format "%s" user))
252             (roles . [])
253             (salt . ,salt)
254             (password_sha . ,(when password
255                                (sha1
256                                 (format "%s%s" password salt))))))
257          (rev (progn
258                 (gnus-sync-lesync-find-prop 'rev design-url design-url)
259                 (gnus-sync-lesync-get-prop 'rev design-url)))
260          (latest-func "function(head,req)
261 {
262   var tosend = [];
263   var row;
264   var ftime = (req.query['ftime'] || 0);
265   while (row = getRow())
266   {
267     if (row.value['float-time'] > ftime)
268     {
269       var s = row.value['_id'];
270       if (s) tosend.push('\"'+s.replace('\"', '\\\"')+'\"');
271     }
272   }
273   send('['+tosend.join(',') + ']');
274 }")
275 ;; <key>read</key>
276 ;; <dict>
277 ;;   <key>de.alt.fan.ipod</key>
278 ;;   <array>
279 ;;       <integer>1</integer>
280 ;;       <integer>2</integer>
281 ;;       <dict>
282 ;;           <key>start</key>
283 ;;           <integer>100</integer>
284 ;;           <key>length</key>
285 ;;           <integer>100</integer>
286 ;;       </dict>
287 ;;   </array>
288 ;; </dict>
289          (xmlplistread-func "function(head, req) {
290   var row;
291   start({ 'headers': { 'Content-Type': 'text/xml' } });
292
293   send('<dict>');
294   send('<key>read</key>');
295   send('<dict>');
296   while(row = getRow())
297   {
298     var read = row.value.read;
299     if (read && read[0] && read[0] == 'invlist')
300     {
301       send('<key>'+row.key+'</key>');
302       //send('<invlist>'+read+'</invlist>');
303       send('<array>');
304
305       var from = 0;
306       var flip = false;
307
308       for (var i = 1; i < read.length && read[i]; i++)
309       {
310         var cur = read[i];
311         if (flip)
312         {
313           if (from == cur-1)
314           {
315             send('<integer>'+read[i]+'</integer>');
316           }
317           else
318           {
319             send('<dict>');
320             send('<key>start</key>');
321             send('<integer>'+from+'</integer>');
322             send('<key>end</key>');
323             send('<integer>'+(cur-1)+'</integer>');
324             send('</dict>');
325           }
326
327         }
328         flip = ! flip;
329         from = cur;
330       }
331       send('</array>');
332     }
333   }
334
335   send('</dict>');
336   send('</dict>');
337 }
338 ")
339          (subs-func "function(doc){emit([doc._id, doc.source], doc._rev);}")
340          (revs-func "function(doc){emit(doc._id, doc._rev);}")
341          (bytimesubs-func "function(doc)
342 {emit([(doc['float-time']||0), doc._id], doc._rev);}")
343          (bytime-func "function(doc)
344 {emit([(doc['float-time']||0), doc._id], doc);}")
345          (groups-func "function(doc){emit(doc._id, doc);}"))
346     (and (if user
347              (and (assq 'ok (gnus-sync-lesync-PUT
348                              security-object
349                              nil
350                              (append (and reader
351                                           (list `(readers . ,user-record)))
352                                      (and admin
353                                           (list `(admins . ,user-record))))))
354                   (assq 'ok (gnus-sync-lesync-PUT
355                              (concat (file-name-directory url)
356                                      "_users/"
357                                      couch-user-name)
358                              nil
359                              couch-user-record)))
360            t)
361          (assq 'ok (gnus-sync-lesync-PUT
362                     design-url
363                     nil
364                     `(,@(when rev (list (cons '_rev rev)))
365                       (lists . ((latest . ,latest-func)
366                                 (xmlplistread . ,xmlplistread-func)))
367                       (views . ((subs . ((map . ,subs-func)))
368                                 (revs . ((map . ,revs-func)))
369                                 (bytimesubs . ((map . ,bytimesubs-func)))
370                                 (bytime . ((map . ,bytime-func)))
371                                 (groups . ((map . ,groups-func)))))))))))
372
373 (defun gnus-sync-lesync-find-prop (prop url key)
374   "Retrieve a PROPerty of a document KEY at URL.
375 Calls `gnus-sync-lesync-set-prop'.
376 For the 'rev PROP, uses '_rev against the document."
377   (gnus-sync-lesync-set-prop
378    prop key (cdr (assq (if (eq prop 'rev) '_rev prop)
379                        (gnus-sync-lesync-GET url nil)))))
380
381 (defun gnus-sync-lesync-set-prop (prop key val)
382   "Update the PROPerty of document KEY at URL to VAL.
383 Updates `gnus-sync-lesync-props-hash'."
384     (puthash (format "%s.%s" key prop) val gnus-sync-lesync-props-hash))
385
386 (defun gnus-sync-lesync-get-prop (prop key)
387   "Get the PROPerty of KEY from `gnus-sync-lesync-props-hash'."
388     (gethash (format "%s.%s" key prop) gnus-sync-lesync-props-hash))
389
390 (defun gnus-sync-deep-print (data)
391   (let* ((print-quoted t)
392          (print-readably t)
393          (print-escape-multibyte nil)
394          (print-escape-nonascii t)
395          (print-length nil)
396          (print-level nil)
397          (print-circle nil)
398          (print-escape-newlines t))
399     (format "%S" data)))
400
401 (defun gnus-sync-newsrc-loader-builder (&optional only-modified)
402   (let* ((entries (cdr gnus-newsrc-alist))
403          entry name ret)
404     (while entries
405       (setq entry (pop entries)
406             name (car entry))
407       (when (gnus-grep-in-list name gnus-sync-newsrc-groups)
408         (if only-modified
409             (when (not (equal (gnus-sync-deep-print entry)
410                               (gnus-sync-lesync-get-prop 'checksum name)))
411               (gnus-message 9 "%s: add %s, it's modified"
412                             "gnus-sync-newsrc-loader-builder" name)
413               (push entry ret))
414           (push entry ret))))
415     ret))
416
417 ; (json-encode (gnus-sync-range2invlist '((1 . 47137) (47139 . 47714) 48129 48211 49231 49281 49342 49473 49475 49502)))
418 (defun gnus-sync-range2invlist (ranges)
419   (append '(invlist)
420           (let ((ranges (delq nil ranges))
421                 ret range from to)
422             (while ranges
423               (setq range (pop ranges))
424               (if (atom range)
425                   (setq from range
426                         to range)
427                 (setq from (car range)
428                       to (cdr range)))
429               (push from ret)
430               (push (1+ to) ret))
431             (reverse ret))))
432
433 ; (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))
434 (defun gnus-sync-invlist2range (inv)
435   (setq inv (append inv nil))
436   (if (equal (format "%s" (car inv)) "invlist")
437       (let ((i (cdr inv))
438             (start 0)
439             ret cur top flip)
440         (while i
441           (setq cur (pop i))
442           (when flip
443             (setq top (1- cur))
444             (if (= start top)
445                 (push start ret)
446               (push (cons start top) ret)))
447           (setq flip (not flip))
448           (setq start cur))
449         (reverse ret))
450     inv))
451
452 (defun gnus-sync-position (search list &optional test)
453   "Find the position of SEARCH in LIST using TEST, defaulting to `eq'."
454   (let ((pos 0)
455         (test (or test 'eq)))
456     (while (and list (not (funcall test (car list) search)))
457       (pop list)
458       (incf pos))
459     (if (funcall test (car list) search) pos nil)))
460
461 (defun gnus-sync-topic-group-position (group topic-name)
462   (gnus-sync-position
463    group (cdr (assoc topic-name gnus-topic-alist)) 'equal))
464
465 (defun gnus-sync-fix-topic-group-position (group topic-name position)
466   (unless (equal position (gnus-sync-topic-group-position group topic-name))
467     (let* ((loc "gnus-sync-fix-topic-group-position")
468            (groups (delete group (cdr (assoc topic-name gnus-topic-alist))))
469            (position (min position (1- (length groups))))
470            (old (nth position groups)))
471       (when (and old (not (equal old group)))
472         (setf (nth position groups) group)
473         (setcdr (assoc topic-name gnus-topic-alist)
474                 (append groups (list old)))
475         (gnus-message 9 "%s: %s moved to %d, swap with %s"
476                       loc group position old)))))
477
478 (defun gnus-sync-lesync-pre-save-group-entry (url nentry &rest passed-props)
479   (let* ((loc "gnus-sync-lesync-save-group-entry")
480          (k (car nentry))
481          (revision (gnus-sync-lesync-get-prop 'rev k))
482          (sname gnus-sync-lesync-name)
483          (topic (gnus-group-topic k))
484          (topic-offset (gnus-sync-topic-group-position k topic))
485          (sources (gnus-sync-lesync-get-prop 'source k)))
486     ;; set the revision so we don't have a conflict
487     `(,@(when revision
488           (list (cons '_rev revision)))
489       (_id . ,k)
490       ;; the time we saved
491       ,@passed-props
492       ;; add our name to the sources list for this key
493       (source ,@(if (member gnus-sync-lesync-name sources)
494                     sources
495                   (cons gnus-sync-lesync-name sources)))
496       ,(cons 'level (nth 1 nentry))
497       ,@(if topic (list (cons 'topic topic)) nil)
498       ,@(if topic-offset (list (cons 'topic-offset topic-offset)) nil)
499       ;; the read marks
500       ,(cons 'read (gnus-sync-range2invlist (nth 2 nentry)))
501       ;; the other marks
502       ,@(delq nil (mapcar (lambda (mark-entry)
503                             (gnus-message 12 "%s: prep param %s in %s"
504                                           loc
505                                           (car mark-entry)
506                                           (nth 3 nentry))
507                             (if (listp (cdr mark-entry))
508                                 (cons (car mark-entry)
509                                       (gnus-sync-range2invlist
510                                        (cdr mark-entry)))
511                               (progn    ; else this is not a list
512                                 (gnus-message 9 "%s: non-list param %s in %s"
513                                               loc
514                                               (car mark-entry)
515                                               (nth 3 nentry))
516                                 nil)))
517                           (nth 3 nentry))))))
518
519 (defun gnus-sync-lesync-post-save-group-entry (url entry)
520   (let* ((loc "gnus-sync-lesync-post-save-group-entry")
521          (k (cdr (assq 'id entry))))
522     (cond
523      ;; success!
524      ((and (assq 'rev entry) (assq 'id entry))
525       (progn
526         (gnus-sync-lesync-set-prop 'rev k (cdr (assq 'rev entry)))
527         (gnus-sync-lesync-set-prop 'checksum
528                                    k
529                                    (gnus-sync-deep-print
530                                     (assoc k gnus-newsrc-alist)))
531         (gnus-message 9 "%s: successfully synced %s to %s"
532                       loc k url)))
533      ;; specifically check for document conflicts
534      ((equal "conflict" (format "%s" (cdr-safe (assq 'error entry))))
535       (gnus-error
536        1
537        "%s: use `%s' to resolve the conflict synchronizing %s to %s: %s"
538        loc "gnus-sync-read" k url (cdr (assq 'reason entry))))
539      ;; generic errors
540      ((assq 'error entry)
541       (gnus-error 1 "%s: got error while synchronizing %s to %s: %s"
542                   loc k url (cdr (assq 'reason entry))))
543
544      (t
545       (gnus-message 2 "%s: unknown sync status after %s to %s: %S"
546                     loc k url entry)))
547     (assoc 'error entry)))
548
549 (defun gnus-sync-lesync-groups-builder (url)
550   (let ((u (concat url gnus-sync-lesync-design-prefix "/_view/groups")))
551     (cdr (assq 'rows (gnus-sync-lesync-GET u nil)))))
552
553 (defun gnus-sync-subscribe-group (name)
554   "Subscribe to group NAME.  Returns NAME on success, nil otherwise."
555   (gnus-subscribe-newsgroup name))
556
557 (defun gnus-sync-lesync-read-group-entry (url name entry &rest passed-props)
558   "Read ENTRY information for NAME.  Returns NAME if successful.
559 Skips entries whose sources don't contain
560 `gnus-sync-lesync-name'.  When the alist PASSED-PROPS has a
561 `subscribe-all' element that evaluates to true, we attempt to
562 subscribe to unknown groups.  The user is also allowed to delete
563 unwanted groups via the LeSync URL."
564   (let* ((loc "gnus-sync-lesync-read-group-entry")
565          (entry (gnus-sync-lesync-normalize-group-entry entry passed-props))
566          (subscribe-all (cdr (assq 'subscribe-all passed-props)))
567          (sources (cdr (assq 'source entry)))
568          (rev (cdr (assq 'rev entry)))
569          (in-sources (member gnus-sync-lesync-name sources))
570          (known (assoc name gnus-newsrc-alist))
571          cell)
572     (unless known
573       (if (and subscribe-all
574                (y-or-n-p (format "Subscribe to group %s?" name)))
575           (setq known (gnus-sync-subscribe-group name)
576                 in-sources t)
577         ;; else...
578         (when (y-or-n-p (format "Delete group %s from server?" name))
579           (if (equal name (gnus-sync-lesync-delete-group url name))
580               (gnus-message 1 "%s: removed group %s from server %s"
581                             loc name url)
582             (gnus-error 1 "%s: could not remove group %s from server %s"
583                         loc name url)))))
584     (when known
585       (unless in-sources
586         (setq in-sources
587               (y-or-n-p
588                (format "Read group %s even though %s is not in sources %S?"
589                        name gnus-sync-lesync-name (or sources ""))))))
590     (when rev
591       (gnus-sync-lesync-set-prop 'rev name rev))
592
593     ;; if the source matches AND we have this group
594     (if (and known in-sources)
595         (progn
596           (gnus-message 10 "%s: reading LeSync entry %s, sources %S"
597                         loc name sources)
598           (while entry
599             (setq cell (pop entry))
600             (let ((k (car cell))
601                   (val (cdr cell)))
602               (gnus-sync-lesync-set-prop k name val)))
603           name)
604       ;; else...
605       (unless known
606         (gnus-message 5 "%s: ignoring entry %s, it wasn't subscribed.  %s"
607                         loc name "Call `gnus-sync-read' with C-u to force it."))
608       (unless in-sources
609         (gnus-message 5 "%s: ignoring entry %s, %s not in sources %S"
610                       loc name gnus-sync-lesync-name (or sources "")))
611       nil)))
612
613 (defun gnus-sync-lesync-install-group-entry (name)
614   (let* ((master (assoc name gnus-newsrc-alist))
615          (old-topic-name (gnus-group-topic name))
616          (old-topic (assoc old-topic-name gnus-topic-alist))
617          (target-topic-name (gnus-sync-lesync-get-prop 'topic name))
618          (target-topic-offset (gnus-sync-lesync-get-prop 'topic-offset name))
619          (target-topic (assoc target-topic-name gnus-topic-alist))
620          (loc "gnus-sync-lesync-install-group-entry"))
621     (if master
622         (progn
623           (when (eq 'ask gnus-sync-lesync-install-topics)
624             (setq gnus-sync-lesync-install-topics
625                   (y-or-n-p "Install topics from LeSync?")))
626           (when (and (eq t gnus-sync-lesync-install-topics)
627                      target-topic-name)
628             (if (equal old-topic-name target-topic-name)
629                 (gnus-message 12 "%s: %s is already in topic %s"
630                               loc name target-topic-name)
631               ;; see `gnus-topic-move-group'
632               (when (and old-topic target-topic)
633                 (setcdr old-topic (gnus-delete-first name (cdr old-topic)))
634                 (gnus-message 5 "%s: removing %s from topic %s"
635                               loc name old-topic-name))
636               (unless target-topic
637                 (when (y-or-n-p (format "Create missing topic %s?"
638                                         target-topic-name))
639                   (gnus-topic-create-topic target-topic-name nil)
640                   (setq target-topic (assoc target-topic-name
641                                             gnus-topic-alist))))
642               (if target-topic
643                   (prog1
644                       (nconc target-topic (list name))
645                     (gnus-message 5 "%s: adding %s to topic %s"
646                                   loc name (car target-topic))
647                     (gnus-topic-enter-dribble))
648                 (gnus-error 2 "%s: LeSync group %s can't go in missing topic %s"
649                             loc name target-topic-name)))
650             (when (and target-topic-offset target-topic)
651               (gnus-sync-fix-topic-group-position
652                name target-topic-name target-topic-offset)))
653           ;; install the subscription level
654           (when (gnus-sync-lesync-get-prop 'level name)
655             (setf (nth 1 master) (gnus-sync-lesync-get-prop 'level name)))
656           ;; install the read and other marks
657           (setf (nth 2 master) (gnus-sync-lesync-get-prop 'read name))
658           (setf (nth 3 master) (gnus-sync-lesync-get-prop 'marks name))
659           (gnus-sync-lesync-set-prop 'checksum
660                                      name
661                                      (gnus-sync-deep-print master))
662           nil)
663       (gnus-error 1 "%s: invalid LeSync group %s" loc name)
664       'invalid-name)))
665
666 ; (gnus-sync-lesync-delete-group (cdr gnus-sync-backend) "nntp+Gmane:gwene.org.slashdot")
667
668 (defun gnus-sync-lesync-delete-group (url name)
669   "Returns NAME if successful deleting it from URL, an error otherwise."
670   (interactive "sEnter URL to set up: \rsEnter group name: ")
671   (let* ((u (concat (cadr gnus-sync-backend) "/" (url-hexify-string name)))
672          (del (gnus-sync-lesync-DELETE
673                u
674                `(,@(when (gnus-sync-lesync-get-prop 'rev name)
675                      (list (cons "If-Match"
676                                  (gnus-sync-lesync-get-prop 'rev name))))))))
677     (or (cdr (assq 'id del)) del)))
678
679 ;;; (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)))
680
681 (defun gnus-sync-lesync-normalize-group-entry (entry &optional passed-props)
682   (let (ret
683         marks
684         cell)
685     (setq entry (append passed-props entry))
686     (while (setq cell (pop entry))
687       (let ((k (car cell))
688             (val (cdr cell)))
689         (cond
690          ((eq k 'read)
691           (push (cons k (gnus-sync-invlist2range val)) ret))
692          ;; we ignore these parameters
693          ((member k '(_id subscribe-all _deleted_conflicts))
694           nil)
695          ((eq k '_rev)
696           (push (cons 'rev val) ret))
697          ((eq k 'source)
698           (push (cons 'source (append val nil)) ret))
699          ((or (eq k 'float-time)
700               (eq k 'level)
701               (eq k 'topic)
702               (eq k 'topic-offset)
703               (eq k 'read-time))
704           (push (cons k val) ret))
705 ;;; "How often have I said to you that when you have eliminated the
706 ;;; impossible, whatever remains, however improbable, must be the
707 ;;; truth?" --Sherlock Holmes
708           ;; everything remaining must be a mark
709           (t (push (cons k (gnus-sync-invlist2range val)) marks)))))
710     (cons (cons 'marks marks) ret)))
711
712 (defun gnus-sync-save (&optional force)
713 "Save the Gnus sync data to the backend.
714 With a prefix, FORCE is set and all groups will be saved."
715   (interactive "P")
716   (cond
717    ((and (listp gnus-sync-backend)
718          (eq (nth 0 gnus-sync-backend) 'lesync)
719          (stringp (nth 1 gnus-sync-backend)))
720
721     ;; refresh the revisions if we're forcing the save
722     (when force
723       (mapc (lambda (entry)
724               (when (and (assq 'key entry)
725                          (assq 'value entry))
726                 (gnus-sync-lesync-set-prop
727                  'rev
728                  (cdr (assq 'key entry))
729                  (cdr (assq 'value entry)))))
730             ;; the revs view is key = name, value = rev
731             (cdr (assq 'rows (gnus-sync-lesync-GET
732                               (concat (nth 1 gnus-sync-backend)
733                                       gnus-sync-lesync-design-prefix
734                                       "/_view/revs")
735                               nil)))))
736
737     (let* ((ftime (float-time))
738            (url (nth 1 gnus-sync-backend))
739            (entries
740             (mapcar (lambda (entry)
741                       (gnus-sync-lesync-pre-save-group-entry
742                        (cadr gnus-sync-backend)
743                        entry
744                        (cons 'float-time ftime)))
745                     (gnus-sync-newsrc-loader-builder (not force))))
746            ;; when there are no entries, there's nothing to save
747            (sync (if entries
748                      (gnus-sync-lesync-POST
749                       (concat url "/_bulk_docs")
750                       '(("Content-Type" . "application/json"))
751                       `((docs . ,(vconcat entries nil))))
752                    (gnus-message
753                     2 "gnus-sync-save: nothing to save to the LeSync backend")
754                    nil)))
755       (mapcar (lambda (e) (gnus-sync-lesync-post-save-group-entry url e))
756               sync)))
757    ((stringp gnus-sync-backend)
758     (gnus-message 7 "gnus-sync-save: saving to backend %s" gnus-sync-backend)
759     ;; populate gnus-sync-newsrc-loader from all but the first dummy
760     ;; entry in gnus-newsrc-alist whose group matches any of the
761     ;; gnus-sync-newsrc-groups
762     ;; TODO: keep the old contents for groups we don't have!
763     (let ((gnus-sync-newsrc-loader
764            (loop for entry in (cdr gnus-newsrc-alist)
765                  when (gnus-grep-in-list
766                        (car entry)     ;the group name
767                        gnus-sync-newsrc-groups)
768                  collect (cons (car entry)
769                                (mapcar (lambda (offset)
770                                          (cons offset (nth offset entry)))
771                                        gnus-sync-newsrc-offsets)))))
772       (with-temp-file gnus-sync-backend
773         (progn
774           (let ((coding-system-for-write gnus-ding-file-coding-system)
775                 (standard-output (current-buffer)))
776             (princ (format ";; -*- mode:emacs-lisp; coding: %s; -*-\n"
777                            gnus-ding-file-coding-system))
778             (princ ";; Gnus sync data v. 0.0.1\n")
779             ;; TODO: replace with `gnus-sync-deep-print'
780             (let* ((print-quoted t)
781                    (print-readably t)
782                    (print-escape-multibyte nil)
783                    (print-escape-nonascii t)
784                    (print-length nil)
785                    (print-level nil)
786                    (print-circle nil)
787                    (print-escape-newlines t)
788                    (variables (cons 'gnus-sync-newsrc-loader
789                                     gnus-sync-global-vars))
790                    variable)
791               (while variables
792                 (if (and (boundp (setq variable (pop variables)))
793                            (symbol-value variable))
794                     (progn
795                       (princ "\n(setq ")
796                       (princ (symbol-name variable))
797                       (princ " '")
798                       (prin1 (symbol-value variable))
799                       (princ ")\n"))