[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]
[elpa] externals/srht b1bdf349ab 07/27: Add build.yml and external depen
From: |
ELPA Syncer |
Subject: |
[elpa] externals/srht b1bdf349ab 07/27: Add build.yml and external dependencies. |
Date: |
Tue, 17 May 2022 22:57:59 -0400 (EDT) |
branch: externals/srht
commit b1bdf349abe838e5faac8586136978dcf7e8bb53
Author: Aleksandr Vityazev <avityazev@posteo.org>
Commit: Aleksandr Vityazev <avityazev@posteo.org>
Add build.yml and external dependencies.
---
.build.yml | 11 ++
Eldev | 2 +-
external/plz.el | 523 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++
3 files changed, 535 insertions(+), 1 deletion(-)
diff --git a/.build.yml b/.build.yml
new file mode 100644
index 0000000000..11ef39da39
--- /dev/null
+++ b/.build.yml
@@ -0,0 +1,11 @@
+image: guix
+packages:
+ - make
+ - emacs
+ - emacs-eldev
+sources:
+ - https://git.sr.ht/~akagi/srht.el
+tasks:
+ - check: |
+ cd srht.el
+ make test
diff --git a/Eldev b/Eldev
index e13ca687bc..b5a00dde97 100644
--- a/Eldev
+++ b/Eldev
@@ -8,4 +8,4 @@
;; (eldev-use-plugin 'autoloads)
;; (file-name-directory (find-library-name "plz"))
(setq eldev-project-main-file "lisp/srht.el")
-(eldev-use-local-dependency
"/gnu/store/r48pqszz01hvph933i7k5m2h0cwh0wpz-emacs-rrr-plz-0.1-pre-2.1d3efc0/share/emacs/site-lisp/rrr-plz-0.1-pre-2.1d3efc0/")
+(eldev-use-local-dependency "./external/" 'source)
diff --git a/external/plz.el b/external/plz.el
new file mode 100644
index 0000000000..62fd5563b8
--- /dev/null
+++ b/external/plz.el
@@ -0,0 +1,523 @@
+;;; plz.el --- HTTP library -*- lexical-binding: t; -*-
+
+;; Copyright (C) 2019 Adam Porter
+
+;; Author: Adam Porter <adam@alphapapa.net>
+;; URL: https://github.com/alphapapa/plz.el
+;; Version: 0.1-pre
+;; Package-Requires: ((emacs "26.3"))
+;; Keywords: comm, network, http
+
+;;; License:
+
+;; This program 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.
+
+;; This program 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 this program. If not, see <https://www.gnu.org/licenses/>.
+
+;;; Commentary:
+;;
+;; An HTTP library that uses curl as a backend. Inspired by, and some
+;; code copied from, Christopher Wellons's library, elfeed-curl.el.
+;;
+;; Why this package?
+;;
+;; 1. `url' works well for many things, but it has some issues (and have
+;; you seen its code?).
+;; 2. `request' works well for many things, but it has some issues (and
+;; have you seen its code?).
+;; 3. Chris Wellons doesn't have time to factor his excellent
+;; elfeed-curl.el library out of Elfeed. This will have to do.
+;;
+;; Why is it called `plz'?
+;;
+;; 1. There's already a package called `http'.
+;; 2. There's already a package called `request'.
+;; 3. Naming things is hard.
+
+;;; Code:
+
+;;;; Requirements
+
+(require 'cl-lib)
+(require 'rx)
+(require 'subr-x)
+
+;;;; Errors
+
+;; FIXME: `condition-case' can't catch these...?
+(define-error 'plz-curl-error "Curl error")
+(define-error 'plz-http-error "HTTP error")
+
+;;;; Structs
+
+(cl-defstruct plz-response
+ version status headers body)
+
+(cl-defstruct plz-error
+ curl-error response message)
+
+;;;; Constants
+
+(defconst plz-http-response-status-line-regexp
+ (rx bol "HTTP/" (group (1+ (or digit "."))) (1+ blank)
+ (group (1+ digit)))
+ "Regular expression matching HTTP response status line.")
+
+(defconst plz-curl-errors
+ ;; Copied from elfeed-curl.el.
+ '((1 . "Unsupported protocol.")
+ (2 . "Failed to initialize.")
+ (3 . "URL malformed. The syntax was not correct.")
+ (4 . "A feature or option that was needed to perform the desired request
was not enabled or was explicitly disabled at build-time.")
+ (5 . "Couldn't resolve proxy. The given proxy host could not be resolved.")
+ (6 . "Couldn't resolve host. The given remote host was not resolved.")
+ (7 . "Failed to connect to host.")
+ (8 . "FTP weird server reply. The server sent data curl couldn't parse.")
+ (9 . "FTP access denied.")
+ (11 . "FTP weird PASS reply.")
+ (13 . "FTP weird PASV reply.")
+ (14 . "FTP weird 227 format.")
+ (15 . "FTP can't get host.")
+ (17 . "FTP couldn't set binary.")
+ (18 . "Partial file. Only a part of the file was transferred.")
+ (19 . "FTP couldn't download/access the given file, the RETR (or similar)
command failed.")
+ (21 . "FTP quote error. A quote command returned error from the server.")
+ (22 . "HTTP page not retrieved.")
+ (23 . "Write error.")
+ (25 . "FTP couldn't STOR file.")
+ (26 . "Read error. Various reading problems.")
+ (27 . "Out of memory. A memory allocation request failed.")
+ (28 . "Operation timeout.")
+ (30 . "FTP PORT failed.")
+ (31 . "FTP couldn't use REST.")
+ (33 . "HTTP range error. The range \"command\" didn't work.")
+ (34 . "HTTP post error. Internal post-request generation error.")
+ (35 . "SSL connect error. The SSL handshaking failed.")
+ (36 . "FTP bad download resume.")
+ (37 . "FILE couldn't read file.")
+ (38 . "LDAP bind operation failed.")
+ (39 . "LDAP search failed.")
+ (41 . "Function not found. A required LDAP function was not found.")
+ (42 . "Aborted by callback.")
+ (43 . "Internal error. A function was called with a bad parameter.")
+ (45 . "Interface error. A specified outgoing interface could not be used.")
+ (47 . "Too many redirects.")
+ (48 . "Unknown option specified to libcurl.")
+ (49 . "Malformed telnet option.")
+ (51 . "The peer's SSL certificate or SSH MD5 fingerprint was not OK.")
+ (52 . "The server didn't reply anything, which here is considered an
error.")
+ (53 . "SSL crypto engine not found.")
+ (54 . "Cannot set SSL crypto engine as default.")
+ (55 . "Failed sending network data.")
+ (56 . "Failure in receiving network data.")
+ (58 . "Problem with the local certificate.")
+ (59 . "Couldn't use specified SSL cipher.")
+ (60 . "Peer certificate cannot be authenticated with known CA
certificates.")
+ (61 . "Unrecognized transfer encoding.")
+ (62 . "Invalid LDAP URL.")
+ (63 . "Maximum file size exceeded.")
+ (64 . "Requested FTP SSL level failed.")
+ (65 . "Sending the data requires a rewind that failed.")
+ (66 . "Failed to initialise SSL Engine.")
+ (67 . "The user name, password, or similar was not accepted and curl
failed to log in.")
+ (68 . "File not found on TFTP server.")
+ (69 . "Permission problem on TFTP server.")
+ (70 . "Out of disk space on TFTP server.")
+ (71 . "Illegal TFTP operation.")
+ (72 . "Unknown TFTP transfer ID.")
+ (73 . "File already exists (TFTP).")
+ (74 . "No such user (TFTP).")
+ (75 . "Character conversion failed.")
+ (76 . "Character conversion functions required.")
+ (77 . "Problem with reading the SSL CA cert (path? access rights?).")
+ (78 . "The resource referenced in the URL does not exist.")
+ (79 . "An unspecified error occurred during the SSH session.")
+ (80 . "Failed to shut down the SSL connection.")
+ (82 . "Could not load CRL file, missing or wrong format (added in
7.19.0).")
+ (83 . "Issuer check failed (added in 7.19.0).")
+ (84 . "The FTP PRET command failed")
+ (85 . "RTSP: mismatch of CSeq numbers")
+ (86 . "RTSP: mismatch of Session Identifiers")
+ (87 . "unable to parse FTP file list")
+ (88 . "FTP chunk callback reported error")
+ (89 . "No connection available, the session will be queued")
+ (90 . "SSL public key does not matched pinned public key"))
+ "Alist mapping curl error code integers to helpful error messages.")
+
+;;;; Variables
+
+(defvar-local plz-else nil
+ "Callback function for errored completion of request.
+Called in current curl process buffer.")
+
+(defvar-local plz-then nil
+ "Callback function for successful completion of request.
+Called in current curl process buffer.")
+
+(defvar-local plz-finally nil
+ "Function called unconditionally after completion of request.
+Called after the then/else function, without arguments, outside
+the curl process buffer.")
+
+(defvar-local plz-result nil
+ "Used when `plz' is called synchronously.")
+
+(defvar-local plz-sync nil
+ "Used when `plz' is called synchronously.")
+
+;;;; Customization
+
+(defgroup plz nil
+ "Options for `plz'."
+ :group 'network
+ :link '(url-link "https://github.com/alphapapa/plz.el"))
+
+(defcustom plz-curl-program "curl"
+ "Name of curl program to call."
+ :type 'string)
+
+(defcustom plz-curl-default-args
+ '("--silent"
+ "--compressed"
+ "--location"
+ "--dump-header" "-")
+ "Default arguments to curl.
+Note that these arguments are passed on the command line, which
+may be visible to other users on the local system."
+ :type '(repeat string))
+
+(defcustom plz-connect-timeout 5
+ "Default connection timeout in seconds.
+This limits how long the connection phase may last (the
+\"--connect-timeout\" argument to curl)."
+ :type 'number)
+
+(defcustom plz-timeout 60
+ "Default request timeout in seconds.
+This limits how long an entire request may take, including the
+connection phase and waiting to receive the response (the
+\"--max-time\" argument to curl)."
+ :type 'number)
+
+;;;; Functions
+
+;;;;; Public
+
+(cl-defun plz (method url &key headers body else finally noquery
+ (as 'string) (then 'sync)
+ (body-type 'text) (decode t decode-s)
+ (connect-timeout plz-connect-timeout) (timeout
plz-timeout))
+ "Request METHOD from URL with curl.
+Return the curl process object or, for a synchronous request, the
+selected result.
+
+HEADERS may be an alist of extra headers to send with the
+request.
+
+BODY-TYPE may be `text' to send BODY as text, or `binary' to send
+it as binary.
+
+AS selects the kind of result to pass to the callback function
+THEN, or the kind of result to return for synchronous requests.
+It may be:
+
+- `buffer' to pass the response buffer.
+
+- `binary' to pass the response body as an undecoded string.
+
+- `string' to pass the response body as a decoded string.
+
+- `response' to pass a `plz-response' struct.
+
+- `file' to pass a temporary filename to which the response body
+ has been saved without decoding.
+
+- `(file FILENAME)' to pass FILENAME after having saved the
+ response body to it without decoding. FILENAME must be a
+ non-existent file; if it exists, it will not be overwritten,
+ and an error will be signaled.
+
+- A function, which is called in the response buffer with it
+ narrowed to the response body (suitable for, e.g. `json-read').
+
+If DECODE is non-nil, the response body is decoded automatically.
+For binary content, it should be nil. When AS is `binary',
+DECODE is automatically set to nil.
+
+THEN is a callback function, whose sole argument is selected
+above with AS. Or THEN may be `sync' to make a synchronous
+request, in which case the result is returned directly.
+
+ELSE is an optional callback function called when the request
+fails with one argument, a `plz-error' struct. If ELSE is nil,
+an error is signaled when the request fails, either
+`plz-curl-error' or `plz-http-error' as appropriate, with a
+`plz-error' struct as the error data. For synchronous requests,
+this argument is ignored.
+
+FINALLY is an optional function called without argument after
+THEN or ELSE, as appropriate. For synchronous requests, this
+argument is ignored.
+
+CONNECT-TIMEOUT and TIMEOUT are a number of seconds that limit
+how long it takes to connect to a host and to receive a response
+from a host, respectively.
+
+NOQUERY is passed to `make-process', which see."
+ ;; Inspired by and copied from `elfeed-curl-retrieve'.
+ (declare (indent defun))
+ (setf decode (if (and decode-s (not decode))
+ nil decode))
+ ;; NOTE: By default, for PUT requests and POST requests >1KB, curl sends an
+ ;; "Expect:" header, which causes servers to send a "100 Continue" response,
which
+ ;; we don't want to have to deal with, so we disable it by setting the
header to
+ ;; the empty string. See <https://gms.tf/when-curl-sends-100-continue.html>.
+ ;; TODO: Handle "100 Continue" responses and remove this workaround.
+ (push (cons "Expect" "") headers)
+ (let* ((data-arg (pcase-exhaustive body-type
+ ('binary "--data-binary")
+ ('text "--data")))
+ (curl-command-line-args (append plz-curl-default-args
+ (list "--config" "-")))
+ (curl-config-header-args (cl-loop for (key . value) in headers
+ collect (cons "--header" (format
"%s: %s" key value))))
+ (curl-config-args (append curl-config-header-args
+ (list (cons "--url" url))
+ (when connect-timeout
+ (list (cons "--connect-timeout"
+ (number-to-string
connect-timeout))))
+ (when timeout
+ (list (cons "--max-time"
(number-to-string timeout))))
+ (pcase method
+ ((or 'put 'post)
+ (cl-assert body)
+ (list (cons "--request" (upcase
(symbol-name method)))
+ ;; It appears that this must be
the last argument
+ ;; in order to pass data on the
rest of STDIN.
+ (cons data-arg "@-")))
+ ('delete
+ (list (cons "--request" (upcase
(symbol-name method))))))))
+ (curl-config (cl-loop for (key . value) in curl-config-args
+ concat (format "%s \"%s\"\n" key value)))
+ (decode (pcase as
+ ('binary nil)
+ (_ decode)))
+ sync-p)
+ (when (eq 'sync then)
+ (setf sync-p t
+ then (lambda (result)
+ (setf plz-result result))))
+ (with-current-buffer (generate-new-buffer " *plz-request-curl*")
+ ;; Avoid making process in a nonexistent directory (in case the current
+ ;; default-directory has since been removed). It's unclear what the best
+ ;; directory is, but this seems to make sense, and it should still exist.
+ (let ((default-directory temporary-file-directory)
+ (process (make-process :name "plz-request-curl"
+ :buffer (current-buffer)
+ :coding 'binary
+ :command (append (list plz-curl-program)
curl-command-line-args)
+ :connection-type 'pipe
+ :sentinel #'plz--sentinel
+ :stderr (current-buffer)
+ :noquery noquery))
+ ;; The THEN function is called in the response buffer.
+ (then (pcase-exhaustive as
+ ((or 'binary 'string)
+ (lambda ()
+ (let ((coding-system (or (plz--coding-system) 'utf-8)))
+ (pcase as
+ ('binary (set-buffer-multibyte nil)))
+ (plz--narrow-to-body)
+ (when decode
+ (decode-coding-region (point) (point-max)
coding-system))
+ (funcall then (buffer-string)))))
+ ('buffer (lambda ()
+ (funcall then (current-buffer))))
+ ('response (lambda ()
+ (funcall then (plz--response :decode-p
decode))))
+ ('file (lambda ()
+ (set-buffer-multibyte nil)
+ (plz--narrow-to-body)
+ (let ((filename (make-temp-file "plz-")))
+ (condition-case err
+ (write-region (point-min) (point-max)
filename)
+ ;; In case of an error writing to the file,
delete the temp file
+ ;; and signal the error. Ignore any errors
encountered while
+ ;; deleting the file, which would obscure the
original error.
+ (error (ignore-errors
+ (delete-file filename))
+ (signal (car err) (cdr err))))
+ (funcall then filename))))
+ (`(file ,(and (pred stringp) filename))
+ (lambda ()
+ (set-buffer-multibyte nil)
+ (plz--narrow-to-body)
+ (condition-case err
+ (write-region (point-min) (point-max) filename nil
nil nil 'excl)
+ ;; Since we are creating the file, it seems sensible
to delete it in case of an
+ ;; error while writing to it (e.g. a disk-full
error). And we ignore any errors
+ ;; encountered while deleting the file, which would
obscure the original error.
+ (error (ignore-errors
+ (when (file-exists-p filename)
+ (delete-file filename)))
+ (signal (car err) (cdr err))))
+ (funcall then filename)))
+ ((pred functionp) (lambda ()
+ (let ((coding-system (or
(plz--coding-system) 'utf-8)))
+ (plz--narrow-to-body)
+ (when decode
+ (decode-coding-region (point)
(point-max) coding-system))
+ (funcall then (funcall as))))))))
+ (setf plz-then then
+ plz-else else
+ plz-finally finally
+ plz-sync sync-p)
+ ;; Send --config arguments.
+ (process-send-string process curl-config)
+ (when body
+ (cl-typecase body
+ (string (process-send-string process body))
+ (buffer (with-current-buffer body
+ (process-send-region process (point-min) (point-max))))))
+ (process-send-eof process)
+ (if sync-p
+ (progn
+ (while
+ ;; According to the Elisp manual, blocking on a process's
+ ;; output is really this simple. And it seems to work.
+ (accept-process-output process))
+ (prog1 plz-result
+ (unless (eq as 'buffer)
+ (kill-buffer))))
+ process)))))
+
+;;;;; Private
+
+(defun plz--sentinel (process-or-buffer status)
+ "Process buffer of curl output in PROCESS-OR-BUFFER.
+If PROCESS-OR-BUFFER if a process, uses its buffer; if a buffer,
+uses it. STATUS should be the process's event string (see info
+node `(elisp) Sentinels'). Kills the buffer before returning."
+ ;; Inspired by and some code copied from `elfeed-curl--sentinel'.
+ (let* ((buffer (cl-etypecase process-or-buffer
+ (process (process-buffer process-or-buffer))
+ (buffer process-or-buffer)))
+ (finally (buffer-local-value 'plz-finally buffer))
+ sync)
+ (unwind-protect
+ (with-current-buffer buffer
+ (setf sync plz-sync)
+ (pcase-exhaustive status
+ ((or 0 "finished\n")
+ ;; Curl exited normally: check HTTP status code.
+ (pcase (plz--http-status)
+ (200 (funcall plz-then))
+ (_ (let ((err (make-plz-error :response (plz--response))))
+ (pcase-exhaustive plz-else
+ (`nil (signal 'plz-http-error err))
+ ((pred functionp) (funcall plz-else err)))))))
+
+ ((or (and (pred numberp) code)
+ (rx "exited abnormally with code " (let code (group (1+
digit)))))
+ ;; Curl error.
+ (let* ((curl-exit-code (cl-typecase code
+ (string (string-to-number code))
+ (number code)))
+ (curl-error-message (alist-get curl-exit-code
plz-curl-errors))
+ (err (make-plz-error :curl-error (cons curl-exit-code
curl-error-message))))
+ (pcase-exhaustive plz-else
+ ;; FIXME: Returning a plz-error struct which has a curl-error
slot, wrapped in a plz-curl-error, is confusing.
+ (`nil (signal 'plz-curl-error err))
+ ((pred functionp) (funcall plz-else err)))))
+
+ ("killed\n"
+ ;; Curl process killed.
+ (let ((err (make-plz-error :message "curl process killed")))
+ (pcase-exhaustive plz-else
+ (`nil (signal 'plz-curl-error err))
+ ((pred functionp) (funcall plz-else err)))))))
+ (when finally
+ (funcall finally))
+ (unless sync
+ (kill-buffer buffer)))))
+
+;;;;;; HTTP Responses
+
+;; Functions for parsing HTTP responses.
+
+(cl-defun plz--response (&key (decode-p t))
+ "Return response struct for HTTP response in current buffer.
+When DECODE-P is non-nil, decode the response body automatically
+according to the apparent coding system."
+ (save-excursion
+ (goto-char (point-min))
+ ;; Parse HTTP version and status code.
+ (unless (looking-at plz-http-response-status-line-regexp)
+ (error "Unable to parse HTTP response"))
+ (let* ((http-version (string-to-number (match-string 1)))
+ (status-code (string-to-number (match-string 2)))
+ (headers (plz--headers))
+ (coding-system (or (plz--coding-system headers) 'utf-8)))
+ (plz--narrow-to-body)
+ (when decode-p
+ (decode-coding-region (point) (point-max) coding-system))
+ (make-plz-response
+ :version http-version
+ :status status-code
+ :headers headers
+ :body (buffer-string)))))
+
+(defun plz--coding-system (&optional headers)
+ "Return coding system for HTTP response in current buffer.
+HEADERS may optionally be an alist of parsed HTTP headers to
+refer to rather than the current buffer's unparsed headers."
+ (let* ((headers (or headers (plz--headers)))
+ (content-type (alist-get 'content-type headers)))
+ (when content-type
+ (coding-system-from-name content-type))))
+
+(defun plz--http-status ()
+ "Return HTTP status code for HTTP response in current buffer."
+ (save-excursion
+ (goto-char (point-min))
+ (when (looking-at plz-http-response-status-line-regexp)
+ (string-to-number (match-string 2)))))
+
+(defun plz--headers ()
+ "Return headers alist for HTTP response in current buffer."
+ (save-excursion
+ (goto-char (point-min))
+ (forward-line 1)
+ (let ((limit (save-excursion
+ (re-search-forward "^\r\n" nil)
+ (point))))
+ (cl-loop while (re-search-forward (rx bol (group (1+ (not (in ":"))))
":" (1+ blank)
+ (group (1+ (not (in "\r\n")))))
+ limit t)
+ ;; NOTE: Some HTTP servers send all-lowercase header keys,
which means an alist
+ ;; lookup with `equal' or `string=' fails when the case
differs. We don't want
+ ;; users to have to worry about this, so for consistency, we
downcase the
+ ;; header name. And while we're at it, we might as well intern
it so we can
+ ;; use `alist-get' without having to add "nil nil #'equal"
every time.
+ collect (cons (intern (downcase (match-string 1)))
(match-string 2))))))
+
+(defun plz--narrow-to-body ()
+ "Narrow to body of HTTP response in current buffer."
+ (goto-char (point-min))
+ (re-search-forward "^\r\n" nil)
+ (narrow-to-region (point) (point-max)))
+
+;;;; Footer
+
+(provide 'plz)
+
+;;; plz.el ends here
- [elpa] branch externals/srht created (now 0e53961bbc), ELPA Syncer, 2022/05/17
- [elpa] externals/srht f88188bdcb 02/27: Add license., ELPA Syncer, 2022/05/17
- [elpa] externals/srht 0e2038516e 01/27: Initial commit., ELPA Syncer, 2022/05/17
- [elpa] externals/srht cf96533faf 03/27: Add guix.scm., ELPA Syncer, 2022/05/17
- [elpa] externals/srht 65e791cce0 05/27: Move Elisp files to the lisp dir., ELPA Syncer, 2022/05/17
- [elpa] externals/srht b1bdf349ab 07/27: Add build.yml and external dependencies.,
ELPA Syncer <=
- [elpa] externals/srht 41b2163b39 08/27: Replace emacs with emacs-next., ELPA Syncer, 2022/05/17
- [elpa] externals/srht 0db1310218 09/27: Downgrade Emacs requirements to 27.1., ELPA Syncer, 2022/05/17
- [elpa] externals/srht 235929a7a6 21/27: Update README., ELPA Syncer, 2022/05/17
- [elpa] externals/srht e17edd7643 04/27: Add README., ELPA Syncer, 2022/05/17
- [elpa] externals/srht 6037d9f03c 22/27: Update README., ELPA Syncer, 2022/05/17
- [elpa] externals/srht 3c4df2cff1 06/27: Add make rules., ELPA Syncer, 2022/05/17
- [elpa] externals/srht 7b3792ac75 12/27: Eldev: use the path from the store for plz., ELPA Syncer, 2022/05/17
- [elpa] externals/srht 81519fed59 15/27: Add git service bindings., ELPA Syncer, 2022/05/17
- [elpa] externals/srht a99a8e4bd1 19/27: srht-git--repo-name-read: Add doc string., ELPA Syncer, 2022/05/17
- [elpa] externals/srht 9dd0d15e4a 20/27: srht-git-repo-update: Fix., ELPA Syncer, 2022/05/17