private void RegisterAddingWebHookEvent(ClientContext cc) { btnCreate.Click += async(s, args) => { // Hookup event to capture access token cc.ExecutingWebRequest += Cc_ExecutingWebRequest; var lists = cc.Web.Lists; Guid listId = new Guid(ListDropDown.SelectedItem.Text.Split(new string[] { "||" }, StringSplitOptions.None)[1]); IEnumerable <List> sharePointLists = cc.LoadQuery <List>(lists.Where(lst => lst.Id == listId)); cc.Load(cc.Web, w => w.Url); cc.ExecuteQueryRetry(); WebHookManager webHookManager = new WebHookManager(); var res = await webHookManager.AddListWebHookAsync(cc.Web.Url, listId.ToString(), "https://pnpwebhooksdemo.azurewebsites.net/api/webhooks", this.accessToken); // persist the latest changetoken of the list when we create a new webhook. This allows use to only grab the changes as of web hook creation when the first notification comes in using (SharePointWebHooks dbContext = new SharePointWebHooks("pnpwebhooksdemoEntities")) { dbContext.ListWebHooks.Add(new ListWebHook() { Id = new Guid(res.Id), StartingUrl = cc.Web.Url, ListId = listId, LastChangeToken = sharePointLists.FirstOrDefault().CurrentChangeToken.StringValue, }); var saveResult = await dbContext.SaveChangesAsync(); } }; }
private async Task ReactToWebHookDeletion() { string target = Request["__EVENTTARGET"]; if (target == "deletewebhook") { using (var cc = spContext.CreateAppOnlyClientContextForSPAppWeb()) { string[] parameters = Request["__EVENTARGUMENT"].Split(new string[] { "||" }, StringSplitOptions.None); string id = parameters[0]; string listId = parameters[1]; // Hookup event to capture access token cc.ExecutingWebRequest += Cc_ExecutingWebRequest; // Just load the Url property to trigger the ExecutingWebRequest event handler to fire cc.Load(cc.Web, w => w.Url); cc.ExecuteQueryRetry(); WebHookManager webHookManager = new WebHookManager(); // delete the web hook if (await webHookManager.DeleteListWebHookAsync(cc.Web.Url, listId, id, this.accessToken)) { using (SharePointWebHooks dbContext = new SharePointWebHooks()) { var webHookRow = await dbContext.ListWebHooks.FindAsync(new Guid(id)); if (webHookRow != null) { dbContext.ListWebHooks.Remove(webHookRow); var saveResult = await dbContext.SaveChangesAsync(); } } } } } }
/// <summary> /// Processes a received notification. This typically is triggered via an Azure Web Job that reads the Azure storage queue /// </summary> /// <param name="notification">Notification to process</param> public void ProcessNotification(NotificationModel notification) { ClientContext cc = null; try { string url = null; Guid listId = new Guid(notification.Resource); Guid id = new Guid(notification.SubscriptionId); // grab last change token from database if possible using (SharePointWebHooks dbContext = new SharePointWebHooks("pnpwebhooksdemoEntities")) { var listWebHookRow = dbContext.ListWebHooks.Find(id); #region Setup an app-only client context AuthenticationManager am = new AuthenticationManager(); string startingUrl = WebConfigurationManager.AppSettings["TenantName"]; url = String.Format("https://{0}{1}", startingUrl, notification.SiteUrl); string realm = TokenHelper.GetRealmFromTargetUrl(new Uri(url)); if (listWebHookRow != null) { startingUrl = listWebHookRow.StartingUrl; url = string.Format(startingUrl + notification.SiteUrl); } string clientId = WebConfigurationManager.AppSettings["ClientId"]; string clientSecret = WebConfigurationManager.AppSettings["ClientSecret"]; if (new Uri(url).DnsSafeHost.Contains("spoppe.com")) { cc = am.GetAppOnlyAuthenticatedContext(url, realm, clientId, clientSecret, acsHostUrl: "windows-ppe.net", globalEndPointPrefix: "login"); } else { cc = am.GetAppOnlyAuthenticatedContext(url, clientId, clientSecret); } cc.ExecutingWebRequest += Cc_ExecutingWebRequest; #endregion #region Grab the list for which the web hook was triggered ListCollection lists = cc.Web.Lists; var changeList = cc.Web.Lists.GetById(listId); var listItems = changeList.GetItems(new Microsoft.SharePoint.Client.CamlQuery()); cc.Load(changeList, lst => lst.Title, lst => lst.DefaultViewUrl); cc.Load(listItems, i => i.Include(item => item.DisplayName, item => item.Id)); cc.ExecuteQuery(); if (changeList == null) { // list has been deleted inbetween the event being fired and the event being processed return; } #endregion #region Grab the list used to write the web hook history // Ensure reference to the history list, create when not available List historyList = cc.Web.GetListByTitle("WebHookHistory"); if (historyList == null) { historyList = cc.Web.CreateList(ListTemplateType.GenericList, "WebHookHistory", false); } #endregion #region Grab the list changes and do something with them // grab the changes to the provided list using the GetChanges method // on the list. Only request Item changes as that's what's supported via // the list web hooks ChangeQuery changeQuery = new ChangeQuery(false, true); changeQuery.Item = true; changeQuery.FetchLimit = 1000; // Max value is 2000, default = 1000 ChangeToken lastChangeToken = null; if (listWebHookRow != null) { lastChangeToken = new ChangeToken(); lastChangeToken.StringValue = listWebHookRow.LastChangeToken; } // Start pulling down the changes bool allChangesRead = false; do { // should not occur anymore now that we record the starting change token at // subscription creation time, but it's a safety net if (lastChangeToken == null) { lastChangeToken = new ChangeToken(); // See https://blogs.technet.microsoft.com/stefan_gossner/2009/12/04/content-deployment-the-complete-guide-part-7-change-token-basics/ lastChangeToken.StringValue = string.Format("1;3;{0};{1};-1", notification.Resource, DateTime.Now.AddMinutes(-5).ToUniversalTime().Ticks.ToString()); } // Assign the change token to the query...this determines from what point in // time we'll receive changes changeQuery.ChangeTokenStart = lastChangeToken; // Execute the change query var changes = changeList.GetChanges(changeQuery); cc.Load(changes); cc.ExecuteQueryRetry(); if (changes.Count > 0) { foreach (Change change in changes) { lastChangeToken = change.ChangeToken; if (change is ChangeItem) { // do "work" with the found change DoWork(cc, changeList, historyList, change); } } // We potentially can have a lot of changes so be prepared to repeat the // change query in batches of 'FetchLimit' untill we've received all changes if (changes.Count < changeQuery.FetchLimit) { allChangesRead = true; } } else { allChangesRead = true; } // Are we done? } while (allChangesRead == false); // Persist the last used changetoken as we'll start from that one // when the next event hits our service if (listWebHookRow != null) { // Only persist when there's a change in the change token if (!listWebHookRow.LastChangeToken.Equals(lastChangeToken.StringValue, StringComparison.InvariantCultureIgnoreCase)) { listWebHookRow.LastChangeToken = lastChangeToken.StringValue; dbContext.SaveChanges(); } } else { // should not occur anymore now that we record the starting change token at // subscription creation time, but it's a safety net dbContext.ListWebHooks.Add(new ListWebHook() { Id = id, ListId = listId, LastChangeToken = lastChangeToken.StringValue, }); dbContext.SaveChanges(); } } #endregion #region "Update" the web hook expiration date when needed // Optionally add logic to "update" the expirationdatetime of the web hook // If the web hook is about to expire within the coming 5 days then prolong it if (notification.ExpirationDateTime.AddDays(-5) < DateTime.Now) { WebHookManager webHookManager = new WebHookManager(); Task <bool> updateResult = Task.WhenAny( webHookManager.UpdateListWebHookAsync( url, listId.ToString(), notification.SubscriptionId, WebConfigurationManager.AppSettings["WebHookEndPoint"], DateTime.Now.AddMonths(3), this.accessToken) ).Result; if (updateResult.Result == false) { throw new Exception(String.Format("The expiration date of web hook {0} with endpoint {1} could not be updated", notification.SubscriptionId, WebConfigurationManager.AppSettings["WebHookEndPoint"])); } } #endregion } catch (Exception ex) { // Log error Console.WriteLine(ex.ToString()); } finally { if (cc != null) { cc.Dispose(); } } }
private async Task ExecuteWebHooksLogic() { using (var cc = spContext.CreateAppOnlyClientContextForSPAppWeb()) { cc.ExecutingWebRequest += Cc_ExecutingWebRequest; var lists = cc.Web.Lists; cc.Load(cc.Web, w => w.Url); cc.Load(lists, l => l.Include(p => p.Title, p => p.Id, p => p.Hidden)); cc.ExecuteQueryRetry(); WebHookManager webHookManager = new WebHookManager(); // Grab the current lists List <SharePointList> modelLists = new List <SharePointList>(); List <SubscriptionModel> webHooks = new List <SubscriptionModel>(); foreach (var list in lists) { if (!list.Hidden) { modelLists.Add(new SharePointList() { Title = list.Title, Id = list.Id }); var existingWebHooks = await webHookManager.GetListWebHooksAsync(cc.Web.Url, list.Id.ToString(), this.accessToken); if (existingWebHooks.Value.Count > 0) { foreach (var existingWebHook in existingWebHooks.Value) { webHooks.Add(existingWebHook); } } } } SharePointSiteModel sharePointSiteModel = new SharePointSiteModel(); sharePointSiteModel.Lists = modelLists; sharePointSiteModel.WebHooks = webHooks; sharePointSiteModel.SelectedSharePointList = modelLists[0].Id; phWebHookTable.Controls.Clear(); if (sharePointSiteModel.WebHooks.Count() == 0) { phWebHookTable.Controls.Add(new Literal() { Text = "No web hooks..." }); } else { StringBuilder sb = new StringBuilder(); sb.Append("<table><tr><th>Actions</th><th>ID</th><th>List name</th><th>Notification URL</th><th>Expiration time</th></tr>"); foreach (var webHook in sharePointSiteModel.WebHooks) { var list = sharePointSiteModel.Lists.Where(f => f.Id == new Guid(webHook.Resource)).FirstOrDefault(); string listName = ""; if (list != null) { listName = String.Format("{0} - {1}", list.Title, webHook.Resource); } sb.Append($"<tr>"); sb.Append($"<td><a href='javascript:__doPostBack(\"deletewebhook\",\"{webHook.Id}||{list.Id.ToString("D")}\");'>Delete</a></td>"); sb.Append($"<td>{webHook.Id}</td>"); sb.Append($"<td>{listName}</td>"); sb.Append($"<td>{webHook.NotificationUrl}</td>"); sb.Append($"<td>{webHook.ExpirationDateTime}</td>"); sb.Append($"</tr>"); } sb.Append("</table>"); phWebHookTable.Controls.Add(new Literal() { Text = sb.ToString() }); } ListDropDown.DataSource = from l in sharePointSiteModel.Lists select new System.Web.UI.WebControls.ListItem() { Value = l.Id.ToString("D"), Text = l.Title + "||" + l.Id.ToString("D") }; ListDropDown.DataBind(); } }