summaryrefslogtreecommitdiff
path: root/elfeed-custom-feeds-github.el
diff options
context:
space:
mode:
authorVineet <vin@hastur.epistemia>2025-10-21 14:13:31 -0400
committerVineet <vin@hastur.epistemia>2025-10-21 14:23:27 -0400
commit6c76f25427d88faa5176efa1458cceca83afe533 (patch)
tree55e2222ccdcbf531f0933524d0a2d6e88297e59a /elfeed-custom-feeds-github.el
initial commit
Diffstat (limited to 'elfeed-custom-feeds-github.el')
-rw-r--r--elfeed-custom-feeds-github.el125
1 files changed, 125 insertions, 0 deletions
diff --git a/elfeed-custom-feeds-github.el b/elfeed-custom-feeds-github.el
new file mode 100644
index 0000000..660350d
--- /dev/null
+++ b/elfeed-custom-feeds-github.el
@@ -0,0 +1,125 @@
1;;; elfeed-custom-feeds-github.el --- GitHub PR feed generator for Elfeed -*- lexical-binding: t; -*-
2
3;; Copyright (C) 2025
4
5;; SPDX-License-Identifier: GPL-3.0-or-later
6
7;;; Commentary:
8
9;; GitHub pull request feed generator for Elfeed with custom queries.
10
11;;; Code:
12
13(require 'elfeed)
14(require 'url)
15(require 'json)
16(require 'cl-lib)
17
18(defgroup elfeed-custom-feeds-github nil
19 "GitHub PR feed generator for Elfeed."
20 :group 'elfeed
21 :prefix "elfeed-custom-github-")
22
23(defcustom elfeed-custom-github-pr-queries
24 '(("emacs-mirror/emacs" "" "Emacs")
25 ("atlas-engineer/nyxt" "is:open" "Nyxt")
26 ("qutebrowser/qutebrowser" "label:\"bug: behavior\"" "qutebrowser behavior bug"))
27 "List of (repo query display-name) tuples for GitHub PR monitoring."
28 :type '(repeat (list (string :tag "Repository")
29 (string :tag "Query")
30 (string :tag "Display Name")))
31 :group 'elfeed-custom-feeds-github)
32
33(defcustom elfeed-custom-github-request-delay 1.0
34 "Delay in seconds between GitHub API requests to avoid rate limiting."
35 :type 'number
36 :group 'elfeed-custom-feeds-github)
37
38(defun elfeed-custom-feeds-github--fetch-prs (repo query display-name callback delay)
39 "Fetch PRs for REPO matching QUERY after DELAY seconds and call CALLBACK with results."
40 (let ((url (format "https://api.github.com/search/issues?q=repo:%s+is:pr+%s&sort=created&order=desc&per_page=20"
41 repo (url-hexify-string query)))
42 (cb callback)
43 (r repo)
44 (dn display-name))
45 (run-at-time delay nil
46 (lambda ()
47 (url-retrieve url
48 (lambda (status)
49 (if (plist-get status :error)
50 (message "Error fetching GitHub PRs %s [%s]: %s" r dn (plist-get status :error))
51 (goto-char (point-min))
52 (re-search-forward "\n\n")
53 (let* ((json-object-type 'alist)
54 (json-array-type 'list)
55 (response (json-read))
56 (items (alist-get 'items response)))
57 (funcall cb r dn items))))
58 nil t)))))
59
60(defun elfeed-custom-feeds-github--prs-to-elfeed (repo display-name prs)
61 "Convert GitHub PRS to elfeed entries for REPO with DISPLAY-NAME."
62 (let ((feed-url (format "https://github.com/%s/pulls" repo))
63 (new-count 0)
64 (new-entries ()))
65 (let ((feed (elfeed-db-get-feed feed-url)))
66 (setf (elfeed-feed-url feed) feed-url
67 (elfeed-feed-title feed) (format "GitHub: %s [%s]" repo display-name)))
68
69 (dolist (pr prs)
70 (let* ((number (alist-get 'number pr))
71 (title-text (alist-get 'title pr))
72 (html-url (alist-get 'html_url pr))
73 (state (alist-get 'state pr))
74 (updated (alist-get 'updated_at pr))
75 (created (alist-get 'created_at pr))
76 (user (alist-get 'login (alist-get 'user pr)))
77 (body (or (alist-get 'body pr) "No description provided."))
78 (labels (mapcar (lambda (label) (alist-get 'name label))
79 (alist-get 'labels pr)))
80 (entry-id html-url)
81 (title (format "#%d: %s" number title-text))
82 (date (float-time (date-to-time updated)))
83 (content (format "<h2>PR #%d: %s</h2>\n<p><strong>State:</strong> %s</p>\n<p><strong>Author:</strong> %s</p>\n<p><strong>Created:</strong> %s</p>\n<p><strong>Updated:</strong> %s</p>\n%s\n<h3>Description:</h3>\n<pre>%s</pre>"
84 number title-text state user created updated
85 (if labels
86 (format "<p><strong>Labels:</strong> %s</p>"
87 (mapconcat 'identity labels ", "))
88 "")
89 (replace-regexp-in-string "<" "&lt;"
90 (replace-regexp-in-string ">" "&gt;" body)))))
91 (unless (elfeed-db-get-entry entry-id)
92 (let ((entry (elfeed-entry--create
93 :id entry-id
94 :title title
95 :link html-url
96 :date date
97 :content content
98 :content-type 'html
99 :feed-id feed-url
100 :tags '(unread github))))
101 (push entry new-entries)
102 (setq new-count (1+ new-count))))))
103
104 (when new-entries
105 (elfeed-db-add (nreverse new-entries)))
106
107 (message "GitHub feed %s [%s]: %d new entries" repo display-name new-count)
108 (when (get-buffer "*elfeed-search*")
109 (with-current-buffer "*elfeed-search*"
110 (elfeed-search-update--force)))))
111
112;;;###autoload
113(defun elfeed-custom-feeds-github-update ()
114 "Update all GitHub PR feeds."
115 (interactive)
116 (cl-loop for (repo query display-name) in elfeed-custom-github-pr-queries
117 for i from 0
118 for delay = (* i elfeed-custom-github-request-delay)
119 do (elfeed-custom-feeds-github--fetch-prs repo query display-name
120 #'elfeed-custom-feeds-github--prs-to-elfeed
121 delay)))
122
123(provide 'elfeed-custom-feeds-github)
124
125;;; elfeed-custom-feeds-github.el ends here