diff options
Diffstat (limited to 'elfeed-custom-feeds-github.el')
| -rw-r--r-- | elfeed-custom-feeds-github.el | 125 |
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 "<" "<" | ||
| 90 | (replace-regexp-in-string ">" ">" 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 | ||
