Transcript
00:00:00Hola, muchas gracias por acompañarnos hoy.
00:00:02Soy Praneet, del equipo de Workflow aquí en Vercel.
00:00:05Hola, soy Nate, también del equipo de Workflow.
00:00:08Nate, tú y I hemos estado en el equipo de Workflow desde el principio,
00:00:12y de todas las cosas que hemos lanzado en los últimos seis meses,
00:00:15creo que los hooks y webhooks son una de mis funciones favoritas,
00:00:18y eso es exactamente de lo que vienes a hablar hoy.
00:00:21Los hooks y webhooks también son mi función favorita.
00:00:23Son increíblemente potentes, y mostraré algunas demos para explicar por qué.
00:00:28La primera demo es algo con lo que probablemente todos estemos familiarizados, los Magic Links.
00:00:33Magic Link es un formulario de inicio de sesión. Escribes tu correo, recibes un email,
00:00:40y cuando haces clic en ese enlace, inicias sesión en el servicio.
00:00:44Sí, y si mal no recuerdo, con Vercel, incluso antes de que se llamara Vercel,
00:00:48cuando todavía se llamaba ZEIT, los Magic Links eran la única forma de autenticarse,
00:00:52y básicamente nosotros mismos construimos todo ese sistema en aquel entonces.
00:00:56Así es, y todavía tengo trauma postraumático.
00:01:01Porque sin Workflow, implementar un sistema así es mucho más complicado de lo que parece a simple vista.
00:01:08La lógica termina dispersa en múltiples archivos.
00:01:12Necesitas involucrar una base de datos para rastrear el estado, y se vuelve un caos rápido.
00:01:19Sí, ya estaba pensando en cómo estructuraría esto y qué base de datos usaría,
00:01:24porque parece el tipo de problema común para el que ya he construido cosas antes.
00:01:28Así que sí, me encantaría ver cómo queda.
00:01:30Sí, para demostrar a qué me refiero, esos puntos críticos de los que hablo,
00:01:38comencé implementando una versión "tradicional" sin Workflow de un inicio de sesión con Magic Link.
00:01:43Y hay tres endpoints involucrados.
00:01:47El primero es cuando se envía el formulario de inicio de sesión,
00:01:50y necesita generar una sesión y guardarla en una base de datos, como Redis.
00:01:57Tienes que implementar un TTL; no puedes dejar datos ahí para siempre, deben expirar.
00:02:06Y luego enviar el correo; esto podría fallar, el inicio de sesión no funcionaría y sería frustrante.
00:02:14Exacto, y luego tienes que tener un cron job o un becario limpiando la base de datos.
00:02:19Yo podría haber sido ese becario en aquel momento.
00:02:22Pero luego hay un segundo endpoint, el que ocurre cuando el usuario hace clic en el enlace del correo.
00:02:28Y este básicamente necesita consultar la base de datos y restaurar el estado creado en el primer endpoint.
00:02:36Y ya estamos llegando a un código tipo espagueti.
00:02:38Al intentar imaginar cómo se vería esto, este código me resulta familiar y así es como yo lo estructuraría.
00:02:48Vemos que se complica rápido, aunque sea un concepto muy sencillo.
00:02:54Así que veamos cómo implementarías esta función en Workflow.
00:02:59La implementación del Magic Link usando el SDK de Workflow se ve algo así.
00:03:05Vemos que tenemos nuestra función con la directiva useWorkflow, lo que indica que es nuestra función de Workflow.
00:03:11Y lo primero que hacemos es llamar a la función createWebhook, que viene del paquete de Workflow.
00:03:18También usamos la opción respondWithManual, lo que significa que el Workflow se encargará
00:03:36de enviar la respuesta a la solicitud HTTP que activa este Webhook.
00:03:40¿Y esto es para poder hacer una redirección o algo después de que inicien sesión?
00:03:51Sí, por si hay información en nuestra función de Workflow que necesitemos para saber qué respuesta enviar.
00:03:57Al igual que en el primer endpoint, enviamos el email. Esta es una función useStep.
00:04:03Si algo así falla, el SDK de Workflow tiene reintentos automáticos.
00:04:10El aspecto de la durabilidad ya ofrece una ventaja sobre el enfoque tradicional.
00:04:21Así que sendLoginEmails es un paso, y si el envío falla, reintenta
00:04:26con la misma URL que ya creaste para el Webhook.
00:04:30Y si miramos aquí, hay un patrón muy interesante.
00:04:35Estamos usando promise.race con un sleep de cinco minutos.
00:04:40Esto es posible porque este objeto Webhook implementa una promesa.
00:04:50Para esperar la solicitud de este Webhook, solo haces await Webhook.
00:04:58O en este caso, lo haces con la carrera. Y es genial porque esperaba que
00:05:02esta función de Webhook tuviera un timeout o alguna opción como argumento.
00:05:06Pero me gusta que sea tan limpio; para hacer un timeout, simplemente haces una carrera entre el Webhook y un sleep.
00:05:12Siento que puedo hacer mucho más con esto. Podría poner a competir dos Webhooks distintos entre sí.
00:05:16No hay mucho que puedas hacer cuando tienes solo un par de argumentos en una función.
00:05:21Pero el hecho de que sea una promesa y pueda hacer un promise.race contra un sleep, o quizás otro paso...
00:05:23Me encanta este patrón. Mi mente ya está imaginando todo lo que podría construir con esto.
00:05:28Exacto, y esa es la belleza de las primitivas que ofrece el SDK de Workflow.
00:05:33Todo se expone como una promesa.
00:05:41Así que los patrones estándar de JavaScript como await promise.race simplemente funcionan.
00:05:44Y otra cosa a destacar aquí es que no hay Redis. No hay base de datos.
00:05:50En el ejemplo tradicional, usábamos el TTL de Redis para implementar este tiempo de espera.
00:05:51Y en este caso, estamos usando la primitiva sleep de Workflow.
00:05:59Y tampoco hay becarios que tengan que limpiar una base de datos desordenada después.
00:06:07Esa es la mejor parte.
00:06:12Y puedes ver que el Workflow responde a la solicitud pública redirigiendo a la página de éxito.
00:06:17Luego recupera información sobre tu usuario para devolverla al cliente que inició el login.
00:06:24Y ese es todo nuestro Workflow. Nuestra implementación de Magic Link son 50 líneas de código.
00:06:31Es increíble verlo. ¿Podemos verlo en acción?
00:06:41Aquí está la demo de Magic Link. Introduciré mi correo electrónico.
00:06:47Nuestro Workflow se inició ahí y envió el correo. Y hay un webhook simplemente esperando.
00:06:52De hecho, nuestro Workflow está suspendido ahora mismo. Se consume cero cómputo mientras se espera al humano.
00:06:57¿Y cómo se ve esto en Vercel? ¿Puedo ver una ejecución que esté pendiente?
00:07:02Bien, ya recibimos el correo. Antes de hacer clic, echemos un vistazo a la observabilidad.
00:07:08Sé que estoy saltando de un lado a otro, pero me encanta que estemos viendo esto.
00:07:13Vale, vemos que nuestra ejecución está aquí y comenzó hace 40 segundos.
00:07:17Si miramos, tenemos las funciones de observabilidad estándar que proporciona Workflow.
00:07:25Podemos ver las entradas de nuestra ejecución. Ves mi correo que escribí en el formulario.
00:07:39Y curiosamente, podemos ver que nuestro hook está simplemente esperando.
00:07:46Dijiste que no hay cómputo ejecutándose. Esta es la observabilidad, pero no hay nada esperando activamente.
00:07:50Exacto. El hook está esperando y el sleep está durmiendo, y ninguna de esas cosas implica cómputo real.
00:07:59Pero vemos nuestro hook y, si recuerdas, ambos están compitiendo en un promise.race.
00:08:01Así que uno de ellos debe terminar primero para que el Workflow continúe.
00:08:05Si hago clic en el enlace... vale, vemos que fui redirigido a la página de éxito,
00:08:08que era uno de los pasos en nuestra lógica de Workflow.
00:08:11Y si vuelvo al formulario de inicio de sesión...
00:08:17Y de vuelta en el dashboard, eso también debería estar completado.
00:08:27Correcto. Nuestro Workflow se completó.
00:08:34Y ves que el temporizador se detiene en cuanto el hook gana.
00:08:41Sí, pudimos implementar el magic link con unas 50 líneas de código.
00:08:59Es muy ingenioso. Es genial ver cómo, si tuviéramos que dibujar un diagrama de cómo funciona el magic link,
00:09:07los pasos que tienes en el código son exactamente como lo planearías, y así es el código final.
00:09:10No hubo una base de datos intermedia. No hubo múltiples rutas de API. Se lee con mucha claridad.
00:09:16Correcto. Así que la función "create webhook" es un poco más de alto nivel en ese sentido, ya que proporciona una URL de webhook única generada aleatoriamente.
00:09:23Que se vincula a una ejecución específica de Workflow.
00:09:31En el caso de nuestra ruta de webhook de GitHub o Slack, eso podría vincularse a cualquier número de ejecuciones de Workflow.
00:09:36porque me da una forma totalmente distinta de pensar en ellos.
00:09:49Es solo una URL efímera que puedo crear y sobre la cual puedo suspender la ejecución.
00:09:54De hecho, esto sirve de transición, porque nosotros construimos muchos agentes en Vercel.
00:10:04Hacemos agentes de Slack y de GitHub, y solemos suscribirnos a sus webhooks, ¿verdad?
00:10:07Cada vez que hay un comentario en un PR y queremos activar un agente de Vercel,
00:10:14queremos hacerlo basándonos en un evento de estos webhooks que envía GitHub.
00:10:20¿Podemos usar los webhooks de Workflow para suscribirnos a eventos de GitHub, por ejemplo?
00:10:28Bueno, para un webhook enviado desde Slack o GitHub, normalmente
00:10:31tienes que ir al dashboard manualmente y configurar una URL de callback estática.
00:10:32Cierto. No puedes crear esto como algo único; no puedo darle una URL temporal como hice con el correo.
00:10:35Exacto. La función createWebhook es de un nivel un poco más alto en ese sentido,
00:10:40ya que proporciona una URL de webhook única generada aleatoriamente.
00:10:47Que apunta a una ejecución específica de Workflow.
00:10:52En el caso de nuestra ruta de webhook de GitHub o Slack, eso podría apuntar a cualquier número de ejecuciones.
00:11:05Claro. Tienes que preconfigurar algo, pero tienes múltiples pull requests y todos van al mismo endpoint.
00:11:09Así que para implementar eso con el SDK, bajaremos un nivel y usaremos la primitiva hook de bajo nivel.
00:11:13Y tengo una demo para enseñártelo.
00:11:17Echémosle un vistazo.
00:11:20Vale. Este es el bot de cuentos (Storytime Bot).
00:11:26Fue la primera aplicación que escribí con el SDK de Workflow hace algo más de un año.
00:11:28Funciona así: escribes el comando /storytime y veremos cómo se crea este hilo.
00:11:35Cada hilo está representado por una ejecución individual de Workflow.
00:11:38Cuando expandimos el hilo, vemos que un LLM comenzó la historia por nosotros,
00:11:44y tú, yo o cualquiera en este canal puede continuarla.
00:11:50Y el LLM nos ayudará a llevarla hasta su conclusión final.
00:11:53Vale. Luna tiene una semilla mágica y ¿qué pasa después? Ella planta la semilla.
00:12:04Bien. Vemos algo de actividad ocurriendo aquí.
00:12:23¿Qué pasa después? Algo mágico.
00:12:27Nuestra historia ha terminado, tenemos el relato final y también se generará una pequeña imagen.
00:12:34Pero volveremos a eso luego.
00:12:40Ya tengo curiosidad, porque esperaba una solicitud de webhook pero hubo al menos dos,
00:12:55porque tenías dos mensajes. Tengo muchas ganas de ver cómo se ve esto en el código.
00:13:04Bien. Esta es la función de Workflow para nuestro bot de cuentos.
00:13:10Vemos que recibe el ID del canal, que es el canal del bot.
00:13:14Tiene algunas opciones de configuración que puedes pasar.
00:13:28Pero lo interesante es este array de mensajes que, si conoces el AI SDK, es el formato de datos de la IA.
00:13:35En una aplicación típica de bot de Slack como esta, normalmente guardarías esto
00:13:44en una base de datos y, en cada iteración o evento de webhook, restaurarías el estado.
00:13:58Y eso no es lo que pasa aquí. Es solo un array dentro de tu función.
00:14:04Sí. Me reí antes porque vi la intro y tenías ese comentario que decía: "Mira mamá, sin colas ni KV".
00:14:17Y no hay ningún import para una base de datos. Solo estás importando Workflow.
00:14:29Y volviendo a lo del último mensaje, casi se pasa por alto, pero tienes una variable
00:14:37llamada final story a la que presumiblemente iremos añadiendo mensajes,
00:14:50y la historia final aparecerá como un string, pero no tiene que ir a ninguna base de datos.
00:14:59Es casi como si "let" fuera tu base de datos aquí.
00:15:07Sí, "let es tu base de datos" es un gran término; vamos a acuñar eso.
00:15:13Puede que te lo haya robado a ti, pero...
00:15:20Lo interesante aquí, y de lo que venimos a hablar, es la función hook que estamos creando.
00:15:28A diferencia del ejemplo del magic link, en este caso estamos proporcionando un token,
00:15:35que es un string con información identificativa única para esta ejecución de Workflow.
00:15:44El TS es el ID del hilo. Así que este string es el token que identifica unívocamente la ejecución.
00:15:50Cuando miremos el código de la ruta del webhook, veremos que los datos que envía Slack
00:15:55contienen todo lo necesario para recrear este identificador de forma determinista.
00:16:06Pero siempre han sido buenos para demostraciones y no había podido encontrar una buena forma de usarlos.
00:16:11Aquí me parece que simplemente tienes un bucle.
00:16:17Pero en lugar de iterar sobre un conjunto fijo de elementos o una marca de tiempo, al usar for await y el hook, el bucle encaja exactamente.
00:16:25Entiendo que tienes una API ya conectada a Slack, pero cada vez que recibes un mensaje,
00:16:33básicamente calculas el mismo token en el lado de la reanudación.
00:16:42Así tu Workflow puede esperar este token y tú puedes construir el mismo token
00:16:46desde los datos del mensaje para reanudar la ejecución.
00:16:50Exactamente. El bot de Slack se configuró una vez manualmente en su dashboard,
00:17:01donde tienes que definir estáticamente una URL de callback.
00:17:05Por eso la primitiva hook de nivel inferior funciona mejor aquí, porque recreamos el token dinámicamente.
00:17:12Echemos un vistazo rápido: esta es la ruta del webhook, y en realidad no pasa gran cosa.
00:17:22Lo principal es cómo recreamos el token a partir de los datos que nos pasa Slack.
00:17:33Y luego llamamos a la función resume, y esto reanuda la ejecución única de ese Workflow.
00:17:42Me parece genial. Imagino que con los webhooks normales estáis haciendo algo parecido.
00:17:43¿Es el webhook básicamente un token aleatorio y un endpoint HTTP que resuelve ese mismo token?
00:17:48Sí, bueno, la diferencia con la función webhook es que no necesitas definir esa ruta de API en tu código.
00:17:57El SDK de Workflow ya implementa una ruta por defecto para ti.
00:18:03Pero sí, aparte de eso, es un token aleatorio único para una ejecución específica.
00:18:08Pero en este caso, tenemos nuestro hook con el token y, como mencionaste antes,
00:18:12este hook puede recibir datos varias veces.
00:18:16Esto es distinto al ejemplo del Magic Link, que solo necesitaba activarse una vez.
00:18:23Aquí queremos que el hook se active por cada mensaje único que alguien escriba en el hilo de Slack.
00:18:32Para eso, usamos la sintaxis for await de JavaScript, común con los iteradores asíncronos.
00:18:39Pero en este caso, recibimos múltiples datos del webhook de Slack usando nuestro hook.
00:18:49Qué bueno. Nunca había encontrado un buen caso de uso... me encantan los iteradores asíncronos,
00:18:52incluso di una charla sobre esto hace mucho, pero siempre parecían mejores para demos.
00:18:56Aquí se lee simplemente como un bucle.
00:18:58Pero en lugar de iterar sobre una lista fija o un tiempo, usas for await con el hook.
00:19:05Aquí tienes un bucle que encaja perfectamente.
00:19:10Todo lo que hay dentro del bucle corresponde a un mensaje del usuario.
00:19:14Es una forma agradable de pensarlo: cada mensaje causa una iteración y simplemente se encola.
00:19:16Lo mejor es que, en cada iteración del bucle, mientras esperamos el siguiente mensaje,
00:19:25no se consume absolutamente nada de cómputo.
00:19:34El workflow está suspendido y el siguiente mensaje podría llegar en minutos, días o nunca.
00:19:40Probablemente haya hilos en ese canal de Slack donde podría volver ahora mismo
00:19:42y habría una ejecución esperando desde hace semanas si nadie respondió. Es genial.
00:19:43Y volviendo al array de mensajes de antes, ahora estamos modificando el array.
00:19:54Añadimos el nuevo mensaje y esa es nuestra modificación de "base de datos" en la variable local.
00:19:55Qué guay. Y veo que haces más promise.all para paralelizar pasos intermedios.
00:20:02Se lee muy limpio para cada mensaje de Slack.
00:20:05Me gusta porque es exactamente como lo modelarías si estuvieras en una hackathon.
00:20:06Es como: "aquí escribo lo que ocurre con cada mensaje".
00:20:17Sí, en el modelo promise.all, estas son solo funciones useStep normales ejecutándose en paralelo.
00:20:32Añadir una reacción al mensaje de Slack es para dar feedback inmediato al usuario.
00:20:41Pero al mismo tiempo, queremos iniciar el LLM para que avance la generación de la historia.
00:20:51Me interesaría mucho ver cómo queda la observabilidad cuando podamos,
00:20:56porque imagino esos tramos empezando a la vez y haciéndolo muy obvio.
00:21:00Ya tenemos la observabilidad de Storytime. Ya terminó, así que luego veremos la imagen.
00:21:09Así que, de hecho, esta operación es un Paso.
00:21:12Por lo que puedes distribuir un paquete de NPM.
00:21:15Sandbox básicamente distribuye un paquete NPM que tiene la directiva "use Step" dentro de esta función.
00:21:21Así, cuando lo importas y usas en un flujo de trabajo, Sandbox se convierte en un Paso sin que escribas nada de ese código.
00:21:29Pero eso no quita que puedas seguir usando Sandbox para crear fuera de un flujo de trabajo.
00:21:32¿Qué sucede cuando llamas a esto sin un flujo de trabajo?
00:21:35Si te das cuenta, la directiva es solo una cadena de texto, y si la ejecutas sin el compilador de flujos de trabajo,
00:21:47esa cadena no hace nada. Simplemente funciona.
00:21:49Añadir "use Step" en tus paquetes NPM funciona perfectamente sin el SDK de flujos de trabajo.
00:21:55Y una vez que usas esa función dentro del SDK de flujos de trabajo, obtienes los beneficios de durabilidad de inmediato.
00:22:03Bien, el Sandbox solo realiza algunas tareas típicas.
00:22:07Instala FFmpeg porque no está disponible por defecto.
00:22:11Descarga la URL de un archivo que vamos a especificar.
00:22:14¿Y cada una de estas ejecuciones también son Pasos ahora mismo?
00:22:17Sí, ejecutan un comando individual en el Sandbox y son Pasos. Podremos verlos en la observabilidad.
00:22:29Bien, entiendo. Qué bien. Ya veo que, me estaba adelantando un poco, pero noté que hay un AND en esta ejecución.
00:22:36Pero en este caso, pasaremos esa URL del webhook a nuestro script de Bash que ejecutaremos en el Sandbox.
00:22:43Lo que sucede es que ejecutaremos FFmpeg y convertiremos el archivo al formato que solicitemos en la interfaz.
00:22:53Y cuando termine, el script de Bash ejecutará un cURL contra nuestra URL de retorno del webhook.
00:22:59Y cuando ocurra esa solicitud de cURL, nuestra lógica del flujo de trabajo se reanudará.
00:23:04Entiendo. Eso es genial. Me estaba adelantando un poco, pero noté que hay un "AND" en esta ejecución.
00:23:11Estás escribiendo el script ejecutando esto en segundo plano porque un paso de FFmpeg como este podría tardar mucho.
00:23:17No quieres un Paso que se quede ahí sentado esperando.
00:23:20Exacto. Esta línea de aquí inicia nuestro script de conversión de FFmpeg en segundo plano.
00:23:28Luego, nuestra función de flujo de trabajo se suspende y esperamos a que el webhook se reanude.
00:23:34Veo el "promise race" de nuevo con una espera de una hora. Es un patrón muy interesante.
00:23:40Cierto, y esta vez, el proceso de conversión de FFmpeg podría tardar bastante.
00:23:46Podría ser un archivo multimedia muy grande. Así que especificamos un tiempo de espera de una hora en este caso.
00:23:51Y eso está bien. En un flujo de trabajo, puedes suspenderlo por un tiempo esencialmente indefinido.
00:23:56Y de nuevo, hay cero consumo de cómputo mientras esperamos a que se reanude este webhook.
00:24:01¿Podemos ver esto? ¿Podemos ver esta ejecución? ¿Tenemos una demostración?
00:24:04La tenemos.
00:24:05Es un ejemplo un poco tonto.
00:24:07Sí, reconocí el ejemplo del conejo grande inmediatamente. Es de Blender.
00:24:12Sí, recuerdo ver estos videos cuando aprendía Blender hace mucho tiempo.
00:24:16Oh, vaya, tengo envidia.
00:24:19Ya pegamos la URL de nuestro archivo multimedia. En este caso, simplemente extraeremos la capa de audio.
00:24:26Una vez que hacemos clic en el botón, se inicia un flujo de trabajo y deberíamos poder ir a nuestra observabilidad.
00:24:33Ahí está. Sí, podemos ver la creación de nuestro sandbox.
00:24:37Y eso nos devuelve nuestra instancia de sandbox. Muy genial.
00:24:42Y esto es porque los sandboxes, todo en un flujo de trabajo, tiene que ser serializable.
00:24:46Pero como dijiste, los sandboxes implementan serialización, así que son serializables y aparecen en el flujo de trabajo.
00:24:53Correcto. Sí. El paquete de Vercel sandbox tiene una clase sandbox y esa clase implementa las funciones de serialización del flujo.
00:25:03Así que simplemente funciona en nuestra observabilidad.
00:25:06Y cualquier paquete puede hacer esto, ¿verdad? No es solo el sandbox. Podría implementar lo mismo y tener directivas de pasos.
00:25:17Así es. Podemos ver que nuestro enlace terminó recibiendo la llamada en 20 segundos esta vez.
00:25:25Una conversión un poco más rápida porque es un archivo pequeño, pero podría haber sido cualquier cantidad de tiempo.
00:25:31Vemos que tras crear e inicializar el sandbox, se creó nuestro hook y lo pasamos al sandbox para iniciar el comando FFmpeg.
00:25:43Y cuando eso terminó, recibimos una carga útil de nuestro sandbox.
00:25:48Este es el curl que ocurrió antes en el script de bash. Escribe el comando y usa curl en un sandbox para completar el webhook.
00:25:57Exacto. Nuestro sandbox terminó el trabajo que estaba haciendo, así que devuelve el control a nuestro flujo de trabajo.
00:26:04Como lo veo ahora, con los pasos en un flujo de trabajo, ejecutas un paso, corre un código en segundo plano y luego continúa.
00:26:13Pero tanto el hook como el webhook parecen ser de un nivel más bajo. Puedo crear un token o URL y esperar lo que sea.
00:26:21Podría ser un enlace mágico humano, un correo, un sandbox o cualquier tipo de proceso que deba ocurrir.
00:26:27Y mi flujo de trabajo simplemente se pausa con todo su estado hasta que ocurra ese evento. Parece más básico que el Paso en sí.
00:26:34Sí. Yo lo veo así: webhook y hook son una forma de pasar cargas útiles externas a tu flujo de trabajo.
00:26:42Pienso que un paso es una forma en la que un flujo puede suspenderse y esperar a que termine un proceso para reanudarse.
00:26:50Pero hook y webhook parecen de nivel más bajo porque solo generas un token o URL que podrías enviar a cualquier parte.
00:27:01Podría ser una persona, un correo electrónico o incluso otro flujo de trabajo, por ejemplo.
00:27:05Y cuando eso se completa, tu flujo de trabajo principal básicamente se despierta y reanuda justo donde lo dejó.
00:27:12Así que es de un nivel más bajo que un Paso. Es una forma de suspender tu flujo para cualquier acción externa.
00:27:19Sí. Me gusta pensar que el hook es una forma de suspender tu flujo y esperar una carga útil externa, lo cual es muy potente.
00:27:31Esto es genial. Ya se nos acabó el tiempo hoy, pero con estas demostraciones me has validado por qué el hook es mi función favorita.
00:27:42Fantástico. Me alegra que lo hayas disfrutado.
Community Posts
No posts yet. Be the first to write about this video!
Write about this video