Enero de 2018

Volumen 33, número 1

Office: compilación de la API para su organización con Microsoft Graph y Azure Functions

Por Mike Ammerlaan | Enero de 2018

Si piensa en su organización como una API, ¿cuál sería su aspecto? 

Probablemente, comenzaría con las personas, el núcleo de una organización, y con los tipos de roles y funciones que desempeñan. Estas personas suelen agruparse en equipos definidos y virtuales que realizan tareas y proyectos. Definiría las capas en los recursos, incluido dónde trabajan las personas y las herramientas que usan para realizar su trabajo. Luego, agregaría procesos y actividades de trabajo. ¿Es posible que estos sean métodos en la API? Aunque quizás "widgetMarketingTeam.runCampaign()" es extremadamente simple, con una API para su organización, obtendría excelente información sobre cómo funciona su organización y podría transformar la productividad con la compilación de procesos y herramientas más eficientes.

La clave es hacer que cada recurso esté disponible de manera coherente e interconectado de forma lógica, de modo que pueda confeccionar procesos completos para ajustarlos a la manera en que los individuos y equipos quieren trabajar. Cuantas más API pueda unir y conectar, más útil podrá ser con creces el conjunto de productos total que compile, más que la suma de sus componentes.

Con este fin, ofrecemos Microsoft Graph, una API que abarca conjuntos de datos clave en su organización y permite reunirlo todo para transformar la manera de realizar el trabajo. Además, Microsoft Graph funciona con los servicios al consumidor, como OneDrive y Correo (Outlook.com), para permitirle transformar también su productividad personal.

Solución del problema de la expansión de la API

En una organización, el conjunto de sistemas de software en uso puede variar enormemente. Para los desarrolladores, cada uno presenta una estructura única, generalmente con un conjunto de API, con requisitos de autenticación y con un estilo de interacción distintos. Con frecuencia, un reto importante de los proyectos de software es simplemente tender un puente entre estos sistemas diferentes para proporcionar un mayor nivel de información, y puede incluir la abstracción de las API diferentes y el dominio de esquemas de autenticación individuales.

Históricamente, las API individuales de distintos equipos de productos (en mi caso, en Microsoft) funcionan de manera diferente y requieren la integración entre productos. Incluso hace cinco años, el proceso de obtener el perfil completo y la foto de un usuario requería llamadas tanto a las API de Exchange (para obtener información sobre una persona) como a las API de SharePoint (para obtener una foto del perfil administrado de un usuario). Cada uno tenía su propia autenticación, su esquema de API y sus diversos requisitos. ¿Qué sucedería si entonces quisiera obtener la información sobre el administrador de una persona? Ello implicaría la consulta de un tercer sistema para obtener la jerarquía organizativa. Todas estas operaciones se podían reunir, pero eran más complejas de lo necesario.

Microsoft Graph nació del deseo de resolver este problema. Al unificar los datos y la autenticación, y unificar los sistemas, el conjunto de API neto resulta mucho más fácil y práctico de usar. Microsoft Graph reúne diversos sistemas de su organización, y representa facetas y funciones clave de una compañía. Desde que se lanzó hace dos años, Microsoft Graph ha seguido creciendo tanto en funcionalidad como en capacidad, hasta que ha podido servir como una API fundacional para su organización.

En el núcleo de Microsoft Graph se encuentra el conjunto de usuarios (por lo general, todos los empleados con una cuenta de una organización). Los grupos simplificados y centralizados son un concepto emergente en Microsoft Graph, que suele empezar con una lista de usuarios y otros grupos de seguridad. Los grupos pueden tener un conjunto de recursos asociado, como un área de trabajo basada en chats de Microsoft Teams, un panel de tareas de Planner y un sitio de SharePoint con bibliotecas de documentos y archivos. Desde ahí, se representan distintas herramientas de trabajo para usuarios y grupos, incluidos los archivos a través de la API de Drive, las tareas a través de la API de Planner, el correo entrante de usuarios y grupos, contactos, calendario, etc., como se muestra en la Figura 1.

