Microsoft Graph - Eventos calendarios Outlook II

Microsoft graph

 

Seguimos con la segunda parte del post-tutorial.
La primera parte la podemos encontrar en este enlace Microsoft graph - Eventos calendarios Outlook I

A programar un poco

Esperad un poco, antes de empezar a programar, vamos a ver qué nos vamos a encontrar cuando hagamos las llamadas. 
Microsoft graph cuenta con una herramienta para explorar las posibles llamadas a través de este sistema llamada Microsoft graph explorer en la que propone una serie de ejemplos ya definidos muy útiles y que conviene probar.
Una vez dentro pulsamos en el icono de arriba derecha para loguearnos y tener disponible nuestra cuenta.
Veremos algo así:


Si conocéis Postman, veréis que se asemeja bastante.
Vamos a probar una llamada a todos los eventos en mi calendario, pulsamos en calendario de Outlook, como se ve en la imagen en el ejemplo todos los eventos en mi calendario (dentro de calendario de Outlook, 2ª opción) y la orden de llamada se mostrará en el recuadro superior:
https://graph.microsoft.com/v1.0/me/events?$select=subject,body,bodyPreview,organizer,attendees,start,end,location

Debemos otorgar permisos a esta llamada, para ello pulsamos en Modify permissions



y concedemos permisos pulsando sobre los botones azules en los que pone Consent  y una vez otorgados los permisos lanzamos la llamada pulsando en el botón Run query (arriba derecha en azul)
y observamos la respuesta:



