aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorEric Abrahamsen2023-06-17 18:47:59 -0700
committerEric Abrahamsen2023-06-17 18:47:59 -0700
commit5c0cf970799d4d20473b6d232c1061ac0366cd85 (patch)
tree0b0a2fe08467cfe77921d72ddc84fadd73995585
parent1b0348d95934a66d9991a7331ab55e1b9a6c1367 (diff)
downloademacs-gnus/nnatom.tar.gz
emacs-gnus/nnatom.zip
Add nnatom.elgnus/nnatom
Direct copy from https://git.sr.ht/~dsemy/nnatom, with updated copyright header.
-rw-r--r--lisp/gnus/nnatom.el540
1 files changed, 540 insertions, 0 deletions
diff --git a/lisp/gnus/nnatom.el b/lisp/gnus/nnatom.el
new file mode 100644
index 00000000000..83ef1c3f28d
--- /dev/null
+++ b/lisp/gnus/nnatom.el
@@ -0,0 +1,540 @@
1;;; nnatom.el --- Atom backend for Gnus -*- lexical-binding: t -*-
2
3;; Copyright (C) 2023 Free Software Foundation, Inc.
4
5;; Author: Daniel Semyonov <daniel@dsemy.com>
6
7;; This file is not part of GNU Emacs.
8
9;; nnatom is free software: you can redistribute it and/or modify
10;; it under the terms of the GNU General Public License as published by
11;; the Free Software Foundation, either version 3 of the License, or
12;; (at your option) any later version.
13
14;; nnatom is distributed in the hope that it will be useful,
15;; but WITHOUT ANY WARRANTY; without even the implied warranty of
16;; MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
17;; GNU General Public License for more details.
18
19;; You should have received a copy of the GNU General Public License
20;; along with nnatom. If not, see <https://www.gnu.org/licenses/>.
21
22;;; Commentary:
23
24;; Gnus backend for Atom feeds.
25
26;;; Code:
27
28(eval-when-compile
29 (require 'cl-lib)
30 (require 'gv)
31 (require 'subr-x))
32
33(require 'gnus)
34(require 'nnheader)
35(require 'nnoo)
36(require 'gnus-group)
37(require 'mm-url)
38
39(defgroup nnatom nil
40 "Atom backend for Gnus."
41 :group 'gnus)
42
43(nnoo-declare nnatom)
44
45(defvoo nnatom-backend 'nnatom
46 "Symbol which identifies this backend.")
47
48(defvoo nnatom-status-string nil
49 "Last status message reported by this backend.")
50
51(defsubst nnatom--backend-prefix (backend)
52 (concat (symbol-name backend) ":"))
53
54;;;; Atom feed parser:
55
56(defsubst nnatom--node-attributes (node subnode)
57 (cadr (assq subnode node)))
58
59(defsubst nnatom--node-subnodes (node subnode)
60 (let ((sub (cddr (assq subnode node))))
61 (or (and (atom (car sub)) (car sub)) sub)))
62
63(defun nnatom--parse-feed (feed)
64 "Return a list structure representing FEED, or nil."
65 (if (string-match-p "^https?://" feed)
66 (nnheader-report
67 nnatom-backend
68 "Address shouldn't start with \"http://\" or \"https://\"")
69 (with-temp-buffer
70 (condition-case e
71 (if (file-readable-p feed)
72 (insert-file-contents feed)
73 (mm-url-insert-file-contents (concat "https://" feed)))
74 (file-error (nnheader-report nnatom-backend (cdr e)))
75 (:success (cddr (libxml-parse-xml-region (point-min) (point-max))))))))
76
77(defun nnatom--read-article (data _)
78 "Return the next article and the remaining DATA in a cons cell, or nil."
79 (and data
80 `(,(let (article)
81 (while (not (eq (car (setq article (car data))) 'entry))
82 (setq data (cdr data)))
83 (setq data (cdr data))
84 article)
85 . ,data)))
86
87(defun nnatom--read-title (feed)
88 "Return the title of FEED, or nil."
89 (nnatom--node-subnodes feed 'title))
90
91(defun nnatom--read-article-or-feed-author (article-or-feed)
92 "Return the author of ARTICLE-OR-FEED, or nil."
93 (let* ((author (nnatom--node-subnodes article-or-feed 'author))
94 (name (nnatom--node-subnodes author 'name))
95 (email (nnatom--node-subnodes author 'email)))
96 (or (and name email (format "%s <%s>" name email))
97 name email)))
98
99(defun nnatom--read-id (article)
100 "Return the ID of ARTICLE.
101If the ARTICLE doesn't contain an ID but it does contain a subject,
102return the subject. Otherwise, return nil."
103 (or (nnatom--node-subnodes article 'id)
104 (nnatom--read-subject article)))
105
106(defun nnatom--read-subject (article)
107 "Return the subject of ARTICLE, or nil."
108 (nnatom--node-subnodes article 'title))
109
110(defun nnatom--read-publish (article)
111 "Return the date and time ARTICLE was published, or nil."
112 (when-let ((pub (nnatom--node-subnodes article 'published)))
113 (and (stringp pub) (date-to-time pub))))
114
115(defun nnatom--read-update (article)
116 "Return the date and time of the last update to ARTICLE, or nil."
117 (when-let ((update (nnatom--node-subnodes article 'updated)))
118 (and (stringp update) (date-to-time update))))
119
120(defun nnatom--read-link (article)
121 "Return the link to ARTICLE, or nil."
122 (alist-get 'href (or (nnatom--node-attributes article 'link)
123 (nnatom--node-subnodes article 'link))))
124
125(defun nnatom--read-summary-or-content-type (article summary-or-content)
126 "Return the type of SUMMARY-OR-CONTENT of ARTICLE, or \"plain\"."
127 (or (cdr (assq 'type
128 (nnatom--node-attributes article summary-or-content)))
129 "plain"))
130
131(defun nnatom--read-summary-or-content (article summary-or-content)
132 "Return SUMMARY-OR-CONTENT of ARTICLE, or nil."
133 (nnatom--node-subnodes article summary-or-content))
134
135;;;; Feed I/O:
136
137(defvoo nnatom-read-feed-function #'nnatom--parse-feed
138 "Function returning a Lisp object representing a feed (or part of it).
139It should accept a single argument, the address of a feed.")
140
141(defvoo nnatom-read-article-function #'nnatom--read-article
142 "Function returning a cons cell of an article and remaining data from a feed.
143It should accept a two arguments, a Lisp object representing a feed,
144and a flag indicating whether the last article was stale (not new or updated).
145If there are no remaining articles, it should return nil.")
146
147(defvoo nnatom-read-title-function #'nnatom--read-title
148 "Function returning the title of a feed (a string).
149It should accept a single argument, a Lisp object representing a feed.")
150
151(defvoo nnatom-read-feed-author-function #'nnatom--read-article-or-feed-author
152 "Function returning the author of a feed (a string).
153It should accept a single argument, a Lisp object representing a feed.")
154
155(defvoo nnatom-read-id-function #'nnatom--read-id
156 "Function returning the ID of an article.
157It should accept a single argument, a Lisp object representing an article.")
158
159(defvoo nnatom-read-subject-function #'nnatom--read-subject
160 "Function returning the subject of an article (a string).
161It should accept a single argument, a Lisp object representing an article.")
162
163(defvoo nnatom-read-publish-date-function #'nnatom--read-publish
164 "Function returning the publish date of an article (a time value).
165It should accept a single argument, a Lisp object representing an article.")
166
167(defvoo nnatom-read-update-date-function #'nnatom--read-update
168 "Function returning the update date of an article (a time value).
169It should accept a single argument, a Lisp object representing an article.")
170
171(defvoo nnatom-read-author-function #'nnatom--read-article-or-feed-author
172 "Function returning the author of an article (a string).
173It should accept a single argument, a Lisp object representing an article.")
174
175(defvoo nnatom-read-link-function #'nnatom--read-link
176 "Function returning the link contained in an article (a string).
177It should accept a single argument, a Lisp object representing an article.")
178
179(defvoo nnatom-read-summary-type-function
180 (lambda (article)
181 (nnatom--read-summary-or-content-type article 'summary))
182 "Function returning the type of the summary of an article (a string).
183This is appended to \"text/\" to form the MIME type of the summary.
184It should accept a single argument, a Lisp object representing an article.")
185
186(defvoo nnatom-read-content-type-function
187 (lambda (article)
188 (nnatom--read-summary-or-content-type article 'content))
189 "Function returning the type of the content of an article (a string).
190This is appended to \"text/\" to form the MIME type of the content.
191It should accept a single argument, a Lisp object representing an article.")
192
193(defvoo nnatom-read-summary-function
194 (lambda (article)
195 (nnatom--read-summary-or-content article 'summary))
196 "Function returning the summary of an article (a string).
197It should accept a single argument, a Lisp object representing an article.")
198
199(defvoo nnatom-read-content-function
200 (lambda (article)
201 (nnatom--read-summary-or-content article 'content))
202 "Function returning the content of an article (a string).
203It should accept a single argument, a Lisp object representing an article.")
204
205(defvoo nnatom-groups (make-hash-table :test 'equal)
206 "Hash table mapping known group names to their data.
207
208Each value in this table should be a vector of the form
209[GROUP IDS ARTICLES MAX MIN], where:
210- GROUP is the name of the group.
211- IDS is a hash table mapping article IDs to their numbers.
212- ARTICLES is a hash table mapping article numbers to articles and
213 their attributes (see `nnatom-group-articles').
214- MAX is the maximum article number.
215- MIN is the minimum article number.")
216
217(defun nnatom-group-file (group)
218 "Return the file containing feed data for GROUP."
219 (expand-file-name (format "%s/%s.eld"
220 (string-trim (symbol-name nnatom-backend)
221 "nn")
222 (gnus-newsgroup-savable-name group))
223 gnus-directory))
224
225(defun nnatom--read-feed (feed &optional group)
226 "Read FEED into a new or existing GROUP."
227 (let ((prefix (nnatom--backend-prefix nnatom-backend)))
228 (when (string-suffix-p "-ephemeral" feed)
229 (setq feed (or (cadr (assq (nnoo-symbol nnatom-backend 'address)
230 (cddr (gnus-server-to-method
231 (concat prefix feed)))))
232 feed)))
233 (when-let ((data (funcall nnatom-read-feed-function feed))
234 (group (or (and group (string-trim group prefix))
235 (cadr (assq (nnoo-symbol nnatom-backend 'name)
236 (gnus-server-to-method
237 (concat prefix feed))))
238 (funcall nnatom-read-title-function data))))
239 (let* ((info (gnus-get-info (concat prefix group)))
240 (g (or (gethash group nnatom-groups)
241 (nnatom-read-group group)
242 `[ ,group ,(make-hash-table :test 'equal)
243 ,(make-hash-table :test 'eql) nil 1]))
244 (ids (aref g 1))
245 (articles (aref g 2))
246 (max (aref g 3))
247 (max (if max max
248 (setq max 0) ; Find max article number
249 (dolist ; remembered by Gnus.
250 ( r (cons (gnus-info-read info)
251 (gnus-info-marks info))
252 max)
253 (mapc (lambda (x)
254 (let ((x (if (consp x)
255 (if (< (car x) (cdr x))
256 (cdr x) (car x))
257 x)))
258 (when (< max x) (setq max x))))
259 (if (symbolp (car r)) (cdr r) r)))))
260 (feed-author (funcall nnatom-read-feed-author-function data))
261 article stale)
262 (while (setq article (funcall nnatom-read-article-function data stale)
263 data (cdr article)
264 article (car article))
265 (when-let ((id (funcall nnatom-read-id-function article))
266 (id (format "<%s@%s.%s>" id group nnatom-backend)))
267 (let* ((num (gethash id ids))
268 (update (funcall nnatom-read-update-date-function article))
269 (prev-update (aref (gethash num articles
270 '[nil nil nil nil nil])
271 4)))
272 (if (or (null num) ; New article ID.
273 (and (null prev-update) update)
274 (and prev-update update
275 (time-less-p prev-update update)))
276 (let* ((num (or num (aset g 3 (cl-incf max))))
277 (publish (funcall nnatom-read-publish-date-function
278 article)))
279 (setf
280 (gethash id (aref g 1)) num
281 (gethash num (aref g 2))
282 `[ ,id
283 ,(or (funcall nnatom-read-author-function article)
284 feed-author group)
285 ,(or (funcall nnatom-read-subject-function article)
286 "no subject")
287 ,(or publish update '(0 0)) ; published
288 ,(or update publish '(0 0)) ; updated
289 ,(funcall nnatom-read-link-function article)
290 (,(funcall nnatom-read-summary-function article)
291 . ,(funcall nnatom-read-summary-type-function article))
292 (,(funcall nnatom-read-content-function article)
293 . ,(funcall
294 nnatom-read-content-type-function
295 article))]
296 stale nil))
297 (setq stale t)))))
298 (puthash group g nnatom-groups)))))
299
300(defun nnatom-read-group (group)
301 "Read GROUP's information from `nnatom-directory'."
302 (if-let ((f (nnatom-group-file group))
303 ((file-readable-p f)))
304 (with-temp-buffer
305 (insert-file-contents f)
306 (goto-char (point-min))
307 (puthash group (read (current-buffer)) nnatom-groups))
308 (nnheader-report nnatom-backend "Can't read %s")))
309
310(defun nnatom-write-group (group)
311 "Write GROUP's information to `nnatom-directory'."
312 (if-let ((f (nnatom-group-file group))
313 ((file-writable-p f)))
314 (if-let ((g (gethash group nnatom-groups))
315 ((vectorp g)))
316 (with-temp-file f
317 (insert ";;;; -*- mode: lisp-data -*- DO NOT EDIT\n"
318 (prin1-to-string g))
319 t)
320 t)
321 (nnheader-report nnatom-backend "Can't write %s" f)))
322
323;;;; Gnus backend functions:
324
325(defvoo nnatom-group nil
326 "Name of the current group.")
327
328(defvoo nnatom-group-article-ids (make-hash-table :test 'equal)
329 "Hash table mapping article IDs to their article number.")
330
331(defvoo nnatom-group-articles (make-hash-table :test 'eql)
332 "Hash table mapping article numbers to articles and their attributes.
333
334Each value in this table should be a vector of the form
335[ID FROM SUBJECT DATE UPDATED LINK SUMMARY CONTENT], where:
336- ID is the ID of the article.
337- FROM is the author of the article or group.
338- SUBJECT is the subject of the article.
339- DATE is the date the article was published, or last updated (time value).
340- UPDATE is the date the article was last updated, or published (time value).
341- LINK is the URL to the full (remote) version of the article.
342- SUMMARY and CONTENT are cons cells where the car is the raw
343 summary/content of the article and the cdr is the \"type\" of
344 the summary/content (appended to \"text/\" to form the MIME type).")
345
346(defvoo nnatom-group-article-max-num 0
347 "Maximum article number for the current group.")
348
349(defvoo nnatom-group-article-min-num 1
350 "Minimum article number for the current group.")
351
352(defvar nnatom-date-format "%F %X"
353 "Format of displayed dates.")
354
355(nnoo-define-basics nnatom)
356
357(defun nnatom-retrieve-article (article group)
358 (if-let ((a (gethash article (aref group 2))))
359 (insert (format "221 %s Article retrieved.
360From: %s\nSubject: %s\nDate: %s\nMessage-ID: %s\n.\n"
361 article
362 (aref a 1)
363 (aref a 2)
364 (format-time-string nnatom-date-format (aref a 3))
365 (aref a 0)))
366 (insert "404 Article not found.\n.\n")))
367
368(deffoo nnatom-retrieve-headers (articles &optional group _server _fetch-old)
369 (if-let ((g (or (gethash group nnatom-groups)
370 (nnatom-read-group group)
371 `[ nil ,nnatom-group-article-ids
372 ,nnatom-group-articles nil nil])))
373 (with-current-buffer nntp-server-buffer
374 (erase-buffer)
375 (or (and (stringp (car articles))
376 (mapc (lambda (a)
377 (nnatom-retrieve-article
378 (gethash a (aref g 1)) g))
379 articles))
380 (and (numberp (car articles))
381 (range-map (lambda (a) (nnatom-retrieve-article a g))
382 articles)))
383 'headers)
384 (nnheader-report nnatom-backend "Group %s not found" (or group ""))))
385
386(deffoo nnatom-request-close ()
387 (setq nnatom-groups (make-hash-table :test 'equal)
388 nnatom-status-string nil)
389 t)
390
391(defun nnatom--print-content (content type link)
392 "Return CONTENT with LINK appended and formatted according to TYPE."
393 (when (and content type)
394 (let ((html (or (string= type "html") (string= type "xhtml"))))
395 (concat (when html "<html><head></head><body>") content "\n\n"
396 (and (stringp link)
397 (if html (format "<p><a href=\"%s\">[Link]</a></p>" link)
398 link))
399 (when html "</body></html>")))))
400
401(deffoo nnatom-request-article (article &optional group _server to-buffer)
402 (if-let ((g (or (gethash group nnatom-groups)
403 (and nnatom-group
404 `[ nil ,nnatom-group-article-ids
405 ,nnatom-group-articles
406 ,nnatom-group-article-max-num
407 ,nnatom-group-article-min-num])))
408 (num (or (and (stringp article)
409 (gethash article (aref g 1)))
410 (and (numberp article) article)))
411 ((and (<= num (aref g 3))
412 (>= num (aref g 4))))
413 (a (gethash num (aref g 2))))
414 (with-current-buffer (or to-buffer nntp-server-buffer)
415 (erase-buffer)
416 (let* ((boundary (format "-_%s_-" nnatom-backend))
417 (link (aref a 5))
418 (summary (aref a 6))
419 (summary-type (cdr summary))
420 (summary
421 (nnatom--print-content (car summary) summary-type link))
422 (content (aref a 7))
423 (content-type (cdr content))
424 (content
425 (nnatom--print-content (car content) content-type link)))
426 (insert (format
427 "Subject: %s\nFrom: %s\nDate: %s\nMessage-ID: %s\n"
428 (aref a 2) (aref a 1)
429 (format-time-string
430 nnatom-date-format (or (aref a 3) '(0 0)))
431 (aref a 0))
432 "MIME-Version: 1.0\nContent-Type: "
433 (concat (cond ((and summary content)
434 (format "multipart/alternative; boundary=%s
435--%s\nContent-Type: text/%s\n\n%s\n--%s\nContent-Type: text/%s\n\n%s\n--%s--\n"
436 boundary boundary summary-type
437 summary boundary content-type
438 content boundary))
439 (summary (format "text/%s\n\n%s\n"
440 summary-type summary))
441 (content (format "text/%s\n\n%s\n"
442 content-type content))
443 (link (format "text/plain\n\n%s\n" link))
444 (t "text/plain\n\n")))))
445 `(,group . ,num))
446 (nnheader-report nnatom-backend "No such article")))
447
448(deffoo nnatom-request-group (group &optional server fast _info)
449 (with-current-buffer nntp-server-buffer
450 (erase-buffer)
451 (if-let ((s (or server (nnoo-current-server nnatom-backend)))
452 (g (or (if fast
453 (or (gethash group nnatom-groups)
454 (nnatom-read-group group))
455 (nnatom--read-feed s group))
456 `[ ,group ,(make-hash-table :test 'equal)
457 ,(make-hash-table :test 'eql) 0 1])))
458 (progn
459 (setq nnatom-group group
460 nnatom-group-article-ids (aref g 1)
461 nnatom-group-articles (aref g 2)
462 nnatom-group-article-max-num (aref g 3)
463 nnatom-group-article-min-num (aref g 4))
464 (insert (format "211 %s %s %s \"%s\""
465 (hash-table-count nnatom-group-article-ids)
466 nnatom-group-article-min-num
467 nnatom-group-article-max-num group))
468 t)
469 (insert "404 group not found")
470 (nnheader-report nnatom-backend "Group %s not found" group))))
471
472(deffoo nnatom-close-group (group &optional _server)
473 (if (nnatom-write-group group)
474 (setq nnatom-group nil
475 nnatom-group-article-ids (make-hash-table :test 'equal)
476 nnatom-group-articles (make-hash-table :test 'eql)
477 nnatom-group-article-max-num 0
478 nnatom-group-article-min-num 1)
479 (nnheader-report nnatom-backend "Couldn't write group %s" group)))
480
481(deffoo nnatom-request-list (&optional server)
482 (with-current-buffer nntp-server-buffer
483 (erase-buffer)
484 (when-let ((g (nnatom--read-feed
485 (or server (nnoo-current-server nnatom-backend)))))
486 (insert (format "\"%s\" %s %s y\n"
487 (aref g 0) (aref g 3) (aref g 4)))
488 t)))
489
490(deffoo nnatom-request-post (&optional _server)
491 (nnheader-report nnatom-backend "%s is a read only backend" nnatom-backend))
492
493;;;; Optional back end functions:
494
495(deffoo nnatom-retrieve-groups (_groups &optional server)
496 (nnatom-request-list (or server (nnoo-current-server nnatom-backend)))
497 'active)
498
499(deffoo nnatom-request-type (_group &optional _article)
500 'unknown)
501
502(deffoo nnatom-request-delete-group (group _force &optional _server)
503 (delete-file (nnatom-group-file group))
504 (remhash group nnatom-groups)
505 (when (string= group nnatom-group)
506 (setq nnatom-group nil
507 nnatom-group-article-ids (make-hash-table :test 'equal)
508 nnatom-group-articles (make-hash-table :test 'eql)
509 nnatom-group-article-max-num 0
510 nnatom-group-article-min-num 1))
511 t)
512
513(gnus-declare-backend (symbol-name nnatom-backend) 'address)
514
515;;;; Utilities:
516
517(defmacro nnatom-define-basic-backend-interface (backend)
518 "Define a basic set of functions and variables for BACKEND."
519 (let ((bp (symbol-name backend)))
520 `(progn
521 (defvoo ,(intern (concat bp "-backend")) ',backend nil nnatom-backend)
522 (defvoo ,(intern (concat bp "-status-string"))
523 nil nil nnatom-status-string)
524 (defvoo ,(intern (concat bp "-group")) nil nil nnatom-group)
525 (defvoo ,(intern (concat bp "-groups"))
526 (make-hash-table :test 'equal) nil nnatom-groups)
527 (defvoo ,(intern (concat bp "-group-article-ids"))
528 (make-hash-table :test 'equal) nil nnatom-group-article-ids)
529 (defvoo ,(intern (concat bp "-group-articles"))
530 (make-hash-table :test 'eql) nil nnatom-group-articles)
531 (defvoo ,(intern (concat bp "-group-article-max-num")) 0 nil
532 nnatom-group-article-max-num)
533 (defvoo ,(intern (concat bp "-group-article-mix-num")) 1 nil
534 nnatom-group-article-min-num)
535 (nnoo-define-basics ,backend)
536 (nnoo-import ,backend (nnatom)))))
537
538(provide 'nnatom)
539
540;;; nnatom.el ends here