Nel tentativo di estendere un mio pacchetto per Emacs, mi sono ritrovato ad aver bisogno di compiere in Emacs Lisp una chiamata GET con autenticazione. Mi sarei aspettato una maggiore copertura dell'argomento online e più librerie, più esempi. E invece è stato necessario scavare un po'.

Innanzitutto, bisogna dire che, a differenza di URL, non esiste un pacchetto standard per questo genere di operazioni. Cercando un po' in giro per la rete, emergono fondamentalmente tre pacchetti che potrebbero tornare utili per delle GET. Uno di questi è request.el, che qualcuno ha persino definito "il coltellino svizzero" in Emacs per questo genere di operazioni. (Qui il vecchio manuale (legacy)).

Avendo familiarità con la libreria request, in Python, avrei anche potuto impiegare delle bindings o un wrapper per effettuare la richiesta attraverso Python, ma un lavoro del genere può andare bene al massimo per uso personale, poiché la distribuzione si complica notevolmente. Se ognuno decidesse di estendere Emacs con il proprio linguaggio preferito, ci si ritroverebbe costretti ad avere una miriade di dipendenze (da librerie Ocaml a Common Lisp) solo al fine di far girare Emacs. Non sembra il caso.

Certo qualche dipendenza che potremmo accettare per una situazione di questo tipo esiste, ed è CURL, che infatti avevo pensato di richiamare come subprocess. Poi ho scoperto che non ce ne sarebbe bisogno, perché il secondo ed il terzo dei pacchetti che citavo si occupano proprio di fare da wrapper per CURL:

Stavo per fare un tentativo con emacs-curl, ma poi ho notato che è quasi privo di documentazione ed il progetto ha ricevuto l'ultimo aggiornamento 10 anni fa. Non proprio l'altro ieri. E nemmeno grapnel scherza da questo punto di vista, visto che l'ultimo aggiornamento al README risale a 6 anni fa, mentre il codice ha ricevuto l'ultima modifica 9 anni fa. In compenso, in questo caso abbiamo almeno un esempio d'utilizzo nel README, anche se privo di commenti.

Guardando al panorama restante, si capisce come mai Request.el sia il pacchetto favorita della community: non solo si tratta dell'unica libreria aggiornata relativamente di recente, ma supporta sia url.el che CURL, consentendo all'utente di fare a meno della dipenza esterna in casi eccezionali, ma di sfruttare il più delle volte CURL per una maggiore stabilità:

Request.el is a HTTP request library with multiple backends. It supports url.el which is shipped with Emacs and curl command line program. User can use curl when s/he has it, as curl is more reliable than url.el. Library author can use request.el to avoid imposing external dependencies such as curl to users while giving richer experience for users who have curl.

Installazione§

Ho provato ad installare questa su Doom Emacs, ma ho notato che si tratta di un pacchetto preinstallato. Questo evidenzia come request.el sia lo standard de facto su Emacs, in maniera molto simile alla libreria request di Python che citavo inizialmente.

Utilizzo§

Input:

