;;; my-net.el -- Network related extensions for emacs core -*- lexical-binding: t -*- ;; Copyright (C) 2023 Free Software Foundation. ;; Author: Yuchen Pei ;; Package-Requires: ((emacs "28.2")) ;; This file is part of dotfiles. ;; dotfiles is free software: you can redistribute it and/or modify it under ;; the terms of the GNU Affero General Public License as published by ;; the Free Software Foundation, either version 3 of the License, or ;; (at your option) any later version. ;; dotfiles is distributed in the hope that it will be useful, but WITHOUT ;; ANY WARRANTY; without even the implied warranty of MERCHANTABILITY ;; or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General ;; Public License for more details. ;; You should have received a copy of the GNU Affero General Public ;; License along with dotfiles. If not, see . ;;; Commentary: ;; Network related extensions for emacs core. ;;; Code: ;;; net utilities (defvar my-download-dir "~/Downloads") (defun my-make-file-name-from-url (url) (file-name-nondirectory (directory-file-name (car (url-path-and-query (url-generic-parse-url (url-unhex-string url))))))) ;; stolen from `eww-make-unique-file-name' (defun my-make-unique-file-name (file directory) "Uniquefy FILE under DIRECTORY. Like `expand-file-name', but make sure the file name has not been taken." (cond ((zerop (length file)) (setq file "!")) ((string-match "\\`[.]" file) (setq file (concat "!" file)))) (let ((count 1) (stem file) (suffix "")) (when (string-match "\\`\\(.*\\)\\([.][^.]+\\)" file) (setq stem (match-string 1 file) suffix (match-string 2 file))) (while (file-exists-p (expand-file-name file directory)) (setq file (format "%s(%d)%s" stem count suffix)) (setq count (1+ count))) (expand-file-name file directory))) (defun my-fetch-url (url &optional no-overwrite) "Fetch URL to a buffer, save it to a file, and switch to the buffer. The file is saved under `my-download-dir'. If NO-OVERWRITE is non-nil, do not overwrite any existing file." (interactive "sURL: ") (let ((file-name (if no-overwrite (my-make-unique-file-name (my-make-file-name-from-url url) my-download-dir) (expand-file-name (my-make-file-name-from-url url) my-download-dir)))) (url-retrieve url 'my-fetch-url-save-and-switch (list file-name)))) (defun my-fetch-url-save-and-switch (status file-name) "A `url-retrieve' callback that saves the payload and switch to it. It checks the STATUS, and if it is ok, saves the payload to FILE-NAME." (when (plist-get status :error) (error "My fetch failed: %s" (plist-get status :error))) (my-delete-http-header) (let ((to-insert (buffer-string))) (kill-buffer) (my-save-text-and-switch-to-buffer to-insert file-name)) ) (defun my-kill-http-header () (my-skip-http-header) (let ((killed (buffer-substring-no-properties (point-min) (point)))) (delete-region (point-min) (point)) killed)) (defun my-parse-http-header (text) (let ((status) (fields)) (with-temp-buffer (insert text) (goto-char (point-min)) (re-search-forward "^HTTP.*\\([0-9]\\{3\\}\\).*$") (setq status (match-string 1)) (while (re-search-forward "^\\(.*?\\): \\(.*\\)$" nil t) (push (cons (intern (match-string 1)) (match-string 2)) fields))) (list (cons 'status status) (cons 'fields fields)))) (defvar my-client-buffer-name "*my-api*") (defun my-url-fetch-json (url &optional decompression with-header) (my-url-fetch-internal url (lambda () (json-read-from-string (decode-coding-string (buffer-string) 'utf-8))) decompression with-header)) (defun my-url-fetch-dom (url &optional decompression with-header) (my-url-fetch-internal url (lambda () (libxml-parse-html-region (point) (point-max))) decompression with-header)) (defun my-url-fetch-internal (url buffer-processor decompression with-header) (with-current-buffer (get-buffer-create my-client-buffer-name) (goto-char (point-max)) (insert "[" (current-time-string) "] Request: " url "\n")) (with-current-buffer (url-retrieve-synchronously url t) (let ((header (my-kill-http-header)) (status) (fields)) (goto-char (point-min)) (setq header (my-parse-http-header header) status (alist-get 'status header) fields (alist-get 'fields header)) (with-current-buffer my-client-buffer-name (insert "[" (current-time-string) "] Response: " status "\n")) (when decompression (call-process-region (point) (point-max) "gunzip" t t t) (goto-char (point-min))) (call-interactively 'delete-trailing-whitespace) (if (string= status "200") (unless (= (point) (point-max)) (if with-header (list (cons 'header fields) (cons 'json (funcall buffer-processor))) (funcall buffer-processor))) (error "HTTP error: %s" (buffer-substring (point) (point-max))))))) (provide 'my-net) ;;; my-net.el ends here