API de productividad de Microsoft Graph
Figura 1 API de productividad de Microsoft Graph

Con el tiempo, se han incorporado nuevas funciones en las API de Microsoft Graph. Una nueva capacidad para mantener los metadatos personalizados junto con los elementos de Microsoft Graph le permite personalizar estos elementos en profundidad. Ahora, un grupo ya no es solo un grupo. Con los metadatos adicionales que describen un tema, un instructor e intervalos, un grupo puede representar una clase en una institución educativa. Puede usar estos metadatos para realizar consultas, por ejemplo, para buscar todos los grupos que representen clases de ciencias. También puede conectar sus sistemas en Microsoft Graph mediante la adición de identificadores del sistema a las entidades relacionadas en Microsoft Graph.

Microsoft Graph también va más allá del suministro de las API de creación, lectura, actualización y eliminación (CRUD) para objetos principales. Una característica importante es una capa de información que se genera en segundo plano mientras los usuarios trabajan. Por ejemplo, aunque Graph contiene una jerarquía organizativa completa y una colección de grupos, es posible que estas no siempre formen la mejor representación del trabajo de los equipos. A través de un análisis del trabajo, puede obtener una lista de las personas con una relación más estrecha (equipos virtuales) y los archivos con los que un usuario puede estar conectado. Además, algunas utilidades comunes, como las destinadas a buscar una reunión disponible entre un conjunto de usuarios, se ofrecen en forma de métodos.

Azure Functions

Microsoft Graph existe para usarse y personalizarse en sistemas y procesos más amplios. Como una simple API de REST y en combinación con una amplia matriz de SDK, Microsoft Graph se ha diseñado para que su funcionamiento sea sencillo. Una opción natural para compilar procesos e integraciones en Microsoft Graph es Azure Functions (functions.azure.com), que permite agregar bloques de código localizados donde sea necesario y pagar el código únicamente de manera incremental a medida que se usa. Azure Functions admite el desarrollo entre lenguajes, como C# y Node.js.

Últimamente, un nuevo conjunto de integraciones con Azure Functions facilita la conexión a Microsoft Graph. Azure Functions Binding Extensions, ahora disponible en la versión preliminar con el runtime de Azure Functions 2.0, automatiza algunas de las tareas comunes del trabajo con Microsoft Graph, como la autenticación y el trabajo con la mecánica de los webhooks.

Veamos un ejemplo sobre cómo empezar a trabajar con Microsoft Graph.

Creación de tareas a través de Azure Functions

Imagine que quiere que los administradores revisen y aprueben una acción que ha iniciado un miembro de su equipo. Las tareas de usuario son una manera de pedir a los usuarios que lleven a cabo una acción (para convertir y realizar un seguimiento de acciones de los usuarios). En este caso, quiero implementar un servicio web simple que creará una tarea asignada al administrador de un usuario. 

La primera parada en cualquier proyecto de Microsoft Graph suele ser Probador de Graph. Probador de Graph es un sitio web de aplicaciones que permite copiar rápidamente llamadas de Microsoft Graph, explorar sus resultados y concebir totalmente todo lo que puede hacer. Probador de Graph, que se encuentra disponible en developer.microsoft.com/graph, permite usar un arrendamiento de demostración de solo lectura o iniciar sesión en su propio arrendamiento. Puede iniciar sesión con la cuenta de su organización y acceder directamente a sus propios datos. Se recomienda usar un arrendamiento de desarrollador, disponible a través del Programa de desarrolladores de Microsoft Office en dev.office.com/devprogram. Le proporcionará un arrendamiento independiente donde se sentirá libre para experimentar con el desarrollo.