(require 'request)

(request "http://httpbin.org/get"
  :params '(("key" . "value") ("key2" . "value2"))
  :parser 'json-read
  :success (cl-function
            (lambda (&key data &allow-other-keys)
              (message "I sent: %S" (assoc-default 'args data)))))

In questo esempio, abbiamo definito:

  • :params - I parametri da inviare in forma di una Association List.
  • :parser - Un parser che si adatti al tipo di risposta che ci aspettiamo (solitamente json, ad oggi)
  • :success - Ci lascia persino definire una funzione che sia evocata in caso di successo.

In questo caso, ad esempio, la funzione è una lambda che accetta come argomento i dati ricevuti. Le lambda temo che non siano supportate in Emacs Lisp nativamente, quindi si fa ricorso ad una libreria che riprende le lambada in Common Lisp.

Dalla documentazione di assoc-default:

Find object KEY in a pseudo-alist ALIST.

Ci serve per ricapitolare i dati inviati.

Output:

(request-response nil nil nil nil nil "http://httpbin.org/get?key=value&key2=value2" nil (:params (("key" . "value") ("key2" . "value2")) :parser json-read :success (closure (t) (&rest --cl-rest--) "
fn LEVEL)"] error format "[%s] %s" "request-default-error-callback: %s %s" get-buffer-create t "
" message "%s"] 14 "

(fn &rest ARGS &key SYMBOL-STATUS &allow-other-keys)"] :url "http://httpbin.org/get?key=value&key2=value2" :response #0 :encoding utf-8) #<buffer  *request curl*> nil nil curl)

I sent: ((key . "value") (key2 . "value2"))

Il blocco request-response contiene i dati richiesti (tra i quali figura un intero buffer, che probabilmente va intercettato diversamente) e, a seguire, possiamo leggere un sunto dei dati inviati preceduti da "I sent:".

Una funzione personalizzata§

Prima di mettere in piedi una nuova funzione, diamo un'occhiata alla totalità delle API disponibili.

Dalla documentazione (legacy):

Keyword argumentExplanation
TYPE (string)type of request to make: POST/GET/PUT/DELETE
PARAMS (alist)set ”?key=val” part in URL
DATA (string/alist)data to be sent to the server
FILES (alist)files to be sent to the server (see below)
PARSER (symbol)a function that reads current buffer and return data
HEADERS (alist)additional headers to send with the request
SUCCESS (function)called on success
ERROR (function)called on error
COMPLETE (function)called on both success and error
TIMEOUT (number)timeout in second
STATUS-CODE (alist)map status code (int) to callback
SYNC (bool)If t, wait until request is done. Default is nil.

A titolo di esempio, potremmo provare a replicare in lisp uno degli degli esempi publicati nella repository delle più recenti Twitter API. Faccio riferimento al codice in Python, con cui ho maggiore familiarità.

Variabili d'ambiente§

Poiché non è saggio inserire il nostro Token segreto direttamente nel corpo del codice, potremmo dichiararlo come variabile d'ambiente

export 'BEARER_TOKEN'='<your_bearer_token>'

e poi richiamarlo da lì. Python sfrutta la libreria os, che è parte della libreria standard. Anche emacs può leggere le variabili d'ambiente, basta impiegare la funzione getenv:

(getenv "my-var")

Nel nostro caso, dichiariamo una variabile avente come argomento il nostro token:

(defvar bearer-token (getenv "BEARER_TOKEN"))

Costruzione dell'URL§

Dichiariamo delle variabili contenenti:

  • I campi richiesti (tweet-fields);
  • Gli ID dei tweet di cui vogliamo sapere qualcosa (ids);
(setq tweet_fields "tweet.fields=lang,author_id")
;; Tweet fields are adjustable.
;; Options include:
;; attachments, author_id, context_annotations,
;; conversation_id, created_at, entities, geo, id,
;; in_reply_to_user_id, lang, non_public_metrics, organic_metrics,
;; possibly_sensitive, promoted_metrics, public_metrics, referenced_tweets,
;; source, text, and withheld

(setq ids "ids=1278747501642657792,1255542774432063488")
;; You can adjust ids to include a single Tweet.
;; Or you can add to up to 100 comma-separated IDs

Componiamo l'URL:

(setq url (concat "https://api.twitter.com/2/tweets?" ids "&" tweet_fields))

Autenticazione§

Non sembra esserci nulla di specifico per l'autenticazione mediante token. Poco male, ci basta ragionare in termini di costruzione del comando CURL e poi tradurre in una funzione equivalente. In CURL specifichiamo il nostro token tra gli headers, cioè con il flag -H: qui specificheremo gli headers come previsto da Emacs Request, fornendo una alist alla variabile :headers.

(setq hdrs '(("Authorization" . (concat "Bearer " bearer_token))))

Costruzione della funzione§

(request url
  :type "GET"
  :headers '(("Authorization" . (concat "Bearer " bearer_token))) ; <== ERROR: this variable can't be evaluated in a quoted alist
  :parser 'json-read
  :error
  (cl-function (lambda (&rest args &key error-thrown &allow-other-keys)
                 (message "Got error: %S" error-thrown)))
  :success (cl-function
            (lambda (&key data &allow-other-keys)
              (message "Got: %s" data))))

Dobbiamo riformulare la riga degli headers, perché in questo modo bearer_token non verrebbe valutato.

Innanzitutto dichiariamo una lista vuota che contenga gli headers:

(setq hdrs '())
;; => nil

In secondo luogo, sfruttiamo add-to-list per aggiungere dei valori. In questo caso, basta aggiungere come valore una lista soltanto, corrispondente al nostro header per "Authorization":

(add-to-list 'hdrs (cons "Authorization" (concat "Bearer " bearer_token)))
;; => (("Authorization" . (concat "Bearer " <your_bearer_token>))

Adesso possiamo ricostituire la funzione precedente, ma con la variabile corretta:

(request url
  :type "GET"
  :headers hdrs
  :parser 'json-read
  :error
  (cl-function (lambda (&rest args &key error-thrown &allow-other-keys)
                 (message "Got error: %S" error-thrown)))
  :success (cl-function
            (lambda (&key data &allow-other-keys)
              (message "Got: %s" data))))

Ora funziona.

Il codice integrale:

(require 'request)
;; https://github.com/tkf/emacs-request

;; Retrieve your Environment Variable with the bearer token
;; Alternately, you could just put the token here as the other vars
;; (setq bearer-token ...)
(defvar bearer-token (getenv "BEARER_TOKEN"))

;; Tweet fields are adjustable.
;; Options include:
;; attachments, author_id, context_annotations,
;; conversation_id, created_at, entities, geo, id,
;; in_reply_to_user_id, lang, non_public_metrics, organic_metrics,
;; possibly_sensitive, promoted_metrics, public_metrics, referenced_tweets,
;; source, text, and withheld
(setq tweet_fields "tweet.fields=lang,author_id")

;; You can adjust ids to include a single Tweet.
;; Or you can add to up to 100 comma-separated IDs
(setq ids "ids=1278747501642657792,1255542774432063488")

;; Let's build up the URL
(setq url (concat "https://api.twitter.com/2/tweets?" ids "&" tweet_fields))

;; Headers for the request
(setq hdrs '())
;; We need just this field for authorization
(setq auth_header (concat "Bearer " bearer_token))
(add-to-list 'hdrs (cons "Authorization" (concat "Bearer " bearer_token)))

;; Core function with the actual GET request
(request url
  :type "GET"
  :headers hdrs
  :parser 'json-read
  :error
  (cl-function (lambda (&rest args &key error-thrown &allow-other-keys)
                 (message "Got error: %S" error-thrown)))
  :success (cl-function
            (lambda (&key data &allow-other-keys)
              (message "Got: %s" data))))