Find

Article
· May 9, 2024 9m read

Microsoft 365 - Events and Tasks

 In our previous article, we explored how to send emails through Microsoft 365 using the Graph API. Since then, an anonymous client reached out to me about setting up some other methods of notifications through that API. He was particularly interested in Outlook’s tasks and calendar events. 

If you still have your client ID, client secret, and application ID from the last exercise, you may continue utilizing them. We will reuse the globals we stored from before with the GetToken method. Most of the setup in Microsoft Entra will not need to be repeated. The only exception would be that you will have to go back to your application permissions and add the correct permissions for each item. We will start with adding a Task that requires permission Tasks.ReadWrite.All. Add that permission and grant admin consent employing the same process we described in the previous article.

As always, our API request will be built from a %Net.HttpRequest object. In this case, we will have to make two additional requests besides the one to get the token. Since Tasks in Outlook currently use Microsoft ToDo, your tasks can be organized into lists now. To create a task we must know the ID of the list to which we should send it. We will utilize the default task list for that. To find out its ID we need to create a request to the ToDo lists API. It will be a simple HTTP get request with no entity body, so it will be very easy.

set listRequest = ##class(%Net.HttpRequest).%New()
set listRequest.Server = "graph.microsoft.com"
set listRequest.SSLConfiguration = "O365"
set listRequest.Https = 1
set listRequest.Location = "/v1.0/users/youremail@domain.com/todo/lists"
do listRequest.SetParam("$filter","displayName eq 'Tasks'")
set sc = ..GetToken(.token)
do listRequest.SetHeader("Authorization","Bearer "_token)
set sc = listRequest.Get()

Pay attention to the parameter we set. On HTTP get requests, the graph API supports some OData filtering. By adding it, we specify that we are looking for the task list with the display name “Tasks,” which should be the default task list for most users. Since my client is not very tech savvy - he says he only ever accepts cookies with milk - he hasn’t changed the name of his default task list.

Note that we are going to employ the same basic setup that we operated for the email API request. The server will be graph.microsoft.com. We will  utilize the O365 SSL configuration. We will also get the bearer token and add it to the header. Then we will send the response to a location that includes the target user’s email address. This time we will operate the request’s Get() method rather than Post(). It will return a JSON response that will look similar to the one below:

{
    "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#users('########-####-####-####-############')/todo/lists",
    "value": [
        {
            "@odata.etag": "#####################################",
            "displayName": "Tasks",
            "isOwner": true,
            "isShared": false,
            "wellknownListName": "defaultList",
            "id": "There will be a big long string here."
        }
    ]
}

Without the filter parameter, we would get all user’s task lists. They often have at least two lists: a default task list and a separate one for flagged emails. If they set up other lists in ToDo, there will be even more. We are only interested in the default task list today. To get its ID, we should do the following:

set respObj = ##class(%Library.DynamicObject).%FromJSON(listRequest.HttpResponse.Data)
set respArray = respObj.%Get("value")
set listObj = respArray.%Get(0)
set ListID = listObj.%Get("id")

We need to use it in the task location to complete the request, so let's set it up similarly to the previous request.

set taskRequest = ##class(%Net.HttpRequest).%New()
set taskRequest.SSLConfiguration = "O365"
set taskRequest.Https = 1
set taskRequest.Server = "graph.microsoft.com"
set taskRequest.ContentType = "application/json"
set taskRequest.Location = "/v1.0/users/youremail@domain.com/todo/lists/"_ListID_"/tasks"

The body of this request will have a JSON structure. We could set just the title property of this JSON object and send the request. If we did it, it would give the user a new task with only that title and nothing else.

set mytask = ##class(%Library.DynamicObject).%New()
do mytask.%Set("title","Merry Christmas!")

However, my client’s needs are more advanced than that! He has a recurring annual task that requires a bit more information, including a small description, a due date, a reminder, and some recurrence. We will start with the body which will contain a very basic reminder. He is getting old, you know. So, the body will be a JSON object itself. Just like the email body, it will have two properties. One of them will be contentType which can be either “text” or “html”, and the other will be content, which will consist of the actual content of the body. Let's stick to a simple text.