En este caso, puede introducir dos direcciones URL simples para ver el tipo de llamadas que realizará en este ejemplo. En primer lugar, active la opción "get a user’s manager" ("Obtener el administrador de un usuario), que puede ver en Graph Explorer si selecciona el ejemplo "GET my manager", que se muestra en la Figura 2. La dirección URL correspondiente se muestra en el campo Ejecutar consulta.

Resultados de seleccionar GET my manager
Figura 2 Resultados de seleccionar GET my manager

La segunda parte de la operación consiste en crear una tarea de Planner. En Graph Explorer, puede expandir el conjunto de ejemplos para agregar ejemplos de tareas de Planner. En este conjunto de ejemplos, puede ver la operación de creación de una tarea de Planner (una publicación en https://graph.microsoft.com/v1.0/planner/tasks).

Ahora que comprende las solicitudes de servicio web implicadas, puede compilar una función mediante Azure Functions.

Para empezar, cree una nueva aplicación de Azure Functions. En general, optará por seguir las instrucciones de aka.ms/azfnmsgraph para llevar a cabo esta operación. En resumen, dado que la nueva funcionalidad Azure Functions Binding Extensions es una versión preliminar, deberá cambiar la aplicación Azure Functions al runtime de la versión preliminar 2.0 ("beta"). También deberá instalar Microsoft Graph Extension y configurar la autenticación de App Service.

Cuando configure el registro de la aplicación de Microsoft Graph, para este ejemplo deberá agregar algunos permisos más para admitir la lectura de información y tareas de administrador, tales como:

  • Proyectos y tareas de usuario CRUD (Tasks.ReadWrite)
  • Ver el perfil básico de los usuarios (perfil)
  • Leer y escribir todos los grupos (Group.ReadWrite.All)
  • Leer el perfil básico de todos los usuarios (User.ReadBasic.All)

Optará por usar Azure Functions Binding Extensions para Microsoft Graph con el fin de controlar la autenticación y asegurarse de que tiene un token de acceso autenticado con el que acceder a Microsoft Graph API. Para hacerlo, creará un desencadenador HTTP de C# estándar. Debajo de Integrar, seleccione Editor avanzado y use los enlaces que se muestran en la Figura 3. Ello requerirá que el usuario inicie sesión, se autentique y apruebe su aplicación para que se pueda usar.

Figura 3 Creación de un desencadenador HTTP para controlar la autenticación

{
  "bindings": [
    {
      "name": "req",
      "type": "httpTrigger",
      "direction": "in"
    },
    {
      "type": "token",
      "direction": "in",
      "name": "accessToken",
      "resource": "https://graph.microsoft.com",
      "identity": "userFromRequest"
    },
    {
      "name": "$return",
      "type": "http",
      "direction": "out"
    }
  ],
  "disabled": false
}

El código correspondiente a la función se muestra en la Figura 4. Tenga en cuenta que deberá configurar una variable de entorno para la aplicación de función denominada PlanId, que tiene el identificador del plan de Planner que quiere usar para sus tareas. Esto se puede realizar en Configuración de la aplicación para la aplicación de función.

Figura 4 Publicación de una tarea asignada a un origen de Azure Functions del administrador de un usuario

#r "Newtonsoft.Json"
using System.Net;
using System.Threading.Tasks;
using System.Configuration;
using System.Net.Mail;
using System.IO;
using System.Web;
using System.Text;
using Newtonsoft.Json.Linq;
public static HttpResponseMessage Run(HttpRequestMessage req, string accessToken, TraceWriter log)
{
  log.Info("Processing incoming task creation requests.");
  // Retrieve data from query string
  // Expected format is taskTitle=task text&taskBucket=bucket
  // title&taskPriority=alert
  var values = HttpUtility.ParseQueryString(req.RequestUri.Query);
  string taskTitle = values["taskTitle"];
  string taskBucket = values["taskBucket"];
  string taskPriority = values["taskPriority"];
  if (String.IsNullOrEmpty(taskTitle))
  {
    log.Info("Incomplete request received - no title.");
    return new HttpResponseMessage(HttpStatusCode.BadRequest);
  }
  string planId = System.Environment.GetEnvironmentVariable("PlanId");
  // Retrieve the incoming users' managers ID
  string managerJson = GetJson(
    "https://graph.microsoft.com/v1.0/me/manager/", accessToken, log);
    dynamic manager = JObject.Parse(managerJson);
  string managerId = manager.id;
  string appliedCategories = "{}";
  if (taskPriority == "alert" || taskPriority == "1")
  {
    appliedCategories = "{ \"category1\": true }";
  }
  else
  {
    appliedCategories = "{ \"category2\": true }";
  }
  string now =  DateTime.UtcNow.ToString("yyyy-MM-ddTHH\\:mm\\:ss.fffffffzzz");
  string due =  DateTime.UtcNow.AddDays(5).ToString(
    "yyyy-MM-ddTHH\\:mm\\:ss.fffffffzzz");
  string bucketId = "";
  // If the incoming request wants to place a task in a bucket,
  // find the bucket ID to add it to
  if (!String.IsNullOrEmpty(taskBucket))
  {
    // Retrieve a list of planner buckets so that you can match
    // the task to a bucket, where possible
    string bucketsJson = GetJson(
      "https://graph.microsoft.com/v1.0/planner/plans/" + planId +
      "/buckets", accessToken, log);
    if (!String.IsNullOrEmpty(bucketsJson))
    {
      dynamic existingBuckets = JObject.Parse(bucketsJson);
      taskBucket = taskBucket.ToLower();
      foreach (var bucket in existingBuckets.value)
      {
        var existingBucketTitle = bucket.name.ToString().ToLower();
        if (taskBucket.IndexOf(existingBucketTitle) >= 0)
        {
          bucketId = ", \"bucketId\": \"" + bucket.id.ToString() + "\"";
        }
      }
    }
  }
  string jsonOutput = String.Format(" {{ \"planId\": \"{0}\", \"title\": \"{1}\", \"orderHint\": \" !\", \"startDateTime\": \"{2}\", \"dueDateTime\": \"{6}\", \"appliedCategories\": {3}, \"assignments\": {{ \"{4}\": {{ \"@odata.type\": \"#microsoft.graph.plannerAssignment\",  \"orderHint\": \" !\"  }} }}{5} }}",
    planId, taskTitle, now, appliedCategories, managerId, bucketId, due);
  log.Info("Creating new task: " + jsonOutput);
  PostJson("https://graph.microsoft.com/v1.0/planner/tasks",
    jsonOutput, accessToken, log);
  return new HttpResponseMessage(HttpStatusCode.OK);
}
private static string GetJson(string url, string token, TraceWriter log)
{
  HttpWebRequest hwr = (HttpWebRequest)WebRequest.CreateHttp(url);
  log.Info("Getting Json from endpoint '" + url + "'");
  hwr.Headers.Add("Authorization", "Bearer " + token);
  hwr.ContentType = "application/json";
  WebResponse response = null;
  try
  {
    response = hwr.GetResponse();
    using (Stream stream = response.GetResponseStream())
    {
      using (StreamReader sr = new StreamReader(stream))
      {
        return sr.ReadToEnd();
      }
     }
  }
  catch (Exception e)
  {
    log.Info("Error: " + e.Message);
  }
  return null;
}
private static string PostJson(string url, string body, string token, TraceWriter log)
{
  HttpWebRequest hwr = (HttpWebRequest)WebRequest.CreateHttp(url);
  log.Info("Posting to endpoint " + url);
  hwr.Method = "POST";
  hwr.Headers.Add("Authorization", "Bearer " + token);
  hwr.ContentType = "application/json";
  var postData = Encoding.UTF8.GetBytes(body.ToString());
  using (var stream = hwr.GetRequestStream())
  {
  stream.Write(postData, 0, postData.Length);
  }
  WebResponse response = null;
  try
  {
    response = hwr.GetResponse();
    using (Stream stream = response.GetResponseStream())
    {
      using (StreamReader sr = new StreamReader(stream))
      {
        return sr.ReadToEnd();
      }
    }
  }
  catch (Exception e)
  {
    log.Info("Error: " + e.Message);
  }
  return null;
}

En este ejemplo se muestra cómo reunir conjuntos de datos diversos (el administrador de un usuario y tareas de Planner en este caso) en un fragmento de código con un token de autenticación. Crear y asignar tareas es un método común para dirigir actividades entre equipos, de modo que la posibilidad de crear tareas sobre la marcha y aprovechar las experiencias de Planner existentes es bastante útil. No es como "widgetMarketingTeam.launchCampaign()", pero al menos puede ver cómo se crea el conjunto básico de tareas que permitiría al equipo empezar de manera dirigida y estructurada.

Procesamiento de archivos en OneDrive

Otra tarea que puede realizar es procesar archivos existentes en el servicio OneDrive de un usuario. En este ejemplo, aprovecha Azure Functions Binding Extensions para Microsoft Graph con la finalidad de preparar un archivo para su uso. Luego, lo pasa a Cognitive Services APIs para el reconocimiento de voz. Este es un ejemplo de procesamiento de datos que puede ser una manera práctica de obtener más valor de los archivos en OneDrive y SharePoint.

Para empezar, debe seguir algunos de los pasos del ejemplo anterior, incluidos los de configuración del registro de Azure Active Directory y Function App. Tenga en cuenta que el registro de la aplicación Azure Active Directory que use para este ejemplo deberá tener el permiso "Leer todos los archivos a los que el usuario puede tener acceso" (Files.Read.All). También deberá tener una clave de Speech API de Cognitive Services, que puede obtener de aka.ms/tryspeechapi.

Como antes, empiece con Azure Functions Binding Extensions y configure un nuevo desencadenador HTTP de C#. Debajo de la pestaña Integrar de la función, use el marcado de enlace que se muestra en la Figura 5 para conectar la función a una extensión de enlace. En este caso, la extensión de enlace vincula el parámetro myOneDriveFile de su función de Azure a la extensión de enlace de OneDrive.

Figura 5 Configuración de un nuevo desencadenador para obtener un archivo en OneDrive

{
  "bindings": [
    {
      "name": "req",
      "type": "httpTrigger",
      "direction": "in"
    },
    {
      "name": "myOneDriveFile",
      "type": "onedrive",
      "direction": "in",
      "path": "{query.filename}",
      "identity": "userFromRequest",
    },
    {
      "name": "$return",
      "type": "http",
      "direction": "out"
    }
  ],
  "disabled": false
}

Ha llegado la hora del código, que se muestra en la Figura 6.

Figura 6 Transcripción de un archivo de audio de OneDrive

#r "Newtonsoft.Json"
using System.Net;
using System.Text;
using System.Configuration;
using Newtonsoft.Json.Linq;
public static  async Task<HttpResponseMessage> Run(HttpRequestMessage req,
  Stream myOneDriveFile, TraceWriter log)
{
  // Download the contents of the audio file
  log.Info("Downloading audio file contents...");
  byte[] audioBytes;
  audioBytes = StreamToBytes(myOneDriveFile);
  // Transcribe the file using cognitive services APIs
  log.Info($"Retrieving the cognitive services access token...");
  var accessToken =
    System.Environment.GetEnvironmentVariable("SpeechApiKey");
  var bingAuthToken = await FetchCognitiveAccessTokenAsync(accessToken);
  log.Info($"Transcribing the file...");
  var transcriptionValue = await RequestTranscriptionAsync(
    audioBytes, "en-us", bingAuthToken, log);
  HttpResponseMessage hrm = new HttpResponseMessage(HttpStatusCode.OK);
  if (null != transcriptionValue)
  {
    hrm.Content = new StringContent(transcriptionValue, Encoding.UTF8, "text/html");
  }
  else
  {
    hrm.Content = new StringContent("Content could not be transcribed.");
  }
  return hrm;
}
private static async Task<string> RequestTranscriptionAsync(byte[] audioBytes,
  string languageCode, string authToken, TraceWriter log)
{
  string conversation_url = $"https://speech.platform.bing.com/speech/recognition/conversation/cognitiveservices/v1?language={languageCode}";
  string dictation_url = $"https://speech.platform.bing.com/speech/recognition/dictation/cognitiveservices/v1?language={languageCode}";
  HttpResponseMessage response = null;
  string responseJson = "default";
  try
  {
    response = await PostAudioRequestAsync(conversation_url, audioBytes, authToken);
    responseJson = await response.Content.ReadAsStringAsync();
    JObject data = JObject.Parse(responseJson);
    return data["DisplayText"].ToString();
  }
  catch (Exception ex)
  {
    log.Error($"Unexpected response from transcription service A: {ex.Message} |" +
      responseJson + "|" + response.StatusCode  + "|" +
      response.Headers.ToString() +"|");
    return null;
  }
}
private static async Task<HttpResponseMessage> PostAudioRequestAsync(
  string url, byte[] bodyContents, string authToken)
{
  var payload = new ByteArrayContent(bodyContents);
  HttpResponseMessage response;
  using (var client = new HttpClient())
  {
    client.DefaultRequestHeaders.Add("Authorization", "Bearer " + authToken);
    payload.Headers.TryAddWithoutValidation("content-type", "audio/wav");
    response = await client.PostAsync(url, payload);
  }
  return response;
}
private static byte[] StreamToBytes(Stream stream)
{
  using (MemoryStream ms = new MemoryStream())
  {
    stream.CopyTo(ms);
    return ms.ToArray();
  }
}
private static async Task<string> FetchCognitiveAccessTokenAsync(
  string subscriptionKey)
{
  string fetchUri = "https://api.cognitive.microsoft.com/sts/v1.0";
  using (var client = new HttpClient())
  {
    client.DefaultRequestHeaders.Add("Ocp-Apim-Subscription-Key", subscriptionKey);
    UriBuilder uriBuilder = new UriBuilder(fetchUri);
    uriBuilder.Path += "/issueToken";
    var response = await client.PostAsync(uriBuilder.Uri.AbsoluteUri, null);
    return await response.Content.ReadAsStringAsync();
  }
}

Con esta función implementada, después de que un usuario inicie sesión en su función de Azure, puede especificar un parámetro de nombre de archivo. Si un archivo tiene un nombre .wav e incluye contenido en inglés, el texto se transcribirá en inglés. Dado que se implementa con Azure Functions, la función incurrirá en costos por lo general solo al llamarla, lo que proporciona una manera flexible para extender los datos que tiene en Microsoft Graph.

Azure Functions y Microsoft Graph

Los dos ejemplos aquí presentados muestran cómo compilar procesos humanos y técnicos basados en datos de Microsoft Graph. En combinación con la amplitud de la cobertura de Microsoft Graph y la capacidad de cargas de trabajo cruzadas (por ejemplo, jerarquía organizativa y tareas, como en el ejemplo de tarea de este artículo), puede crear y agregar valor en toda su organización. La combinación de Microsoft Graph y Azure Functions permite compilar la API completa para su organización y transformar la productividad general. Para empezar a compilar soluciones para su organización, visite developer.microsoft.com/graph y trabaje con Azure Functions en functions.azure.com.


Mike Ammerlaan es director de marketing de productos en el equipo del ecosistema de Microsoft Office y ayuda a los usuarios a compilar soluciones atractivas con Office 365. Anteriormente, trabajó en Microsoft como administrador de programas durante 18 años, en el desarrollo de productos como SharePoint, Excel, Yammer, Bing Maps y Combat Flight Simulator.

Gracias a los siguientes expertos técnicos de Microsoft por revisar este artículo:  Ryan Gregg, Matthew Henderson y Dan Silver


Discuta sobre este artículo en el foro de MSDN Magazine