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
En mi calendario principal he creado 2 eventos:
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.
Con esto ya disponemos de la primera parte del gráfico OAuth, esto es, la manera de conseguir un código de autenticación.
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.
Siguiente post Microsoft Graph - Eventos calendarios Outlook III
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.
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í:
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
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 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.
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.
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 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;
}
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".
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.
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
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".
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.
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.
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:
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:
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.
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