set body = ##class(%Library.DynamicObject).%New()
do body.%Set("content","Don't forget to deliver!")
do body.%Set("contentType","text")
do mytask.%Set("body",body)

Since my client is a very influential man in charge of some crucial work, we will also set the importance of this task.

do mytask.%Set("importance","high")

He has also requested to add a due date and a reminder on the task. Additionally, his dates should be easy enough to remember. The dueDateTime property of the task should also be a JSON object with both a timestamp and a time zone. The reminder will likewise be the same kind of object, so we should set the isReminderOn property to true.

set duedate = ##class(%Library.DynamicObject).%New()
do duedate.%Set("dateTime","2024-12-25T00:00:00")
do duedate.%Set("timeZone","Etc/GMT")
do mytask.%Set("dueDateTime",duedate)
set reminder = ##class(%Library.DynamicObject).%New()
do reminder.%Set("dateTime","2024-12-24T00:00:00")
do reminder.%Set("timeZone","Etc/GMT")
do mytask.%Set("reminderDateTime",reminder)
do mytask.%Set("isReminderOn","true","Boolean")

Next, we need to deal with the recurrence. The recurrence object typically contains two more objects: a pattern and a range. The pattern tells us when this task recurs, and the range suggests how long it recurs. My client’s task is annual and will continue without an end, so I guess he will be doing this forever! 

We will start with the pattern. The type of pattern can be daily, weekly, relativeMonthly, absolutelyMonthly, relativeYearly, or absoluteYearly. For the monthly and yearly options, the absolute indicates that the event will occur on the exact same date, for example, September third of every year or on the tenth of every month. Relative means a certain occurrence of a day, like the third Thursday of every month or the fourth Thursday of every November. In my client’s case, it is an absolute yearly recurrence. The interval is based on the type. If it is a yearly event, 1 means this event will repeat every year. If it were a weekly event, an interval of 1 would mean one week. We should also set the month and dayOfMonth. For other types of recurrence, you may need to set the daysOfWeek property as well. It is a collection that can contain any day of the week. However, it does not apply in our case.

set pattern = ##class(%Library.DynamicObject).%New()
do pattern.%Set("type","absoluteYearly")
do pattern.%Set("interval",1)
do pattern.%Set("month",12)
do pattern.%Set("dayOfMonth",25)

The range can have one of three types: noEnd, endDate, and numbered. Since we are going to use the noEnd type, we only need to specify a start date and the type. If we were using the endDate type, we would have to specify an endDate, and if we were employing the numbered type, we would need to specify numberOfOccurrences as a positive integer telling us how many times the event will repeat.

set range = ##class(%Library.DynamicObject).%New()
do range.%Set("type","noEnd")
do range.%Set("startDate","2024-12-25")

Then we need to add the following code to a recurrence object and later add that object to our task. 

set recurrence = ##class(%Library.DynamicObject).%New()
do recurrence.%Set("pattern",pattern)
do recurrence.%Set("range",range)
do mytask.%Set("recurrence",recurrence)

Now we are finally ready to send off our request!

do taskRequest.EntityBody.Write(mytask.%ToJSON())
do taskRequest.Post()

If we have done everything correctly, we should get an HTTP response with a Status Code of 201 telling us that the task was created. The user should be able to see it in Outlook as soon as their Outlook syncs again.

 

After my client’s big annual event, they usually have a company party. He would like me to create an event on his Outlook calendar for it. To do it, you will need to go back to Entra and give your application the Graph API permission Calendars.ReadWrite. Simply sending an event request without specifying a calendar ID will add the event to the user’s default calendar. Once again, we will create a %Net.HttpRequest with our usual setup.

set evtRequest = ##class(%Net.HttpRequest).%New()
set evtRequest.Server = "graph.microsoft.com"
set evtRequest.SSLConfiguration = "O365"
set evtRequest.Https = 1
set evtRequest.Location = "/v1.0/users/"_..UserAddress_"/calendar/events"
set evtRequest.ContentType = "application/json"
set sc = ..GetToken(.token)
do evtRequest.SetHeader("Authorization","Bearer "_token)

