Update User Profile Picture across all Office 365 apps and Skype for Business using Power Apps, SharePoint, Graph Api & Azure Web Jobs – Part 2

There was a requirement that any user in the organization can update his/her profile picture across all Office 365 apps – the approach was quite straight forward: use Power Apps, save the user data in SharePoint and use Power Automate HTTP connector to do a POST using a Graph API endpoint – however, it seems now that the Power Automate HTTP is a Premium connector which becomes overpriced as all the users in the organization are going to use it, therefore used Azure Web Jobs which did the job well. Note that WebJobs provide an easy way to run scripts or programs as background processes in the context of your app.

Let’s get started.

This is the continuation of Part 1 (Power Apps & SharePoint)

B. Azure Web Jobs & Grap API

B1. Create an Azure Web App to host your code.

– Under Project Details, select Subscription and Resource Group.

– Under Instance Details, provide a descriptive Name, and set as follows – Publish: Code, Runtime stack: latest .Net version, Operating System: Window.

B2. Download the publish profile

Once the Web App is provisionned, Get the publish profile from the Overview menu.

B3. Create a Console Application using Visual Studio 2017 or later.

Once created, right-click on the Project and click Publish as Azure WebJob… to create the Web Job within Visual Studio.

– Provide a descriptive WebJob name.

– Select a WebJob run mode: Run Continuously or Run on Demand. (There is a Scheduled run option as well which we will see in the section B5).

B4. On the next Publish screen, Import the profile settings which you saved in steps B2.

B5. Change the Webjob run mode to Scheduled.

Open the webjob-publish-settings.json within Properties and change the code as follows:

Please note that Scheduled WebJob will be executed based on provided CRON expression. Click here to learn more about CRON Expression.

{
  "$schema": "http://schemastore.org/schemas/json/webjob-publish-settings.json",
  "webJobName": "WebJobUpdateProfilePicture",
  "runMode": "Scheduled",
  "schedule": "0 */1 * * * *"
}

B6. Replace Program class with the below code.

– The Program class filters all the SharePoint items which are not yet updated, based on the IsUpdated column.