Una vez consigamos desde nuestro código realizar esta llamada, vamos a recibir como respuesta un JSON similar a este.
Ahora vamos a analizar un un poco esta respuesta.
A primera vista lo que nos interesa son los valores que residen en el nodo value.
El mensaje completo al comenzar por llave { (línea 1) es un elemento de tipo JSON Object.
El contenido del nodo value comienza por corchete [ por lo que será un JSON Array.
Seguimos bajando y vemos que los valores están anidados en JSON Object al comenzar por llave {, etc.
Bueno, ya tenemos una pequeña idea de la estructura del mensaje y de los nombres de los campos.

Aclarar que la llamada devolverá eventos únicamente de mi calendario principal, por lo que para recoger eventos de un calendario específico debemos solicitarlo.
En esta página podemos ver cómo realizar las llamadas, primero deberíamos obtener calendarios y posteriormente los eventos de un calendario concreto.

En mi calendario principal  he creado 2 eventos:
  • Evento 1 en calendario llamado Calendario
  • Evento 2 en calendario llamado Calendario
Ahora sí que comenzamos a programar.

Vamos a crear una tabla donde almacenaremos los datos necesarios para la llamada y no tener que informarlos cada vez que conectemos:
table 60800 GraphSetup
{
    Caption = 'Graph Setup';
    DataClassification = ToBeClassified;

    fields
    {
        field(1; "Primary Key"; Code[10])
        {
            Caption = 'Primary Key';
        }
        field(2; "Client ID"; Text[250])
        {
            Caption = 'Id Cliente';
            DataClassification = EndUserIdentifiableInformation;
        }
        field(3; "Client Secret"; Text[250])
        {
            Caption = 'Secreto';
            DataClassification = EndUserIdentifiableInformation;

            trigger OnValidate()
            begin
                IsolatedStorage.Set('Secret', "Client Secret");
                "Client Secret" := '';
            end;
        }
        field(4; "Redirect URL"; Text[250])
        {
            Caption = 'URL redirección';
        }
        field(5; Scope; Text[250])
        {
            Caption = 'Ámbito';
        }
        field(6; "Authorization URL"; Text[250])
        {
            Caption = 'URL de autorización';
        }
        field(7; "Token URL"; Text[250])
        {
            Caption = 'URL de token';
        }        
        field(8; "Access Token"; Blob)
        {
            Caption = 'Access Token';
            DataClassification = EndUserIdentifiableInformation;
        }
        field(9; "Refresh Token"; Blob)
        {
            Caption = 'Refresh Token';
            DataClassification = EndUserIdentifiableInformation;
        }
        field(10; "Authorization Time"; DateTime)
        {
            Caption = 'Authorization Time';
            Editable = false;
            DataClassification = EndUserIdentifiableInformation;
        }
        field(11; "Expires In"; Integer)
        {
            Caption = 'Expires In';
            Editable = false;
            DataClassification = EndUserIdentifiableInformation;
        }
        field(12; "RefTkn Expires In"; Integer)
        {
            Caption = 'Ext. Expires In';
            Editable = false;
            DataClassification = EndUserIdentifiableInformation;
        }
    }

    keys
    {
        key(PK; "Primary Key")
        {
            Clustered = true;
        }
    }

    procedure AccessTokenToText(): Text
    var
        IStream: InStream;
        TxtBuilder: TextBuilder;
        Line: Text;
    begin
        Rec.CalcFields("Access Token");
        if Rec."Access Token".HasValue then begin
            Rec."Access Token".CreateInStream(IStream, TextEncoding::UTF8);
            while not IStream.EOS do begin
                IStream.ReadText(Line, 1024);
                TxtBuilder.Append(Line);
            end;
        end;
        exit(TxtBuilder.ToText())
    end;
}

En esta tabla alojaremos nuestros datos de conexión y a su vez almacenaremos el token junto con su periodo de validez.
Un token de este tipo tiene un periodo de validez de 1 hora, en concreto 3600 segundos por lo que mientras hagamos llamadas dentro de ese periodo de tiempo no será necesario obtener un nuevo token.

A su vez el campo donde almacenaremos el token es de tipo blob con lo que cuando necesitemos consultarlo debemos transformarlo en algo "legible", para ello usaremos la función AccessTokenToText que hará lo comentado.
A mayores recomiendo fijarnos en el campo 3, Client Secret.
Al ser información sensible vamos a hacer lo siguiente, según se escriba el valor en la celda, en vez de almacenarlo en la tabla lo alojaremos en IsolatedStorage, que es un espacio de datos aislado e interno para este tipo de datos que es mejor mantener oculto. El cómo tratar el manejo de ese campo lo dejo al gusto de cada uno.
Ahora pulsando Ctrl+Alt+F1 no es posible ver el valor del campo, así como depurando desde ninguna otra extensión a parte de la nuestra.
Recomiendo hacer esto para cualquier dato de tipo clave o sensible que no debería estar "expuesto".

Ahora creamos la página para mostrar la configuración anterior
page 60802 "Graph setup"
{
    ApplicationArea = All;
    Caption = 'Configuración conector Outlook';
    PageType = Card;
    SourceTable = GraphSetup;
    UsageCategory = Tasks;
    InsertAllowed = false;
    DeleteAllowed = false;

    layout
    {
        area(content)
        {
            group(General)
            {
                Caption = 'General';

                field("Client ID"; Rec."Client ID")
                {
                }
                field("Client Secret"; Rec."Client Secret")
                {
                }
                field("Redirect URL"; Rec."Redirect URL")
                {
                }
                field(Scope; Rec.Scope)
                {
                }
                field("Authorization URL"; Rec."Authorization URL")
                {
                }
                field("Token URL"; Rec."Token URL")
                {
                }
            }
        }
    }

    trigger OnOpenPage()
    begin
        Rec.Reset;
        if not Rec.Get then begin
            Rec.Init;
            Rec.Insert;
        end;
    end;
}

Lo único a destacar aquí es que al abrir la página, si no existe registro inicial, se crea uno vacío y al no estar permitido insertar ni eliminar, no existe posibilidad de disponer de más de 1 registro de configuración.

Veamos qué tenemos hasta ahora



Es necesario identificarnos para obtener un código de autorización (paso 1 del gráfico OAuth2.0) con el que posteriormente poder conseguir el token o llave con la que abrir la puerta.
Esto lo conseguiremos mediante un addin que utilizaremos en una página a través de la que nos identificaremos, el propósito de esto es recoger el código que es enviado a dicha página y con el que trabajaremos nosotros: nuestro código de autorización.

En mi estructura de carpetas he depositado el código en una carpeta llamada Objects. Dentro de ella crearemos una nueva carpeta llamada Addin.
En ella crearemos el control, será un fichero con extensión al y cuyo nombre es indiferente
controladdin "OAuth 2.0 Integration"
{
    Scripts = './Objects/Addin/OAuthIntegration.js';
    RequestedWidth = 0;
    RequestedHeight = 0;
    HorizontalStretch = false;
    VerticalStretch = false;

    procedure StartAuthorization(AuthRequestUrl: Text);
    event AuthorizationCodeRetrieved(AuthCode: Text; ReturnState: Text);
    event ControlAddInReady();
}


Como vemos utiliza un script dentro de la misma carpeta llamado OAuthIntegration.js, lo creamos
var w;
var invertvalID;

Microsoft.Dynamics.NAV.InvokeExtensibilityMethod('ControlAddInReady');

function StartAuthorization(url) {
    w = window.open(url, '_blank', 'width=972,height=904,location=no');
    invertvalID = window.setInterval(TimerTic, 1000);
}

function TimerTic() {
    var urlParams = new URLSearchParams(w.location.search);
    if (urlParams.has('code')) {
        window.clearInterval(invertvalID);
        Microsoft.Dynamics.NAV.InvokeExtensibilityMethod('AuthorizationCodeRetrieved', [urlParams.get('code'), urlParams.get('state')]);
        window.close();
    }
}

Este control va a ejecutarse desde una página que crearemos a continuación.
Aquí se inicia un reloj que ejecuta una función a intervalos de 1 segundo y la función no es otra que "en cuanto tengas un código, se lo pasas a Business central".
Ahora crearemos la página para mostrar este control

page 60801 OAuth2Dialog
{
    PageType = Card;

    layout
    {
        area(Content)
        {
            usercontrol(OAuthIntegration; "OAuth 2.0 Integration")
            {
                ApplicationArea = All;

                trigger AuthorizationCodeRetrieved(AuthCode: Text; ReturnState: Text);
                var
                    Error001Lbl: Label 'Estados no válidos.';
                begin
                    if State <> ReturnState then
                        Error(Error001Lbl);
                    AuthorizationCode := AuthCode;
                    CurrPage.Close();
                end;

                trigger ControlAddInReady();
                begin
                    CurrPage.OAuthIntegration.StartAuthorization(OAuthRequestUrl);
                end;
            }
        }
    }

    procedure SetOAuth2Properties(AuthRequestUrl: Text; InitialState: Text)
    begin
        OAuthRequestUrl := AuthRequestUrl;
        State := InitialState;
    end;

    procedure GetAuthCode(): Text
    begin
        exit(AuthorizationCode);
    end;

    var
        OAuthRequestUrl: Text;
        State: Text;
        AuthorizationCode: Text;
}

Con esto ya disponemos de la primera parte del gráfico OAuth, esto es, la manera de conseguir un código de autenticación.

Vamos a crear una codeunit de manejo de la conexión en la que primero crearemos una función para identificarnos y conseguir el código de autorización.

codeunit 60800 GraphConnectorMngt
{
    var
        GraphSetup: Record GraphSetup;

    procedure CheckConfig()
    var
        Error001Lbl: Label 'No existe configuración, por favor, cree una antes de continuar.';
    begin
        if not GraphSetup.Get() then
            Error(Error001Lbl);

        GraphSetup.TestField("Authorization URL");
        GraphSetup.TestField("Token URL");
        GraphSetup.TestField("Client ID");
        GraphSetup.TestField("Redirect URL");
        GraphSetup.TestField(Scope);
    end;

    procedure GetAuthorizationCode() AuthorizationCode: Text
    var
        DotNetUriBuilder: Codeunit Uri;
        OAuth2Dialog: Page OAuth2Dialog;
        AuthURL: Text;
        State: Text;
        Error001Lbl: Label 'El código de autorización no es válido.';
    begin        
        State := Format(CreateGuid(), 0, 4);

        AuthURL := GraphSetup."Authorization URL" + '?' +
                    'client_id=' + DotNetUriBuilder.EscapeDataString(GraphSetup."Client ID") +
                    '&redirect_uri=' + DotNetUriBuilder.EscapeDataString(GraphSetup."Redirect URL") +
                    '&state=' + DotNetUriBuilder.EscapeDataString(State) +
                    '&scope=' + DotNetUriBuilder.EscapeDataString(GraphSetup.Scope) +
                    '&response_type=code';

        OAuth2Dialog.SetOAuth2Properties(AuthURL, State);
        OAuth2Dialog.RunModal();

        AuthorizationCode := OAuth2Dialog.GetAuthCode();

        if AuthorizationCode = '' then
            Error(Error001Lbl);
    end;

}

La función GetAuthorizationCode comprueba que dispongamos de un registro de configuración (CheckConfig) y de que dispongamos de datos en los campos necesarios.
Posteriormente construye la url de llamada de autenticación y con esa dirección abrimos la página que acabamos de crear.

Ahora necesitaremos una función que utilizando ese código de autenticación obtenga el token o llave con la que podamos acceder a nuestros datos, la crearemos en la codeunit a continuación:

    procedure GetAccessToken(AuthCode: Text)
    var
        DotNetUriBuilder: Codeunit Uri;
        Client: HttpClient;
        Request: HttpRequestMessage;
        Response: HttpResponseMessage;
        Content: HttpContent;
        ContentHeaders: HttpHeaders;
        JAccessToken: JsonObject;
        JToken: JsonToken;
        OStream: OutStream;
        ContentText: Text;
        ResponseText: Text;
        Property: Text;
        Success: Boolean;
        Secret: Text;
    begin
        CheckConfig();
        IsolatedStorage.Get('Secret', Secret);
        ContentText := 'grant_type=authorization_code' +
                                '&code=' + AuthCode +
                                '&redirect_uri=' + DotNetUriBuilder.EscapeDataString(GraphSetup."Redirect URL") +
                                '&client_id=' + DotNetUriBuilder.EscapeDataString(GraphSetup."Client ID") +
                                '&client_secret=' + DotNetUriBuilder.EscapeDataString(Secret);

        Content.WriteFrom(ContentText);
        Content.GetHeaders(ContentHeaders);
        ContentHeaders.Remove('Content-Type');
        ContentHeaders.Add('Content-Type', 'application/x-www-form-urlencoded');

        Request.Method := 'POST';
        Request.SetRequestUri(GraphSetup."Token URL");
        Request.Content(Content);

        Client.Send(Request, Response);
        Response.Content.ReadAs(ResponseText);
        if Response.IsSuccessStatusCode() then
            Success := JAccessToken.ReadFrom(ResponseText)
        else
            Error(ResponseText);

        if Success then begin
            foreach Property in JAccessToken.Keys() do begin
                JAccessToken.Get(Property, JToken);
                case Property of
                    'token_type',
                    'scope':
                        ;
                    'expires_in', 'expires_on':
                        begin
                            GraphSetup."Expires In" := JToken.AsValue().AsInteger();
                            GraphSetup."Authorization Time" := CurrentDateTime;
                        end;
                    'ext_expires_in':
                        GraphSetup."RefTkn Expires In" := JToken.AsValue().AsInteger();
                    'access_token':
                        begin
                            GraphSetup."Access Token".CreateOutStream(OStream, TextEncoding::UTF8);
                            OStream.WriteText(JToken.AsValue().AsText());
                        end;
                    'refresh_token':
                        begin
                            GraphSetup."Refresh Token".CreateOutStream(OStream, TextEncoding::UTF8);
                            OStream.WriteText(JToken.AsValue().AsText());
                        end;
                end;
            end;
            GraphSetup.Modify();
        end;
    end;

Como punto a tener en cuenta: si recordamos, el campo secreto se había almacenado en un espacio aislado, fuera de cualquier tabla accesible y lo recuperamos IsolatedStorage.Get('Secret', Secret);. Nuestra extensión internamente tiene acceso a esos datos.
Realizamos una llamada HttpRequest de tipo POST, y del resultado obtenido recopilamos los datos que necesitamos y los guardamos en nuestra tabla de configuración.
Como se puede observar aquí existe un dato del que no hemos hablado, el token de refresco, lo veremos más adelante, aunque tiene una gran importancia, aunque en este punto ese campo lo recibimos vacío.

Ahora estamos en disposición de realizar la primera llamada y obtener las citas de nuestro calendario.
Vamos a crear una página a modo de borrador que más adelante eliminaremos o modificaremos. Por ahora será solamente la usaremos para probar todo esto, simplemente comprobar que podemos realizar una llamada y obtener un mensaje similar al que obtuvimos en Microsoft graph explorer:

page 60805 "Outlook calendar try"
{
    ApplicationArea = All;
    Caption = 'Probar eventos Outlook';
    PageType = Card;
    UsageCategory = Tasks;

    layout
    {
        area(content)
        {
            group(General)
            {
                Caption = 'General';

            }
        }
    }

    actions
    {
        area(Processing)
        {
            action(a1)
            {
                Caption = 'Obtener eventos calendario principal';
                Image = Calendar;

                trigger OnAction()
                begin
                    GetEvents();
                end;
            }
        }
        area(Promoted)
        {
            actionref(aa1; a1) { }
        }
    }

    procedure GetEvents()
    var
        GraphConnectorMngt: Codeunit GraphConnectorMngt;
        GraphSetup: Record GraphSetup;
        ElapsedSecs: Integer;
        AuthCode: Text;
        NeedToken: Boolean;
    begin
        GraphConnectorMngt.CheckConfig();
        GraphSetup.Get();
        if (GraphSetup."Authorization Time" <> 0DT) then begin
            ElapsedSecs := Round((CurrentDateTime() - GraphSetup."Authorization Time") / 1000, 1, '>');
            if ElapsedSecs >= GraphSetup."Expires In" then
                NeedToken := true;
        end else
            NeedToken := true;
        if NeedToken then begin
            AuthCode := GraphConnectorMngt.GetAuthorizationCode();
            GraphConnectorMngt.GetAccessToken(AuthCode);
        end;
        SendMessage(GraphSetup.AccessTokenToText());
    end;

    local procedure SendMessage(Token: Text)
    var
        Client: HttpClient;
        Headers: HttpHeaders;
        RequestMessage: HttpRequestMessage;
        ResponseMessage: HttpResponseMessage;
        ResponseText: Text;
        Url: Label 'https://graph.microsoft.com/v1.0/me/events?$select=subject,body,bodyPreview,organizer,attendees,start,end,location';
    begin
        Headers := Client.DefaultRequestHeaders();
        Headers.Add('Authorization', StrSubstNo('Bearer %1', Token));
        RequestMessage.SetRequestUri(Url);
        RequestMessage.Method := 'GET';

        if Client.Send(RequestMessage, ResponseMessage) then begin
            ResponseMessage.Content.ReadAs(ResponseText);
            if ResponseMessage.IsSuccessStatusCode() then begin
                if ResponseMessage.Content.ReadAs(ResponseText) then begin
                    Message(ResponseText);
                end;
            end else
                Error(ResponseText);
        end;
    end;
}


Ahora lanzamos nuestra extensión y localizamos la página Probar eventos Outlook.
En ella veremos una única acción en la que reza "Obtener eventos calendario principal".
Pulsamos en ella y tras identificarnos nos devuelve un mensaje con la respuesta obtenida que como podemos observar en la imagen es similar a la obtenida en Microsoft graph explorer.


Este tipo de tutoriales terminan en este punto, se ha definido la forma de configurar el entorno y una vez desplegado, somos capaces de lanzar ordenes sencillas.
A mi me gustaría ir un poco más allá y darle una aplicación práctica.
Por ejemplo:
  • Recoger los eventos del calendario que seleccionemos
  • Mostrar esos eventos en un calendario visual
  • Configurar un calendario en Business central encargado de mostrar las fabricaciones y poder además de ver los eventos, crear nuevos eventos con las fechas de fabricación de los productos.
  • ...

Hasta aquí el post.
Ya tenemos creada la forma de obtener un token y un borrador de una llamada básica para obtener los eventos del calendario principal de Outlook.
En el siguiente post además de lo comentado, daremos uso al valor que hemos visto antes: token de refresco.

En esta ocasión no habrá código, ya que no veo sentido a dejar código de algo "incompleto". Una vez finalice el tutorial, dejaré disponible el código fuente en github.

Espero que os sea de utilidad.

Siguiente post Microsoft Graph - Eventos calendarios Outlook III

Publicar un comentario

Añade comentario (0)

Artículo Anterior Artículo Siguiente