branch: elpa/fedi
commit 4039c80b718f11ec6d43136b686391b5ec4971fc
Author: marty hiatt <martianhiatus [a t] riseup [d o t] net>
Commit: marty hiatt <martianhiatus [a t] riseup [d o t] net>

    init
---
 fedi-http.el | 368 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
 1 file changed, 368 insertions(+)

diff --git a/fedi-http.el b/fedi-http.el
new file mode 100644
index 00000000000..914a8621d73
--- /dev/null
+++ b/fedi-http.el
@@ -0,0 +1,368 @@
+;;; fedi-http.el --- HTTP request/response functions  -*- lexical-binding: t 
-*-
+
+;; Copyright (C) 2020-2022 Marty Hiatt
+;; Author: Marty Hiatt <[email protected]>
+;; Version: 0.0.1
+;; Package-Requires: ((emacs "27.1"))
+;; Homepage: https://codeberg.org/martianh/fedi.el
+
+;; This file is not part of GNU Emacs.
+
+;; fedi-http.el is free software: you can redistribute it and/or modify
+;; it under the terms of the GNU General Public License as published by
+;; the Free Software Foundation, either version 3 of the License, or
+;; (at your option) any later version.
+
+;; fedi.el 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 General Public License for more details.
+
+;; You should have received a copy of the GNU General Public License
+;; along with fedi.el.  If not, see <http://www.gnu.org/licenses/>.
+
+;;; Commentary:
+
+;; fed-http.el provides HTTP request/response functions. Code from
+;; mastodon-http.el, see its boilerplate for authorship, etc.
+
+;;; Code:
+
+(require 'json)
+;; (require 'request) ; for attachments upload
+(require 'url)
+
+(defvar fedi-instance-url)
+(defvar fedi-toot--media-attachment-ids)
+(defvar fedi-toot--media-attachment-filenames)
+
+(autoload 'shr-render-buffer "shr")
+(autoload 'fedi-auth--access-token "fedi-auth")
+(autoload 'fedi-toot--update-status-fields "fedi-toot")
+
+(defvar fedi-http--api-version "v3")
+
+(defconst fedi-http--timeout 15
+  "HTTP request timeout, in seconds.  Has no effect on Emacs < 26.1.")
+
+(defun fedi-http--api (endpoint)
+  "Return Fedi API URL for ENDPOINT."
+  (concat fedi-instance-url "/api/"
+          fedi-http--api-version "/" endpoint))
+
+(defun fedi-http--response ()
+  "Capture response buffer content as string."
+  (with-current-buffer (current-buffer)
+    (buffer-substring-no-properties (point-min) (point-max))))
+
+(defun fedi-http--response-body (pattern)
+  "Return substring matching PATTERN from `fedi-http--response'."
+  (let ((resp (fedi-http--response)))
+    (string-match pattern resp)
+    (match-string 0 resp)))
+
+(defun fedi-http--status ()
+  "Return HTTP Response Status Code from `fedi-http--response'."
+  (let* ((status-line (fedi-http--response-body "^HTTP/1.*$")))
+    (string-match "[0-9][0-9][0-9]" status-line)
+    (match-string 0 status-line)))
+
+(defun fedi-http--url-retrieve-synchronously (url &optional silent)
+  "Retrieve URL asynchronously.
+This is a thin abstraction over the system
+`url-retrieve-synchronously'.  Depending on which version of this
+is available we will call it with or without a timeout.
+SILENT means don't message."
+  (if (< (cdr (func-arity 'url-retrieve-synchronously)) 4)
+      (url-retrieve-synchronously url)
+    (url-retrieve-synchronously url (or silent nil) nil fedi-http--timeout)))
+
+(defun fedi-http--triage (response success)
+  "Determine if RESPONSE was successful.
+Call SUCCESS if successful. Message status and JSON error from
+RESPONSE if unsuccessful."
+  (let ((status (with-current-buffer response
+                  (fedi-http--status))))
+    (if (string-prefix-p "2" status)
+        (funcall success)
+      (if (string-prefix-p "404" status)
+          (message "Error %s: page not found" status)
+        (let ((json-response (with-current-buffer response
+                               (fedi-http--process-json))))
+          (message "Error %s: %s" status (alist-get 'error json-response)))))))
+
+(defun fedi-http--read-file-as-string (filename)
+  "Read a file FILENAME as a string. Used to generate image preview."
+  (with-temp-buffer
+    (insert-file-contents filename)
+    (string-to-unibyte (buffer-string))))
+
+(defmacro fedi-http--authorized-request (method body &optional 
unauthenticated-p)
+  "Make a METHOD type request using BODY, with Fedi authorization.
+Unless UNAUTHENTICATED-P is non-nil."
+  (declare (debug 'body)
+           (indent 1))
+  `(let ((url-request-method ,method)
+         (url-request-extra-headers
+          (unless ,unauthenticated-p
+            (list (cons "Authorization"
+                        (concat "Bearer " (fedi-auth--access-token)))))))
+     ,body))
+
+(defun fedi-http--build-params-string (params)
+  "Build a request parameters string from parameters alist PARAMS."
+  ;; (url-build-query-string args nil))
+  ;; url-build-query-string adds 'nil' for empty params so lets stick with our
+  ;; own:
+  (mapconcat (lambda (p)
+               (concat (url-hexify-string (car p))
+                       "=" (url-hexify-string (cdr p))))
+             params "&"))
+
+(defun fedi-http--build-array-params-alist (param-str array)
+  "Return parameters alist using PARAM-STR and ARRAY param values.
+Used for API form data parameters that take an array."
+  (cl-loop for x in array
+           collect (cons param-str x)))
+
+(defun fedi-http--post (url &optional params headers unauthenticated-p)
+  "POST synchronously to URL, optionally with PARAMS and HEADERS.
+Authorization header is included by default unless UNAUTHENTICATED-P is 
non-nil."
+  (fedi-http--authorized-request "POST"
+    (let ((url-request-data (when params
+                              (fedi-http--build-params-string params)))
+          (url-request-extra-headers
+           (append url-request-extra-headers ; auth set in macro
+                   (unless (assoc "Content-Type" headers) ; pleroma compat:
+                     '(("Content-Type" . "application/x-www-form-urlencoded")))
+                   headers)))
+      (with-temp-buffer
+        (fedi-http--url-retrieve-synchronously url)))
+    unauthenticated-p))
+
+(defun fedi-http--concat-params-to-url (url params)
+  "Build a query string with PARAMS and concat to URL."
+  (if params
+      (concat url "?"
+              (fedi-http--build-params-string params))
+    url))
+
+(defun fedi-http--get (url &optional params silent)
+  "Make synchronous GET request to URL.
+PARAMS is an alist of any extra parameters to send with the request.
+SILENT means don't message."
+  (fedi-http--authorized-request "GET"
+    ;; url-request-data doesn't seem to work with GET requests?:
+    (let ((url (fedi-http--concat-params-to-url url params)))
+      (fedi-http--url-retrieve-synchronously url silent))
+    t))
+
+(defun fedi-http--get-response (url &optional params no-headers silent vector)
+  "Make synchronous GET request to URL. Return JSON and response headers.
+PARAMS is an alist of any extra parameters to send with the request.
+SILENT means don't message.
+NO-HEADERS means don't collect http response headers.
+VECTOR means return json arrays as vectors."
+  (with-current-buffer (fedi-http--get url params silent)
+    (fedi-http--process-response no-headers vector)))
+
+(defun fedi-http--get-json (url &optional params silent vector)
+  "Return only JSON data from URL request.
+PARAMS is an alist of any extra parameters to send with the request.
+SILENT means don't message.
+VECTOR means return json arrays as vectors."
+  (car (fedi-http--get-response url params :no-headers silent vector)))
+
+(defun fedi-http--process-json ()
+  "Return only JSON data from async URL request.
+Callback to `fedi-http--get-json-async', usually
+`fedi-tl--init*', is run on the result."
+  (car (fedi-http--process-response :no-headers)))
+
+(defun fedi-http--render-html-err (string)
+  "Render STRING as HTML in a temp buffer.
+STRING should be a HTML for a 404 errror."
+  (with-temp-buffer
+    (insert string)
+    (shr-render-buffer (current-buffer))
+    (view-mode) ; for 'q' to kill buffer and window
+    (error ""))) ; stop subsequent processing
+
+(defun fedi-http--process-response (&optional no-headers vector)
+  "Process http response.
+Return a cons of JSON list and http response headers.
+If NO-HEADERS is non-nil, just return the JSON.
+VECTOR means return json arrays as vectors.
+Callback to `fedi-http--get-response-async', usually
+`fedi-tl--init*', is run on the result."
+  ;; view raw response:
+  ;; (switch-to-buffer (current-buffer))
+  (let ((headers (unless no-headers
+                   (fedi-http--process-headers))))
+    (goto-char (point-min))
+    (re-search-forward "^$" nil 'move)
+    (let ((json-array-type (if vector 'vector 'list))
+          (json-string (decode-coding-string
+                        (buffer-substring-no-properties (point) (point-max))
+                        'utf-8)))
+      (kill-buffer)
+      (cond ((or (string-empty-p json-string) (null json-string))
+             nil)
+            ;; if we get html, just render it and error:
+            ;; ideally we should handle the status code in here rather than
+            ;; this crappy hack?
+            ((string-prefix-p "\n<!" json-string) ; html hack
+             (fedi-http--render-html-err json-string))
+            ;; if no json or html, maybe we have a plain string error message
+            ;; (misskey does this, but there are probably better ways to do
+            ;; this):
+            ((not (or (string-prefix-p "\n{" json-string)
+                      (string-prefix-p "\n[" json-string)))
+             (error "%s" json-string))
+            (t
+             `(,(json-read-from-string json-string) . ,headers))))))
+
+(defun fedi-http--process-headers ()
+  "Return an alist of http response headers."
+  (switch-to-buffer (current-buffer))
+  (goto-char (point-min))
+  (let* ((head-str (buffer-substring-no-properties
+                    (point-min)
+                    (re-search-forward "^$" nil 'move)))
+         (head-list (split-string head-str "\n")))
+    (mapcar (lambda (x)
+              (let ((list (split-string x ": ")))
+                (cons (car list) (cadr list))))
+            head-list)))
+
+(defun fedi-http--delete (url &optional params)
+  "Make DELETE request to URL.
+PARAMS is an alist of any extra parameters to send with the request."
+  ;; url-request-data only works with POST requests?
+  (let ((url (fedi-http--concat-params-to-url url params)))
+    (fedi-http--authorized-request "DELETE"
+      (with-temp-buffer
+        (fedi-http--url-retrieve-synchronously url)))))
+
+(defun fedi-http--put (url &optional params headers)
+  "Make PUT request to URL.
+PARAMS is an alist of any extra parameters to send with the request.
+HEADERS is an alist of any extra headers to send with the request."
+  (fedi-http--authorized-request "PUT"
+    (let ((url-request-data
+           (when params (fedi-http--build-params-string params)))
+          (url-request-extra-headers
+           (append url-request-extra-headers ; auth set in macro
+                   (unless (assoc "Content-Type" headers) ; pleroma compat:
+                     '(("Content-Type" . "application/x-www-form-urlencoded")))
+                   headers)))
+      (with-temp-buffer (fedi-http--url-retrieve-synchronously url)))))
+
+;; profile update functions
+
+(defun fedi-http--patch-json (url &optional params)
+  "Make synchronous PATCH request to URL. Return JSON response.
+Optionally specify the PARAMS to send."
+  (with-current-buffer (fedi-http--patch url params)
+    (fedi-http--process-json)))
+
+(defun fedi-http--patch (base-url &optional params)
+  "Make synchronous PATCH request to BASE-URL.
+Optionally specify the PARAMS to send."
+  (fedi-http--authorized-request "PATCH"
+    (let ((url (fedi-http--concat-params-to-url base-url params)))
+      (fedi-http--url-retrieve-synchronously url))))
+
+ ;; Asynchronous functions
+
+(defun fedi-http--get-async (url &optional params callback &rest cbargs)
+  "Make GET request to URL.
+Pass response buffer to CALLBACK function with args CBARGS.
+PARAMS is an alist of any extra parameters to send with the request."
+  (let ((url (fedi-http--concat-params-to-url url params)))
+    (fedi-http--authorized-request "GET"
+      (url-retrieve url callback cbargs))))
+
+(defun fedi-http--get-response-async (url &optional params callback &rest 
cbargs)
+  "Make GET request to URL. Call CALLBACK with http response and CBARGS.
+PARAMS is an alist of any extra parameters to send with the request."
+  (fedi-http--get-async
+   url
+   params
+   (lambda (status)
+     (when status ; for flakey servers
+       (apply callback (fedi-http--process-response) cbargs)))))
+
+(defun fedi-http--get-json-async (url &optional params callback &rest cbargs)
+  "Make GET request to URL. Call CALLBACK with json-list and CBARGS.
+PARAMS is an alist of any extra parameters to send with the request."
+  (fedi-http--get-async
+   url
+   params
+   (lambda (status)
+     (when status ;; only when we actually get sth?
+       (apply callback (fedi-http--process-json) cbargs)))))
+
+(defun fedi-http--post-async (url params _headers &optional callback &rest 
cbargs)
+  "POST asynchronously to URL with PARAMS and HEADERS.
+Then run function CALLBACK with arguements CBARGS.
+Authorization header is included by default unless UNAUTHENTICED-P is non-nil."
+  (fedi-http--authorized-request "POST"
+    (let ((request-timeout 5)
+          (url-request-data (when params
+                              (fedi-http--build-params-string params))))
+      (with-temp-buffer
+        (url-retrieve url callback cbargs)))))
+
+;; ;; TODO: test for curl first?
+;; (defun fedi-http--post-media-attachment (url filename caption)
+;;   "Make POST request to upload FILENAME with CAPTION to the server's media 
URL.
+;; The upload is asynchronous. On succeeding,
+;; `fedi-toot--media-attachment-ids' is set to the id(s) of the
+;; item uploaded, and `fedi-toot--update-status-fields' is run."
+;;   (let* ((file (file-name-nondirectory filename))
+;;          (request-backend 'curl))
+;;     (request
+;;       url
+;;       :type "POST"
+;;       :params `(("description" . ,caption))
+;;       :files `(("file" . (,file :file ,filename
+;;                                 :mime-type "multipart/form-data")))
+;;       :parser 'json-read
+;;       :headers `(("Authorization" . ,(concat "Bearer "
+;;                                              (fedi-auth--access-token))))
+;;       :sync nil
+;;       :success (cl-function
+;;                 (lambda (&key data &allow-other-keys)
+;;                   (when data
+;;                     (push (alist-get 'id data)
+;;                           fedi-toot--media-attachment-ids) ; add ID to list
+;;                     (message "%s file %s with id %S and caption '%s' 
uploaded!"
+;;                              (capitalize (alist-get 'type data))
+;;                              file
+;;                              (alist-get 'id data)
+;;                              (alist-get 'description data))
+;;                     (fedi-toot--update-status-fields))))
+;;       :error (cl-function
+;;               (lambda (&key error-thrown &allow-other-keys)
+;;                 (cond
+;;                  ;; handle curl errors first (eg 26, can't read file/path)
+;;                  ;; because the '=' test below fails for them
+;;                  ;; they have the form (error . error message 24)
+;;                  ((not (proper-list-p error-thrown)) ; not dotted list
+;;               (message "Got error: %s. Shit went south." (cdr 
error-thrown)))
+;;                  ;; handle fedi api errors
+;;                  ;; they have the form (error http 401)
+;;              ((= (car (last error-thrown)) 401)
+;;                   (message "Got error: %s Unauthorized: The access token is 
invalid"
+;;                            error-thrown))
+;;                  ((= (car (last error-thrown)) 422)
+;;                   (message "Got error: %s Unprocessable entity: file or 
file\
+;;  type is unsupported or invalid"
+;;                            error-thrown))
+;;                  (t
+;;                   (message "Got error: %s Shit went south"
+;;                            error-thrown))))))))
+
+(provide 'fedi-http)
+;;; fedi-http.el ends here

Reply via email to