Solicitudes encadenadas Request-Reply con OAuth
¿Cómo uso la salida de una llamada síncrona como la entrada de otra llamada síncrona?
Este ejemplo de connector demuestra las siguientes funcionalidades de los connectors.
-
Message logging
-
Encadenamiento de llamadas de request-reply connector (la salida de una llamada como entrada de la siguiente)
-
Decoración de solicitudes para seguridad
-
Carga de medios
Esta guía también está disponible en el repositorio Git separado connector-samples
aquí.
|
Configuración inicial
Este ejemplo se escribió originalmente cuando era mucho más sencillo comenzar a desarrollar aplicaciones para la API de Twitter. Desafortunadamente, hay algunos pasos que debemos seguir para preparar todo antes de ejecutar el ejemplo, ¡pero con suerte valdrá la pena!
Cuenta de Twitter
Para ejecutar este ejemplo, necesitará una cuenta de Twitter. Si ya tiene una, pase al siguiente paso.
Puede registrarse en Twitter usando este enlace:https://twitter.com/signup?[link].
Cuenta de desarrollador de Twitter
También necesita una cuenta de desarrollador, ya que crearemos una aplicación de Twitter para llamar a APIs en nombre de su usuario.
Puede registrarse para una cuenta de desarrollador en el portal de desarrolladores.
App de Twitter
Una vez que tenga una cuenta de desarrollador, podrá crear una nueva App desde esta página.
Acceso elevado
Para usar la funcionalidad de carga, nuestra app debe tener acceso API elevado. Para obtenerlo debemos solicitarlo; es solo un breve cuestionario para asegurarse de que no tenemos planes maliciosos con todos sus datos.
El acceso elevado puede solicitarse en esta página.
Claves de API
En este punto, nuestra app de Twitter debería estar lista. Solo necesitamos generar algunas credenciales que se usarán para autenticar solicitudes a las APIs de Twitter en nuestro nombre. La página de la app debería tener una pestaña "Keys and tokens" donde se pueden generar todas las credenciales.
Las primeras credenciales son las Consumer Keys.
Es posible que las haya guardado cuando creó la app, pero si no, puede regenerarlas.
El segundo conjunto de credenciales es un Access Token y Access Token Secret, que deben generarse.
Una vez generadas todas las credenciales, se deben establecer las siguientes variables de entorno con ellas.
.setOAuthConsumerKey(System.getenv("TWITTER_CONSUMER_KEY")) // API KEY
.setOAuthConsumerSecret(System.getenv("TWITTER_CONSUMER_SECRET")) // API KEY SECRET
.setOAuthAccessToken(System.getenv("TWITTER_ACCESS_TOKEN"))
.setOAuthAccessTokenSecret(System.getenv("TWITTER_ACCESS_TOKEN_SECRET"))
Tipos de dominio
Nuestro connector de Twitter soporta dos operaciones y, como resultado, tenemos dos tipos de dominio que ambos extienden el tipo abstracto
TwitterRequest.
También podríamos crear dos separados para cada una de estas solicitudes si quisiéramos.
-
UploadMediaRequest: Cargar un medio para compartir en un tweet más tarde -
StatusUpdateRequest: Actualizar un estado de Twitter
Y también tenemos sus respectivas respuestas, bajo el tipo abstracto TwitterResponse.
-
UploadMediaResponse: Respuesta a la carga de medios (que contiene el ID del medio) -
StatusUpdateResponse: Respuesta al tweet (que contiene el tweet recién creado)
Configuración del connector
Primero creamos un HTTP request-reply connector transport así:
return new HttpConnectorTransport.Builder<TwitterRequest>()
.withName("TwitterHttp")
.withActorSystem(actorSystem)
.withConfigRootPath("chained-request-reply-example")
.withEnricher(httpRequest -> { (1)
HttpHeader authorization = HttpHeader.parse("Authorization", getAuthorizationHeader(httpRequest));
HttpRequest request = httpRequest.addHeader(authorization);
return CompletableFuture.completedFuture(request);
})
.build();
| 1 | Observe el enricher aquí, que añade la cabecera Authorization que contiene el importante contexto de usuario de OAuth 1.0 con el que Twitter autenticará.
Las entradas de este método son el método HTTP (siempre POST) y la URL (difiere si es una carga de medios o una actualización de estado) |
Luego creamos el siguiente RequestReplySendConnector.
var connector = new RequestReplySendConnector
.Builder<TwitterRequest, TwitterRequest, TwitterResponse, TwitterResponse>("Twitter")
.withConnectorTransport(connectorTransport)
.withActorSystem(actorSystem)
.withMessageLogger(logger()) (1)
.withSendTransportMessageConverter(request -> { (2)
var messageHeaders = new MessageHeaders(Map.of(
"httpUrl", getUrl(request),
"httpMethod", "POST"
));
if (request instanceof UploadMediaRequest) {
byte[] bytes = ((UploadMediaRequest) request).getData();
var entity = HttpEntities.create(ContentTypes.APPLICATION_OCTET_STREAM, bytes);
var payload = createStrictFormDataFromParts(createFormDataBodyPartStrict("media", entity)).toEntity();
return new TransportMessage(messageHeaders, payload);
}
return new TransportMessage(messageHeaders, "");
})
.withReceiveTransportMessageConverter(transportMessage -> { (3)
if (transportMessage.getPayload().toString().contains("media_id_string")) {
return fromJson(UploadMediaResponse.class).convert(transportMessage);
} else {
return fromJson(StatusUpdateResponse.class).convert(transportMessage);
}
})
.withManualStart(false)
.build();
| 1 | Aquí definimos una interfaz MessageLogger. Tomamos mensajes enviados y recibidos y los registramos. Esto puede reemplazarse con una implementación de base de datos donde se almacenen todas las interacciones de mensajes relacionadas con esta asociación de mensajes. |
| 2 | Aquí definimos una función que toma el TwitterRequest que queremos enviar y crea un
TransportMessage a partir de él, que es la representación de la solicitud sobre el cable.
En este caso necesitamos definir una URL diferente según si es una actualización de estado o una carga de medios. |
| 3 | Dado que este es un connector de request-reply, también debemos definir la operación inversa, que convierte la respuesta recibida nuevamente a un POJO de modelo que entendemos. Usamos Jackson para determinar si se trata de una respuesta de carga de medios o de actualización de estado, y mapearla en consecuencia al POJO correcto. |
Cadena de llamadas
La documentación de Twitter para publicar medios (imágenes, GIFs, videos, etc.) indica que debemos cargar el medio primero usando uno de los métodos de carga (simple o segmentado), lo cual devolverá un media_id que expira en 24 horas salvo que se use en un tweet antes de ese momento.
Luego usamos ese media_id en nuestra llamada de actualización de estado subsiguiente, que adjuntará el medio previamente cargado al tweet.
connector
.send(ProcessingContext.unknown(), new UploadMediaRequest(kittenPicBytes())) (1)
.thenApply(twitterResponse -> (UploadMediaResponse) twitterResponse) (2)
.thenCompose(uploadMediaResponse -> {
String status = String.format("I am a status update at %s! Also check out this cat photo", LocalDateTime.now());
var statusUpdateRequest = new StatusUpdateRequest(status, List.of(uploadMediaResponse.getMediaId()));
return connector.send(ProcessingContext.unknown(), statusUpdateRequest); (3)
})
.toCompletableFuture()
.join();
| 1 | Llamar al connector por primera vez para subir la foto del gatito |
| 2 | Convertir la respuesta a un UploadMediaResponse |
| 3 | Llamar al connector por segunda vez, con el mediaId de UploadMediaResponse para publicar un tweet que contenga el medio como adjunto |
Ejercicios
Ejercicio 1: Recuperar una imagen remota
Por el momento, el ejemplo carga kitten.jpg desde src/test/resources.
Intente, en su lugar, añadir una llamada a la cadena a, por ejemplo, la API Astronomy Picture of the Day (APOD) de la NASA para recuperar dinámicamente una imagen.
La API de APOD devuelve una estructura como esta.
{
"copyright": "Giancarlo Tinè",
"date": "2021-11-15",
"explanation": "What happening above that volcano? Something very unusual -- a volcanic light pillar. More typically, light pillars are caused by sunlight and so appear as a bright column that extends upward above a rising or setting Sun. Alternatively, other light pillars -- some quite colorful -- have been recorded above street and house lights. This light pillar, though, was illuminated by the red light emitted by the glowing magma of an erupting volcano. The volcano is Italy's Mount Etna, and the featured image was captured with a single shot a few hours after sunset in mid-June. Freezing temperatures above the volcano's ash cloud created ice-crystals either in cirrus clouds high above the volcano -- or in condensed water vapor expelled by Mount Etna. These ice crystals -- mostly flat toward the ground but fluttering -- then reflected away light from the volcano's caldera. Explore Your Universe: Random APOD Generator",
"hdurl": "https://apod.nasa.gov/apod/image/2111/EtnaLightPillar_Tine_5100.jpg",
"media_type": "image",
"service_version": "v1",
"title": "Light Pillar over Volcanic Etna",
"url": "https://apod.nasa.gov/apod/image/2111/EtnaLightPillar_Tine_960.jpg"
}
Intente extender la cadena actual añadiendo dos llamadas más al principio.
-
Una nueva llamada
RequestReplySendConnectorpara recuperar el enlace a la imagen del día -
Una nueva llamada
RequestReplySendConnectorpara solicitar la imagen ubicada enurlde la respuesta (deberá convertirla en un arreglo de bytes)
Luego deberá cambiar el contenido de MediaUpload para referenciar el arreglo de bytes de la imagen de APOD en lugar de la imagen local del gatito.
Ejercicio 2: Usar la API de carga segmentada
El ejemplo actualmente usa la API "simple" para subir imágenes a Twitter, lo cual es un método frágil para subir medios grandes ya que no es resiliente a cortes de red y no puede reanudarse. Twitter ofrece un método más resiliente para subir medios llamado API de carga segmentada (chunked) que soporta pausar y reanudar cargas.
La documentación para eso se puede encontrar aquí.
Intente cambiar la implementación existente para usar la API de carga segmentada, y quizás suba algún medio más grande, como un video o GIF. Los pasos serían.
-
Llamada a
INITpara declarar el inicio de una carga de medios -
Múltiples llamadas a
APPENDfragmentos del medio como partes, con un índice incremental -
Llamada a
FINALIZEde la carga de medios y recibir elmedia_id -
Llamada para actualizar nuestro estado usando el
media_iddel paso 3