Next, we have to set up the body of the request. This object will contain some properties that should look very familiar to you. The subject will be a simple string with a description of the event. The body will be the same kind of object as email and task bodies. There we must specify the content type as either "html" or "text". Additionally, remember to mention the start and end dates and times with the time zone included.

set evtObj = ##class(%DynamicObject).%New()
do evtObj.%Set("subject","After Party")
set bodyObj = ##class(%DynamicObject).%New()
do bodyObj.%Set("contentType","html")
do bodyObj.%Set("content","Party after the big day! <br /><b>BRING YOUR OWN NOG!</b>")
do evtObj.%Set("body",bodyObj)
set start = ##class(%Library.DynamicObject).%New()
do start.%Set("dateTime","2024-12-26T20:00:00")
do start.%Set("timeZone","Etc/GMT")
do evtObj.%Set("start",start)
set end = ##class(%Library.DynamicObject).%New()
do end.%Set("dateTime","2024-12-27T00:00:00")
do end.%Set("timeZone","Etc/GMT")
do evtObj.%Set("end",end)

There is a reminderMinutesBeforeStart field that will set a reminder for an event.

do evtObj.%Set("reminderMinutesBeforeStart",60)

We can also add a location, which is an object that can contain a name, address, coordinates, and some contact information.

set locObj = ##class(%Library.DynamicObject).%New()
do locObj.%Set("displayName","SeasonalSpirits")
set coordObj = ##class(%Library.DynamicObject).%New()
do coordObj.%Set("latitude",90)
do coordObj.%Set("longitude",0)
do locObj.%Set("coordinates",coordObj)
do evtObj.%Set("location",locObj)

It will not be much of a party if no one comes, so we should add some attendees too. It will be the same style of email address we used before, plus a type field specifying whether the attendee is required, optional, or a resource.
 

set attendees = ##class(%Library.DynamicArray).%New()
set myatt1 = ##class(%Library.DynamicObject).%New()
do myatt1.%Set("type","required")
set myemail1 = ##class(%Library.DynamicObject).%New()
do myemail1.%Set("name","Mr C.")
do myemail1.%Set("address","youremail@domain.com")
do myatt1.%Set("emailAddress",myemail1)
do attendees.%Push(myatt1)
set myatt2 = ##class(%Library.DynamicObject).%New()
do myatt2.%Set("type","optional")
set myemail2 = ##class(%Library.DynamicObject).%New()
do myemail2.%Set("name","Mrs. C.")
do myemail2.%Set("address","youremail2@domain.com")
do myatt2.%Set("emailAddress",myemail2)
do attendees.%Push(myatt2)
do evtObj.%Set("attendees",attendees)

Finally, we are ready to write the request body and post it.

do evtRequest.EntityBody.Write(evtObj.%ToJSON())
do evtRequest.Post()

Once again, if we have done all of the abovementioned correctly, we should get an HTTP response with a status code of 201, and the user should now have the event on their default calendar.

 

That is all for now! However, it seems to me that I have just got another email from my mysterious client. It is something related to managing big and busy teams of builders, stable managers, etc. This guy’s business is really weird! I have a feeling I will be back soon, talking about Teams.  

Discussion (0)1
Log in or sign up to continue
Announcement
· May 7, 2024

Recapitulação do InterSystems Developer Community, Abril 2024

Olá e Bem-vindo à recapitulação da Comunidade de Desenvolvedores Abril 2024.
Estatísticas gerais
21 novas postages publicadas em Abril:
 12 novos artigos
 9 novos anúncios
2 novos membros ingressaram em Abril
1,049 postagens publicadas ao todo
575 membros ingressaram ao todo
Principais publicações
Principais autores do mês
Artigos
#InterSystems IRIS
 
#Caché
Depurando a Web
Por Danusa Calixto
 
#HealthShare
 