– Thereafter, ProcessUpdateUserPicture class contains all Graph API (C#) related functions to updated the image to Office 365.

public class Program
    {
        #region Variables
        static StringBuilder logMessage;
        static string serviceAccount = "user@myorg.com";
        static string serviceAccountPWD = "myPassword";
        static string listName = "UsersProfileData";
        static string COLUMN_IS_UPDATED = "IsUpdated";
        static string COLUMN_EMPLOYEE_AS_TEXT = "EmployeePhotoAsText";
        static string COLUMN_TITLE = "Title";
        static string COLUMN_EMPLOYEE_UPN = "EmployeeUPN";
        #endregion

        /// <summary>
        /// Main Mthod
        /// </summary>
        /// <param name="args"></param>
        static void Main(string[] args)
        {
            try
            {
                Console.WriteLine("Web Job started");
                ProcessProfileUpdation("url_of_your_sharepointsite");
                Console.WriteLine("Web Job Completed");
            }
            catch (Exception ex)
            {
                logMessage.AppendLine(string.Format("Exception occured in Main - {0}", ex.ToString()));
            }
            finally
            {
            }

        }

        /// <summary>
        /// Use to process profile updation for all entered users photos
        /// </summary>
        /// <param name="url"></param>
        /// <returns></returns>
        public static string ProcessProfileUpdation(string url)
        {
            try
            {
                //Authenticate
                using (var ctx = new Microsoft.SharePoint.Client.ClientContext(url))
                {
                    var passWord = new SecureString();
                    foreach (char c in serviceAccountPWD.ToCharArray()) passWord.AppendChar(c);
                    ctx.Credentials = new SharePointOnlineCredentials(serviceAccount, passWord);

                    UpdateProfilePhoto(ctx);
                }
            }
            catch (Exception ex)
            {
                Console.WriteLine(string.Format("Exception occured in Main - {0}", ex.ToString()));
            }
            return string.Empty;
        }

        /// <summary>
        /// Method used to update profile photo
        /// </summary>
        /// <param name="ctx"></param>
        private static void UpdateProfilePhoto(ClientContext ctx)
        {
            try
            {
                //Get  List
                List oList = ctx.Web.Lists.GetByTitle(listName);

                //Filter the ones which are not yet Updated
                CamlQuery camlQuery = new CamlQuery();
                camlQuery.ViewXml = @"<View><Query><Where><Eq><FieldRef Name='IsUpdated' /><Value Type='Boolean'>0</Value></Eq></Where></Query></View>";

                ListItemCollection collListItem = oList.GetItems(camlQuery);

                ctx.Load(collListItem);
                ctx.ExecuteQuery();

                foreach (ListItem oListItem in collListItem)
                {
                    bool boolValue = Convert.ToBoolean(oListItem[COLUMN_IS_UPDATED]);

                    if (boolValue == false)
                    {
                        string employeePhotoAsText = oListItem[COLUMN_EMPLOYEE_AS_TEXT].ToString();
                        string employeeName = oListItem[COLUMN_TITLE].ToString();
                        string employeeUPN = oListItem[COLUMN_EMPLOYEE_UPN].ToString();

                        var base64Data = Regex.Match(employeePhotoAsText, @"data:image/(?<type>.+?),(?<data>.+)").Groups["data"].Value;
                        byte[] bytes = Convert.FromBase64String(base64Data);
                        System.IO.Stream imageStream = new MemoryStream(bytes);

                        Stream oldImageStream = new MemoryStream();
                        bool isUpdated = ProcessUpdateUserPicture.StartUpdation(ctx, employeeUPN, imageStream, out oldImageStream, logMessage);

                        Console.WriteLine(string.Format("ID: {0} \nEmployeeUPN: {1}", oListItem.Id, employeeUPN));
                        if (isUpdated == true)
                        {
                            oListItem["IsUpdated"] = true;
                            oListItem.Update();
                            ctx.ExecuteQuery();
                        }
                        oListItem.Update();
                        ctx.ExecuteQuery();
                    }
                }
            }
            catch (Exception ex)
            {
                logMessage.AppendLine("Exception occured at UpdateProfilePhoto: - " + ex.ToString());
                Console.WriteLine("Exception occured at UpdateProfilePhoto: - " + ex.ToString());
            }
        }
    }

public class ProcessUpdateUserPicture
    {
        public static string accessToken = null;

        /// <summary>
        /// Use to update profile photo for each user
        /// </summary>
        /// <param name="clientContext">clientContext</param>
        /// <param name="userId">employeeUPN</param>
        /// <param name="streamImage">IMage stream</param>
        /// <param name="logMessage">logMessage</param>
        /// <returns></returns>
        public static bool StartUpdation(ClientContext clientContext, string userId, Stream streamImage, out Stream previousPhoto, StringBuilder logMessage)
        {
            bool isUpdated = false;
            previousPhoto = new MemoryStream();
            try
            {
                logMessage.AppendLine("Calling GetAuth()");
                try
                {
                    GraphServiceClient graphService = GetAuth(clientContext, logMessage);
                    logMessage.AppendLine("After Calling GetAuth()");
                    var result = graphService.Users[userId].Photo.Content.Request().PutAsync(streamImage); //users/{1}/photo/$value
                    do
                    {
                        logMessage.AppendLine(string.Format("Result status: {0}", result.Status));
                        Console.WriteLine("Result status: {0}", result.Status);
                        Thread.Sleep(20000);
                    } while (result.Status == System.Threading.Tasks.TaskStatus.WaitingForActivation);
                    if (result.IsCompleted == true)
                    {
                        if (result.Status == System.Threading.Tasks.TaskStatus.RanToCompletion)
                        {
                            isUpdated = true;
                            Console.WriteLine(string.Format("Profile updated for - {0} successfully", userId));
                            logMessage.AppendLine(string.Format("Profile updated for - {0} successfully", userId));

                        }
                        else
                        {
                            logMessage.AppendLine(string.Format("Profile process failed for - {0} \nException - {0}", userId, result.Exception.InnerException.Message));
                            Console.WriteLine(string.Format("Profile process failed for - {0} \nException - {0}", userId, result.Exception.InnerException.Message));
                        }
                    }
                }
                catch (Microsoft.Graph.ServiceException svcEx)
                {

                    var additionalData = svcEx.Error.AdditionalData;
                    logMessage.AppendLine(string.Format("Microsoft.Graph.ServiceException svcEx - {0}", additionalData["details"].ToString()));
                }

            }
            catch (Exception ex)
            {
                logMessage.AppendLine(ex.ToString());
                Console.WriteLine(ex.ToString());
            }

            return isUpdated;
        }

       /// <summary>
        /// Access token for Graph API call
        /// </summary>
        /// <param name="logMessage"></param>
        /// <returns></returns>
        private static string GetAccessToken(StringBuilder logMessage)
        {
            try
            {
                Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext authContext = new Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationContext(Globals.AuthorityUrl, true);
                Microsoft.IdentityModel.Clients.ActiveDirectory.ClientCredential creds = new Microsoft.IdentityModel.Clients.ActiveDirectory.ClientCredential(Globals.ClentId, Globals.ClientSecret);
                Microsoft.IdentityModel.Clients.ActiveDirectory.AuthenticationResult authenticationResult = authContext.AcquireTokenAsync(Globals.GraphResourceUrl, creds).Result;
                return authenticationResult.AccessToken;

            }
            catch (Exception ex)
            {
                logMessage.AppendLine(string.Format("Exception occured at GetAccessToken - {0}", ex.ToString()));
            }

            return null;
        }

        /// <summary>
        /// Method used for Graph API call
        /// </summary>
        /// <param name="clientContext"></param>
        /// <param name="logMessage"></param>
        /// <returns></returns>
        private static GraphServiceClient GetAuth(ClientContext clientContext, StringBuilder logMessage)
        {
            try
            {
                accessToken = GetAccessToken(logMessage);
                GraphServiceClient graphClient = GetGraphClient(clientContext, accessToken, logMessage);
                return graphClient;


            }
            catch (Exception ex)

            {
                logMessage.AppendLine(string.Format("Exception occured at GetAuth - {0}", ex.ToString()));
                //CustomLogs.LogError(clientContext, string.Format("Exception from GetAuth"), ex);
            }
            return null;
        }

        /// <summary>
        /// Method used for Graph API call
        /// </summary>
        /// <param name="clientContext"></param>
        /// <param name="graphToken"></param>
        /// <param name="logMessage"></param>
        /// <returns></returns>
        public static GraphServiceClient GetGraphClient(ClientContext clientContext, string graphToken, StringBuilder logMessage)
        {
            try
            {
                DelegateAuthenticationProvider authenticationProvider = new DelegateAuthenticationProvider(
                (requestMessage) =>
                {
                    requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", graphToken);
                    return Task.FromResult(0);
                });
                return new GraphServiceClient(authenticationProvider);
            }
            catch (Exception ex)
            {
                logMessage.AppendLine(string.Format("Exception from GetGraphClient - {0} ", ex.ToString()));
            }
            return null;
        }

    }

B7. Deploy the code to the Azure Web App.

In Visual Studio, right-click on the Project, choose to Publish as Azure WebJob then click on Publish to deploy the code.

Once successfully deployed, in the Azure Web App, the job entry must be present within WebJobs – the job is run to scheduled every 15mn in this case.

To ensure the job has run successfuly, click on Logs to see more details.

Cheers!

Update User Profile Picture across all Office 365 apps and Skype for Business using Power Apps, SharePoint, Graph Api & Azure Web Jobs – Part 1

There was a requirement that any user in the organization can update his/her profile picture across all Office 365 apps – the approach was quite straight forward: use Power Apps, save the user data in SharePoint and use Power Automate HTTP connector to do a POST using a Graph API endpoint – however, it seems now that the Power Automate HTTP is a Premium connector which becomes overpriced as all the users in the organization are going to use it, therefore used Azure Web Jobs which did the job well. Note that WebJobs provide an easy way to run scripts or programs as background processes in the context of your app.

Let’s get started.

A. Power Apps & SharePoint

A1. Create a new blank app with a Tablet layout preferably.

A2. Insert an Add picture control to upload the image.

Set the OnSelect property as follows

Set(CapturedPic, UploadedImage1.Image);
Set(vImg,JSON(UploadedImage1.Image,JSONFormat.IncludeBinaryData));
If(Value(Text(Len(vImg) * .00000073,”[$-en-US]##.##”)) >= 4, Notify(“Please choose an image less than 4 Mb”),””);

A3. Insert an Image control to validate the uploaded image.

In the Image property, set it as CapturedPic

A4. Add some labels to make the app more descriptive as follows:

It is preferable to show the image size in a label, the reason being is that Graph API support only an image size less than 4Mb, to show the size set the Text as: “Image size: ” & Text(Len(vImg)*.00000073,”[$-en-US]##.##”) & ” Mb”

A5. Now comes the submission of data to SharePoint

On the OnSelect button, add the following code

//Used for delegation purpose
ClearCollect(
    userImage,
    imgCapture.Image
);

//Checking whether the entry of the same user exists in the list
ClearCollect(
    IsEntryExists,
    Filter(
        UsersProfileData,
        EmployeeUPN = CurrentUser.Email
    )
);

//If so then Update otherwise Add
If(
    CountRows(IsEntryExists) > 0,
    Patch(
        UsersProfileData,
        LookUp(
            UsersProfileData,
            EmployeeUPN = CurrentUser.Email
        ),
        {
            Title: User().FullName,
            EmployeeDisplayName: CurrentUser.FullName,
            EmployeeUPN: CurrentUser.Email,
            EmployeeMail: Office365Users.MyProfile().Mail,
            EmployeePhotoApproval: If(
                chkApprove.Value = true,
                "yes",
                "no"
            ),
            IsUpdated: false,
            UserLanguage:Lower(Language()),
            EmployeePhotoAsBase64: First(userImage).Url,
            EmployeePhotoAsText: Substitute(
                JSON(
                    imgCapture.Image,
                    JSONFormat.IncludeBinaryData
                ),
                """",
                ""
            )
        }
    ),
    Patch(
        UsersProfileData,
        Defaults(UsersProfileData),
        {
            Title: User().FullName,
            EmployeeDisplayName: CurrentUser.FullName,
            EmployeeUPN: CurrentUser.Email,
            EmployeeMail: Office365Users.MyProfile().Mail,
            EmployeePhotoApproval: If(
                chkApprove.Value = true,
                "yes",
                "no"
            ),
            IsUpdated: false,
            IsDeleted: false,
            UserLanguage:Lower(Language()),
            EmployeePhotoAsBase64: First(userImage).Url,
            EmployeePhotoAsText: Substitute(
                JSON(
                    imgCapture.Image,
                    JSONFormat.IncludeBinaryData
                ),
                """",
                ""
            )
        }
    )
);

//Reset all controls and notify
Set(
    CapturedPic,
    Blank()
);
Reset(chkApprove);
Reset(AddMediaButton1);
Notify("Photo submitted successfully - please check after sometimes in Delve portal.");

This is the structure of the SharePoint list

The application should like as follows

Next: Part 2 – Azure Web Jobs & Grap API

Vaccine for all!

Getting the vaccine is not an easy task – either you go to a vaccination center early to get a token or if lucky try to get an available slot in the Cowin site/Aarogya Setu mobile app – this has been the same experience got by friends, relatives, and colleagues across the country.

Therefore using the Co-WIN Public APIs, I decided to provide this web application to help all my fellow citizens to get vaccinated!

Let’s get vaccinated!

This web application looks for the vaccine slot availability in your respective District by selecting the age & available dose criteria – using the Co-WIN Public APIs, for more details about the API, click here

Please note that this web application does NOT book any slot on your behalf whatsoever – it only provides valuable information to help the citizen to select the available center at that point in time.

How it works

The program run in an interval to query the provided Co-WIN Public APIs to look for an available center in your respective District, taking into consideration the age and available dose. 

Once the program finds an available center, an email will be sent at the registered email address a complete report with all details which shows all the available centers along with the available dose at that point in time. 

It is important to mention that upon receiving the report, it is highly recommended to book the slots on the cowin.gov.in website or using the Aarogya Setu mobile app.

https://vaccineforall.azurewebsites.net/ (formerly vaccineforall.co.in)

Coronavirus Vaccine: When should you get vaccinated after recovering from  COVID-19?

Digital Transformation – 5 steps to success

In the year 1995, I remember helping my father identifying and classifying all information-bearing documents, whether they were in the form of hard-copy output or computer 5 1/4″ floppy drive – my dad being in the Travel Industry in Madagascar, we had to process pile of files, it was like running out of space on top of his desk and having to process one pile of files at a time in order to free up space for another pile of files – this was a long and tedious task.

To automate this task, I had developed a small application using DBASE III running on a Windows 286 with an MS-DOS Version 5 which was my first computer – the program design was quite simple, I had to enter all the customer data along with one important attribute “the File Number” – the intent was that when someone searching for a particular string, instantly the app shows the file number containing the respective data – as these efforts started to bear fruit, the happiness and satisfaction on his face were palpable; Wish he could have seen today’s digital era where humans are engaging in smarter experiences through technological innovation.

So what is Digital Transformation?

simply put, use technological innovation to convert your manual tasks or create new business processes.

According to Schumpeter, the process of technological change in a free market consists of three parts: invention (conceiving a new idea or process), innovation (arranging the economic requirements for implementing an invention), and diffusion (whereby people observing the new discovery adopt or imitate it) – in layman’s terms, this means Schumpeter argued that anyone seeking profits must innovate.

This is true for all the companies who have adopted and transformed their services or business through technology – for an instance with this COVID pandemic, today with a majority of individuals working remotely, employee experience of digital technology has gone from “nice to have” to “the only way work gets done” – this is a revolution of the introduction of a new technology that creates entirely new ways of serving existing needs and significantly disrupts an existing industry. On the other hand, companies that are not innovating may not be disrupted however does guarantee a poor outcome and may be defeated by the competitors.

What is the 5-key success of Digital Transformation?

Culture

Everything begins with trust! Digital Transformation use technological innovation to replace your manual tasks or create new business processes however we have seen traditional organization/people who have a strong culture resists to adopt these new changes.

Companies can overcome these barriers by inculcating trust, communicate to their workforce their digital transformation strategy then create opportunities for dialog. Upskilling and re-skilling is crucial to ensure adaptability and employability in the transition times.

Strategy

One of the important pillars of the Digital Transformation success is a strong and determined leadership  which is required for the development and implementation of the strategy. A key strategy is to focus primarily on improving Customer Experience which is possible by implementing digital business models and services, let us see some of the model types:

  • Marketplace model: the biggest ecommerce Amazon would have no business without internet, this is amongst the widely used business models.
  • Free model: Build great products and released them for free (Trial or Basic lifetime features) once the customers get accustomed then monetization would not be an issue. Many companies have adopted this model for their products.
  • Subscription-based model: Netflix and Amazon Prime have built an amazing base of loyal customers which guarantee a continuous revenue over time.
  • On-Demand model: this model is based on supply and model, it has two sided players, for example in Uber’s case this model works as soon as the drivers (supply) are  offering their services to the riders (demand).

Upskilling

Invest more in the upskilling of the workforce, employees are the face and heart of any organization – in this process of Digital Transformation, it is imperative to upskill or reskill otherwise you might fail to meet the customer’s expectations.

Continuous Innovation

It is in human nature to love everything new! Innovation increases your chances to react to changes and discover new opportunities which become the key for survival. Take Apple as an example which has done a continuous innovations such as the iPod, iTunes, iPhone, and iPad.

Smarter Customer Experience

An improved customer experience is a significant factor in efficiency, profitability and brand equity which must be the result of your Digital Transformation Strategy. I cite from a  global telecommunications EY’s study “Automation and efficiency improvement of all processes will serve customer experience.”

Get started today

The intent should be to nurtures lives through innovation and technology. This vision is going to transform your business and tap into new and exciting opportunities globally.