#Outro
 
Anúncios
#InterSystems IRIS
 
#Developer Community Oficial
 
#InterSystems IRIS for Health
 
#Global Masters
 
#InterSystems Oficial
 
#Portal de Aprendizagem
 
Abril, 2024Month at a GlanceInterSystems Developer Community
Discussion (0)1
Log in or sign up to continue
Announcement
· May 6, 2024

Tercer concurso de artículos técnicos en español

¡Hola Comunidad!

Llega un nuevo reto para vosotros ¡El tercer concurso de redacción de artículos técnicos de InterSystems en español ya está aquí!

🏆 3º Concurso de Artículos Técnicos en español 🏆
Descripción del concurso: Escribe un artículo en español en la Comunidad de Desarrolladores, sobre cualquier tema relacionado con la tecnología de InterSystems.

Duración: del 6 de mayo al 2 de junio de 2024.

Premios para todos los participantes: Todas las personas que publiquen un artículo en español durante la vigencia del concurso recibirán un premio.

Premio principal: LEGO Ferrari Daytona SP3 / Banco Mágico Gringotts™ - Edición para coleccionistas. 

 

¡Participa en el concurso y alcanza a cientos de usuarios! Es una de las mejores oportunidades para publicar esos consejos que has descubierto.

Premios

1. Todos los participantes ganan en el concurso de artículos técnicos de InterSystems -> Cualquier miembro de la Comunidad que participe en el concurso, recibirá un detalle de InterSystems por participar.

2. Premios de los Expertos – los artículos ganadores de esta categoría serán elegidos por expertos de InterSystems y podrán elegir en cada caso:

🥇 1er puesto: LEGO Ferrari Daytona SP3 o Banco Mágico Gringotts™ - Edición para coleccionistas. 

🥈 2do puesto: LEGO Sistema de Lanzamiento Espacial Artemis de la NASA o Chaqueta hombre Patagonia Nano Puff® Hoody.

🥉 3er puesto: Altavoz JBL Flip 6, Amazon Kindle 8G Paperwhite (Onceava generación) o Mochila Samsonite SPECTROLITE 3.0 15.6"

O como alternativa, cualquier ganador puede elegir un premio de una categoría inferior a la suya

Nota: Los premios están sujetos a cambiar si la disponibilidad en el país no permite su entrega.

3. Premio de la Comunidad de Desarrolladores – artículo con el mayor número de "Me gusta". La persona que gane, podrá elegir uno de estos premios:

🎁 Altavoz JBL Flip 6, Amazon Kindle 8G Paperwhite (Onceava generación) o Mochila Samsonite SPECTROLITE 3.0 15.6" 

Nota: cada autor solo puede ganar un premio de cada categoría (en total, un autor puede ganar dos premios: uno en la categoría Expertos y otro en la categoría de la Comunidad).

¿Quién puede participar?

Cualquier persona registrada en la Comunidad de Desarrolladores, excepto los empleados de InterSystems. Regístrate aquí en la Comunidad si aún no tienes una cuenta.

Duración del concurso

📝  Del 6 de mayo al 2 de junio: Publicación de artículos.

📝  Del 3 de junio al 9 de junio: Fase de votación. 

Publica tu(s) artículos(s) durante ese período. Los miembros de la Comunidad de Desarrolladores pueden ir votando los artículos que les gustan haciendo clic en "Me gusta" debajo de cada artículo.

Truco: Cuanto antes publiques tu(s) artículo(s), más tiempo tendrás para conseguir más votos de los Expertos y de la Comunidad.

🎉 10 de junio: Anuncio de los ganadores.

    Requisitos

    ❗️ Cualquier artículo escrito durante el período de duración del concurso y que cumpla los siguientes requisitos entrará automáticamente en la competición ❗️:

    • El artículo debe estar relacionado directa o indirectamente con la tecnología de InterSystems (características propias de los productos de InterSystems o, también, herramientas complementarias, soluciones arquitecturales, mejores prácticas de desarrollo,…).
    • El artículo debe estar escrito en español.
    • El artículo debe ser 100% nuevo (puede ser la continuación de un artículo ya publicado).
    • El artículo no puede ser una copia o traducción de otro publicado en la Comunidad de Desarrolladores en español o en otra Comunidad.
    • Tamaño del artículo: >1 000 caracteres (los enlaces no cuentan en el cálculo de caracteres).
    • Modo de participación: individual (se permite que un participante publique varios artículos).

    ¿Sobre qué se puede escribir?

    Se puede escoger cualquier tema técnico relacionado directa o indirectamente con la tecnología de InterSystems.

    🎯 BONUS:

    Los Expertos conceden 3 votos al artículo que consideran el mejor, 2 votos al 2º que consideran mejor y 1 voto al 3º que consideran mejor. Además, los artículos pueden recibir más puntos en función de los siguientes bonus:

    Nota: la decisión de los jueces es inapelable.

    1. Bonus por autor nuevo: Si es la primera vez que participas en el Concurso de Artículos Técnicos en Español, tu artículo recibirá un 1 voto extra de los Expertos.

    2. Bonus por temática: Si tu artículo está dentro de las siguientes temáticas, recibirá 2 puntos extra.

    • Uso de AI/ML/GenAI
    • Cómo aprovechar las posibilidades de la búsqueda vectorial (Vector Search)
    • Aprovechamiento de la función de almacenamiento en columnas
    • ️Uso de Python integrado
    • Uso de Cloud SQL
    • Uso de VSCode
    • Explotación de las capacidades FHIR de IRIS (SMART en FHIR 2.0, modelo de objetos FHIR, FHIR SQL Builder, Bulk FHIR,...)


    3. Vídeo bonus: si además del artículo, se acompaña con un vídeo explicativo, el candidato recibirá 4 puntos.

    4. Bonus por tutorial: Recibirás 3 puntos si el artículo tiene características de tutorial, con instrucciones paso a paso que un desarrollador pueda seguir para completar una o varias tareas específicas.


    Así que... Let's go!

    ¡Esperamos ansiosos vuestros artículos! 

    ¡Comunidad! ¡Que la fuerza os acompañe! ✨🤝

    3 Comments
    Discussion (3)1
    Log in or sign up to continue
    Article
    · May 3, 2024 6m read

    Demo: Connecting Locally to an S3 Bucket without an AWS Account

    Introduction

    Accessing Amazon S3 (Simple Storage Service) buckets programmatically is a common requirement for many applications. However, setting up and managing AWS accounts is daunting and expensive, especially for small-scale projects or local development environments. In this article, we'll explore how to overcome this hurdle by using Localstack to simulate AWS services. Localstack mimics most AWS services, meaning one can develop and test applications without incurring any costs or relying on an internet connection, which can be incredibly useful for rapid development and debugging. We used ObjectScript with embedded Python to communicate with Intersystems IRIS and AWS simultaneously. Before beginning, ensure you have Python and Docker installed on your system. When Localstack is set up and running, the bucket can be created and used. 

    Creating an S3 Bucket from ObjectScript with Embedded Python

    Now that LocalStack is running, let's create an S3 bucket programmatically. We'll use Python and the Boto3 library - a Python SDK for AWS services. Take a look at the MakeBucket method provided in the S3UUtil class. This method utilizes Boto3 to create an S3 bucket:

    ClassMethod MakeBucket(inboundfromiris As %String) As %Status [ Language = python ]
    
    {
    
        import boto3
    
        s3 = boto3.client(
    
            service_name='s3', 
    
            region_name="us-east-1", 
    
            endpoint_url='http://host.docker.internal:4566', 
        )
    
        try:
    
            s3.create_bucket(Bucket=inboundfromiris)
    
            print("Bucket created successfully")
    
            return 1
        except Exception as e:
    
            print("Error:", e)
    
            return 0
    }

    To create a bucket, you would call this method with the desired bucket name:

    status = S3UUtil.MakeBucket("mybucket")

    Uploading Objects to the Bucket from ObjectScript with Embedded Python

    Once the bucket is created, you can upload objects to it programmatically. The PutObject method demonstrates how to achieve this:

    ClassMethod PutObject(inboundfromiris As %String, objectKey As %String) As %Status [ Language = python ]
    
    {
    
        import boto3
    
        try:
    
            content = "Hello, World!".encode('utf-8')
    
            s3 = boto3.client(
    
                service_name='s3',
    
                region_name="us-east-1",
    
                endpoint_url='http://host.docker.internal:4566'
            )
    
            s3.put_object(Bucket=inboundfromiris, Key=objectKey, Body=content)
    
            print("Object uploaded successfully!")
    
            return 1
        except Exception as e:
    
            print("Error:", e)
    
            return 0
    }

    Call this method to upload an object:

    Do ##class(S3.S3UUtil).PutObject("inboundfromiris", "hello-world-test")

     

    Listing Objects in the Bucket from ObjectScript with Embedded Python

    To list objects in the bucket, you can use the FetchBucket method:

    ClassMethod FetchBucket(inboundfromiris As %String) As %Status [ Language = python ]
    
    {
    
        import boto3
    
        s3 = boto3.client(
    
            service_name='s3', 
    
            region_name="us-east-1", 
    
            endpoint_url='http://host.docker.internal:4566', 
        )
    
        try:
    
            response = s3.list_objects(Bucket=inboundfromiris)
    
            if 'Contents' in response:
    
                print("Objects in bucket", inboundfromiris)
    
                for obj in response['Contents']:
    
                    print(obj['Key'])
    
                return 1
            else:
    
                print("Error: Bucket is empty or does not exist")
    
                return 0
        except Exception as e:
    
            print("Error:", e)
    
            return 0
    }

    Call the FetchBucket method to list objects from the bucket:

    do ##class(S3.S3UUtil).FetchBucket("inboundfromiris")


     

    Retrieving Objects from the Bucket from ObjectScript with Embedded Python

    Finally, to retrieve objects from the bucket, you can use the PullObjectFromBucket method:

    ClassMethod PullObjectFromBucket(inboundfromiris As %String, objectKey As %String) As %Status [ Language = python ]
    
    {
    
        import boto3
    
        def pull_object_from_bucket(bucket_name, object_key):
    
            try:
    
                s3 = boto3.client(
    
                    service_name='s3', 
    
                    region_name="us-east-1", 
    
                    endpoint_url='http://host.docker.internal:4566', 
                )
    
                obj_response = s3.get_object(Bucket=bucket_name, Key=object_key)
    
                content = obj_response['Body'].read().decode('utf-8')
    
                print("Content of object with key '", object_key, "':", content)
    
                return True
    
            except Exception as e:
    
                print("Error:", e)
    
                return False
    
        pull_object_from_bucket(inboundfromiris, objectKey)
    
    }

    Call this method:

    Do ##class(DQS.CloudUtils.S3.S3UUtil).PullObjectFromBucket("inboundfromiris", "hello-world-test")

     

    The discussion here is just the beginning, as it's clear there's plenty more ground to cover. I invite readers to dive deeper into this subject and share their insights. Let's keep the conversation going and continue advancing our understanding of this topic.

    I'm eager to hear thoughts and contributions.

    Discussion (0)1
    Log in or sign up to continue
    Announcement
    · May 3, 2024

    VS Code release April 2024 (version 1.89)

     

    Visual Studio Code releases new updates every month with new features and bug fixes, and the April 2024 release is now available. 

    Version 1.89 includes:

    The release also includes contributions from our very own @John Murray through pull requests that address open issues. 

    Find out more about these features in the release notes here > https://code.visualstudio.com/updates/v1_89

    For those with VS Code, your environment should auto-update. You can manually check for updates by running Help > Check for Updates on Linux and Windows or running Code > Check for Updates on macOS.

    If you're thinking about migrating from Studio to VS Code but need some help, take a look at the training courses George James Software offers > https://georgejames.com/migration-from-studio/

    Discussion (0)2
    Log in or sign up to continue