public async Task Re_QueueSendNotificationWithDelayTest() { // Arrange // SendNotificationThrottled var sendFunctionInstance = this.GetSendFunction(); string data = "{\"NotificationId\":\"notificationId\",\"RecipientData\": {\"RecipientId\" : \"TestResp\", \"UserData\": { \"UserId\" : \"userId\",\"ConversationId\":\"conversationId\",\"UserType\":\"Member\"}}}"; SendQueueMessageContent messageContent = JsonConvert.DeserializeObject <SendQueueMessageContent>(data); this.notificationService .Setup(x => x.IsPendingNotification(It.IsAny <SendQueueMessageContent>())) .ReturnsAsync(true); this.notificationService .Setup(x => x.UpdateSentNotification(It.IsAny <string>(), It.IsAny <string>(), It.IsAny <int>(), It.IsAny <int>(), It.IsAny <string>(), It.IsAny <string>())) .Returns(Task.CompletedTask); // mocking throttled. this.notificationService.Setup(x => x.IsSendNotificationThrottled()).ReturnsAsync(true); this.sendQueue.Setup(x => x.SendDelayedAsync(It.IsAny <SendQueueMessageContent>(), It.IsAny <double>())).Returns(Task.CompletedTask); // Act Func <Task> task = async() => await sendFunctionInstance.Run(data, this.deliveryCount, this.dateTime, string.Empty, this.logger.Object, new ExecutionContext()); // Assert await task.Should().NotThrowAsync <NullReferenceException>(); this.sendQueue.Verify(x => x.SendDelayedAsync(It.IsAny <SendQueueMessageContent>(), It.IsAny <double>()), Times.Once); }
/// <inheritdoc/> public async Task <bool> IsPendingNotification(SendQueueMessageContent message) { var recipient = message?.RecipientData; if (string.IsNullOrWhiteSpace(recipient?.RecipientId)) { throw new InvalidOperationException("Recipient id is not set."); } // Check notification status for the recipient. var notification = await this.sentNotificationDataRepository.GetAsync( partitionKey : message.NotificationId, rowKey : message.RecipientData.RecipientId); // To avoid sending duplicate messages, we check if the Status code is either of the following: // 1. InitializationStatusCode: this means the notification has not been attempted to be sent to this recipient. // 2. FaultedAndRetryingStatusCode: this means the Azure Function previously attempted to send the notification // to this recipient but failed and should be retried. if (notification?.StatusCode == SentNotificationDataEntity.InitializationStatusCode || notification?.StatusCode == SentNotificationDataEntity.FaultedAndRetryingStatusCode) { return(true); } return(false); }
private async Task <IMessageActivity> GetMessageActivity(SendQueueMessageContent message) { var notification = await this.notificationRepo.GetAsync( NotificationDataTableNames.SendingNotificationsPartition, message.NotificationId); dynamic counterJson = JObject.Parse(notification.Content); counterJson.actions[0].Replace( JObject.FromObject( new { type = counterJson.actions[0].type, url = counterJson.actions[0].url + "¬ificationid=" + message.NotificationId + "&recipientid=" + message.RecipientData.RecipientId, title = counterJson.actions[0].title, })); var adaptiveCardAttachment = new Attachment() { ContentType = AdaptiveCardContentType, Content = counterJson, }; return(MessageFactory.Attachment(adaptiveCardAttachment)); }
/// <summary> /// Send a preview of a draft notification. /// </summary> /// <param name="draftNotificationEntity">Draft notification entity.</param> /// <param name="teamDataEntity">The team data entity.</param> /// <param name="teamsChannelId">The Teams channel id.</param> /// <returns>It returns HttpStatusCode.OK, if this method triggers the bot service to send the adaptive card successfully. /// It returns HttpStatusCode.TooManyRequests, if the bot service throttled the request to send the adaptive card.</returns> public async Task <HttpStatusCode> SendPreview(NotificationDataEntity draftNotificationEntity, TeamDataEntity teamDataEntity, string teamsChannelId) { if (draftNotificationEntity == null) { throw new ArgumentException("Null draft notification entity."); } if (teamDataEntity == null) { throw new ArgumentException("Null team data entity."); } if (string.IsNullOrWhiteSpace(teamsChannelId)) { throw new ArgumentException("Null channel id."); } // Create bot conversation reference. var conversationReference = this.PrepareConversationReferenceAsync(teamDataEntity, teamsChannelId); // Ensure the bot service URL is trusted. if (!MicrosoftAppCredentials.IsTrustedServiceUrl(conversationReference.ServiceUrl)) { MicrosoftAppCredentials.TrustServiceUrl(conversationReference.ServiceUrl); } // Trigger bot to send the adaptive card. try { var previewQueueMessageContent = new SendQueueMessageContent { NotificationId = draftNotificationEntity.Id, ActivtiyId = null, RecipientData = null, NotificationUpdatePreviewEntity = new NotificationUpdatePreviewEntity { ActionType = "PreviewNotification", NotificationDataEntity = null, ConversationReferance = conversationReference, MessageActivity = await this.GetPreviewMessageActivity(draftNotificationEntity), ServiceUrl = conversationReference.ServiceUrl, AppID = this.botAppId, }, }; await this.sendQueue.SendAsync(previewQueueMessageContent); return(HttpStatusCode.OK); } catch (ErrorResponseException e) { var errorResponse = (ErrorResponse)e.Body; if (errorResponse != null && errorResponse.Error.Code.Equals(DraftNotificationPreviewService.ThrottledErrorResponse, StringComparison.OrdinalIgnoreCase)) { return(HttpStatusCode.TooManyRequests); } throw; } }
/// <summary> /// This method sets the globally accessible delay time indicating to all other functions that the system is currently in a /// throttled state and all messages need to be delayed and sends the queue message back to the queue with a delayed wait time. /// </summary> /// <param name="sendRetryDelayNumberOfSeconds">The number of seconds for the system and message to be delayed.</param> /// <param name="sendQueueMessageContent">The send queue message content to be sent back to the send queue for a delayed retry.</param> /// <param name="log">The logger.</param> /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> public async Task DelaySendingNotificationAsync( double sendRetryDelayNumberOfSeconds, SendQueueMessageContent sendQueueMessageContent, ILogger log) { try { // Shorten this time by 15 seconds to ensure that when the delayed retry message is taken off of the queue // the Send Retry Delay Time will be earlier and will not block it var sendRetryDelayTime = DateTime.UtcNow + TimeSpan.FromSeconds(sendRetryDelayNumberOfSeconds - 15); var globalSendingNotificationDataEntity = new GlobalSendingNotificationDataEntity { SendRetryDelayTime = sendRetryDelayTime, }; var setGlobalSendingNotificationDataEntityTask = this.globalSendingNotificationDataRepository .SetGlobalSendingNotificationDataEntityAsync(globalSendingNotificationDataEntity); var sendDelayedRetryTask = this.sendQueue.SendDelayedAsync(sendQueueMessageContent, sendRetryDelayNumberOfSeconds); await Task.WhenAll( setGlobalSendingNotificationDataEntityTask, sendDelayedRetryTask); } catch (Exception e) { log.LogError(e, $"ERROR: {e.GetType()}: {e.Message}"); throw; } }
public async Task <IActionResult> DeleteSentNotificationAsync(string id) { var notificationEntity = await this.notificationDataRepository.GetAsync( NotificationDataTableNames.SentNotificationsPartition, id); if (notificationEntity == null) { return(this.NotFound()); } await this.notificationDataRepository.DeleteAsync(notificationEntity); var sendingNotificationEntity = await this.notificationDataRepository.GetAsync( NotificationDataTableNames.SendingNotificationsPartition, id); if (notificationEntity == null) { return(this.NotFound()); } await this.notificationDataRepository.DeleteAsync(sendingNotificationEntity); var deleteQueueMessageContent = new SendQueueMessageContent { NotificationId = id, ActivtiyId = null, RecipientData = null, NotificationUpdatePreviewEntity = new NotificationUpdatePreviewEntity { ActionType = "DeleteNotification", NotificationDataEntity = null, ConversationReferance = null, MessageActivity = null, ServiceUrl = null, AppID = null, }, }; await this.sendQueue.SendAsync(deleteQueueMessageContent); var sentNotificationrepositoryEntities = await this.sentNotificationDataRepository.GetWithFilterAsync("PartitionKey eq'" + id + "'", id); foreach (var sentNotificationRepositoryEntity in sentNotificationrepositoryEntities) { var sentNotificationEntity = await this.sentNotificationDataRepository.GetAsync( id, sentNotificationRepositoryEntity.RowKey); if (notificationEntity == null) { return(this.NotFound()); } await this.sentNotificationDataRepository.DeleteAsync(sentNotificationEntity); } return(this.Ok()); }
/// <summary> /// Get service url. /// </summary> /// <param name="message">Send Queue message.</param> /// <returns>Service url.</returns> public static string GetServiceUrl(this SendQueueMessageContent message) { var recipient = message.RecipientData; return(recipient.RecipientType switch { RecipientDataType.User => recipient.UserData.ServiceUrl, RecipientDataType.Team => recipient.TeamData.ServiceUrl, _ => throw new ArgumentException("Invalid recipient type"), });
public async Task SendFunc_NullUserType_ShouldThrowArgumentNullException() { // Arrange var sendFunctionInstance = this.GetSendFunction(); string data = "{\"NotificationId\":\"notificationId\",\"RecipientData\": {\"RecipientId\" : \"TestResp\", \"UserData\": { \"UserId\" : \"userId\",\"ConversationId\":\"conversationId\",\"UserType\":\"\"}}}"; SendQueueMessageContent messageContent = JsonConvert.DeserializeObject <SendQueueMessageContent>(data); // Act Func <Task> task = async() => await sendFunctionInstance.Run(data, this.deliveryCount, this.dateTime, string.Empty, this.logger.Object, new ExecutionContext()); // Assert await task.Should().ThrowAsync <ArgumentNullException>(); }
private async Task <IMessageActivity> GetMessageActivity(SendQueueMessageContent message) { var notification = await this.notificationRepo.GetAsync( NotificationDataTableNames.SendingNotificationsPartition, message.NotificationId); var adaptiveCardAttachment = new Attachment() { ContentType = AdaptiveCardContentType, Content = JsonConvert.DeserializeObject(notification.Content), }; return(MessageFactory.Attachment(adaptiveCardAttachment)); }
public async Task SendFunc_GuestUser_ShouldNotSendMessage() { // Arrange var sendFunctionInstance = this.GetSendFunction(); string data = "{\"NotificationId\":\"notificationId\",\"RecipientData\": {\"RecipientId\" : \"TestResp\", \"UserData\": { \"UserId\" : \"userId\",\"ConversationId\":\"conversationId\",\"UserType\":\"Guest\"}}}"; SendQueueMessageContent messageContent = JsonConvert.DeserializeObject <SendQueueMessageContent>(data); // Act Func <Task> task = async() => await sendFunctionInstance.Run(data, this.deliveryCount, this.dateTime, string.Empty, this.logger.Object, new ExecutionContext()); // Assert await task.Should().NotThrowAsync(); this.notificationService.Verify(x => x.UpdateSentNotification(It.Is <string>(x => x.Equals("notificationId")), It.IsAny <string>(), It.IsAny <int>(), It.IsAny <int>(), It.IsAny <string>(), It.IsAny <string>()), Times.Once); }
public async Task NotificationPendingRecipientIdNotFoundTest() { // Arrange var notificationService = GetNotificationService(); SendQueueMessageContent sendQueueMessageContent = new SendQueueMessageContent() { RecipientData = new RecipientData() }; // Act Func <Task> task = async() => await notificationService.IsPendingNotification(sendQueueMessageContent); // Assert await task.Should().ThrowAsync <InvalidOperationException>().WithMessage("Recipient id is not set."); }
/// <inheritdoc/> public async Task <bool> IsNotificationCanceled(SendQueueMessageContent message) { // Check notification status. var notification = await this.notificationDataRepository.GetAsync( partitionKey : NotificationDataTableNames.SentNotificationsPartition, rowKey : message.NotificationId); // Check if the Status is Canceled. if (notification.Status.Equals(nameof(NotificationStatus.Canceled)) || notification.Status.Equals(nameof(NotificationStatus.Canceling))) { return(true); } return(false); }
private async Task <IMessageActivity> GetMessageActivity(SendQueueMessageContent message) { var notification = await this.notificationRepo.GetAsync( NotificationDataTableNames.SendingNotificationsPartition, message.NotificationId); // replacing id and key for read tracking purposes notification.Content = notification.Content.Replace("[ID]", message.NotificationId); notification.Content = notification.Content.Replace("[KEY]", message.RecipientData.RecipientId); var adaptiveCardAttachment = new Attachment() { ContentType = AdaptiveCardContentType, Content = JsonConvert.DeserializeObject(notification.Content), }; return(MessageFactory.Attachment(adaptiveCardAttachment)); }
/// <summary> /// Process send notification response. /// </summary> /// <param name="messageContent">Message content.</param> /// <param name="sendMessageResponse">Send notification response.</param> /// <param name="log">Logger.</param> private async Task ProcessResponseAsync( SendQueueMessageContent messageContent, SendMessageResponse sendMessageResponse, ILogger log) { var statusReason = string.Empty; if (sendMessageResponse.ResultType == SendMessageResult.Succeeded) { log.LogInformation($"Successfully sent the message." + $"\nRecipient Id: {messageContent.RecipientData.RecipientId}"); } else { log.LogError($"Failed to send message." + $"\nRecipient Id: {messageContent.RecipientData.RecipientId}" + $"\nResult: {sendMessageResponse.ResultType}." + $"\nErrorMessage: {sendMessageResponse.ErrorMessage}."); statusReason = this.localizer.GetString("Failed"); } await this.notificationService.UpdateSentNotification( notificationId : messageContent.NotificationId, recipientId : messageContent.RecipientData.RecipientId, totalNumberOfSendThrottles : sendMessageResponse.TotalNumberOfSendThrottles, statusCode : sendMessageResponse.StatusCode, allSendStatusCodes : sendMessageResponse.AllSendStatusCodes, errorMessage : statusReason, exception : sendMessageResponse.ErrorMessage); // Throttled if (sendMessageResponse.ResultType == SendMessageResult.Throttled) { // Set send function throttled. await this.notificationService.SetSendNotificationThrottled(this.sendRetryDelayNumberOfSeconds); // Requeue. await this.sendQueue.SendDelayedAsync(messageContent, this.sendRetryDelayNumberOfSeconds); return; } }
public async Task SendNotificationResponseThrottledTest() { // Arrange var sendFunctionInstance = this.GetSendFunction(); SendMessageResponse sendMessageResponse = new SendMessageResponse() { ResultType = SendMessageResult.Throttled, }; string data = "{\"NotificationId\":\"notificationId\",\"RecipientData\": {\"RecipientId\" : \"TestResp\", \"UserData\": { \"UserId\" : \"userId\",\"ConversationId\":\"conversationId\",\"UserType\":\"Member\"}}}"; SendQueueMessageContent messageContent = JsonConvert.DeserializeObject <SendQueueMessageContent>(data); this.notificationService .Setup(x => x.IsPendingNotification(It.IsAny <SendQueueMessageContent>())) .ReturnsAsync(true); this.notificationService .Setup(x => x.UpdateSentNotification(It.IsAny <string>(), It.IsAny <string>(), It.IsAny <int>(), It.IsAny <int>(), It.IsAny <string>(), It.IsAny <string>())) .Returns(Task.CompletedTask); this.notificationService .Setup(x => x.IsSendNotificationThrottled()) .ReturnsAsync(false); this.sendQueue .Setup(x => x.SendDelayedAsync(It.IsAny <SendQueueMessageContent>(), It.IsAny <double>())) .Returns(Task.CompletedTask); var notificatioData = new SendingNotificationDataEntity() { Content = "{\"text\":\"Welcome\",\"displayText\":\"Hello\"}" }; this.notificationRepo.Setup(x => x.GetAsync(It.IsAny <string>(), It.IsAny <string>())).ReturnsAsync(notificatioData); this.messageService.Setup(x => x.SendMessageAsync(It.IsAny <IMessageActivity>(), It.IsAny <string>(), It.IsAny <string>(), It.IsAny <int>(), this.logger.Object)).ReturnsAsync(sendMessageResponse); this.notificationService.Setup(x => x.UpdateSentNotification(It.IsAny <string>(), It.IsAny <string>(), It.IsAny <int>(), It.IsAny <int>(), It.IsAny <string>(), It.IsAny <string>())).Returns(Task.CompletedTask); // Act Func <Task> task = async() => await sendFunctionInstance.Run(data, this.deliveryCount, this.dateTime, string.Empty, this.logger.Object, new ExecutionContext()); // Assert await task.Should().NotThrowAsync <NullReferenceException>(); this.sendQueue.Verify(x => x.SendDelayedAsync(It.IsAny <SendQueueMessageContent>(), It.IsAny <double>())); }
/// <summary> /// The service used to fetch or generate the necessary parameters for sending the notification. /// </summary> /// <param name="messageContent">The message content of the send queue message.</param> /// <param name="log">The logger.</param> /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> public async Task <GetSendNotificationParamsResponse> GetSendNotificationParamsAsync( SendQueueMessageContent messageContent, ILogger log) { // Fetch the current sending notification. This is where the data about what is being sent is stored. var activeNotificationEntity = await this.sendingNotificationDataRepository.GetAsync( NotificationDataTableNames.SendingNotificationsPartition, messageContent.NotificationId); // Store the content for the notification that is to be sent. var getSendNotificationParamsResponse = new GetSendNotificationParamsResponse { NotificationContent = activeNotificationEntity.Content, ForceCloseAzureFunction = false, }; // Depending on the recipient type and the data in the send queue message, // fetch or generate the recipient Id, service URL, and conversation Id to be used for // sending the notification. switch (messageContent.RecipientData.RecipientType) { case RecipientDataType.User: await this.SetParamsForUserRecipientAsync( getSendNotificationParamsResponse, messageContent.RecipientData.UserData, messageContent, log); break; case RecipientDataType.Team: this.SetParamsForTeamRecipient( getSendNotificationParamsResponse, messageContent.RecipientData.TeamData); break; default: throw new ArgumentException($"Invalid recipient type: {messageContent.RecipientData.RecipientType}"); } return(getSendNotificationParamsResponse); }
public async Task SendNotificationWhenNoConversationIdTest() { // Arrange var sendFunctionInstance = this.GetSendFunction(); string data = "{\"NotificationId\":\"notificationId\",\"RecipientData\": {\"RecipientId\" : \"TestResp\", \"UserData\": { \"UserId\" : \"userId\",\"ConversationId\":\"\"}}}"; SendQueueMessageContent messageContent = JsonConvert.DeserializeObject <SendQueueMessageContent>(data); this.notificationService .Setup(x => x.IsPendingNotification(It.IsAny <SendQueueMessageContent>())) .ReturnsAsync(true); this.notificationService .Setup(x => x.UpdateSentNotification(It.IsAny <string>(), It.IsAny <string>(), It.IsAny <int>(), It.IsAny <int>(), It.IsAny <string>(), It.IsAny <string>())) .Returns(Task.CompletedTask); // Act Func <Task> task = async() => await sendFunctionInstance.Run(data, this.deliveryCount, this.dateTime, string.Empty, this.logger.Object, new ExecutionContext()); // Assert await task.Should().NotThrowAsync <NullReferenceException>(); this.notificationService.Verify(x => x.UpdateSentNotification(It.Is <string>(x => x.Equals("notificationId")), It.IsAny <string>(), It.IsAny <int>(), It.IsAny <int>(), It.IsAny <string>(), It.IsAny <string>())); }
public async Task SendNotificationException_Test() { // Arrange var sendFunctionInstance = this.GetSendFunction(); SendMessageResponse sendMessageResponse = new SendMessageResponse() { ResultType = SendMessageResult.Throttled, }; string data = "{\"NotificationId\":\"notificationId\",\"RecipientData\": {\"RecipientId\" : null }}"; SendQueueMessageContent messageContent = JsonConvert.DeserializeObject <SendQueueMessageContent>(data); this.notificationService .Setup(x => x.IsPendingNotification(It.IsAny <SendQueueMessageContent>())) .ReturnsAsync(true); this.notificationService .Setup(x => x.UpdateSentNotification(It.IsAny <string>(), It.IsAny <string>(), It.IsAny <int>(), It.IsAny <int>(), It.IsAny <string>(), It.IsAny <string>())) .Returns(Task.CompletedTask); // Act Func <Task> task = async() => await sendFunctionInstance.Run(data, this.deliveryCount, this.dateTime, string.Empty, this.logger.Object, new ExecutionContext()); // Assert await task.Should().ThrowAsync <NullReferenceException>(); }
/// <summary> /// The main point of this is to set the recipient Id, service URL, and conversation Id for the send parameters. /// The recipient Id and service URL is always set in the incoming user data. /// /// For the conversation Id there are three possibilities: /// Sending to all users - the conversationId is present in the incoming user data. /// Sending to a team's members - the conversation Id was already stored for that user in the User Data table. /// For this scenario, the incoming user data is stored back into the User Data table /// because it may hold more information that was not present in the User Data table originally, /// such as name /// Sending to a team's members - the conversation Id was not stored for that user in the User Data table. /// </summary> /// <param name="getSendNotificationParamsResponse">The send notification parameters to be updated.</param> /// <param name="incomingUserDataEntity">The incoming user data entity.</param> /// <param name="messageContent">The queue message content.</param> /// <param name="log">The logger.</param> /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> private async Task SetParamsForUserRecipientAsync( GetSendNotificationParamsResponse getSendNotificationParamsResponse, UserDataEntity incomingUserDataEntity, SendQueueMessageContent messageContent, ILogger log) { try { // The AAD Id and service URL will always be set in the incoming user data. getSendNotificationParamsResponse.RecipientId = incomingUserDataEntity.AadId; getSendNotificationParamsResponse.ServiceUrl = incomingUserDataEntity.ServiceUrl; /* * The rest of the logic in this method is primarily for fetching/generating the conversation Id. * */ // Case where the conversation Id is included in the incoming user data. if (!string.IsNullOrWhiteSpace(incomingUserDataEntity.ConversationId)) { getSendNotificationParamsResponse.ConversationId = incomingUserDataEntity.ConversationId; return; } // Case where the conversation Id is not included in the incoming user data (the conversation Id may or // may not be stored in the User Data table). // Fetch the data for that user from the User Data table to see if they are present. // It is possible that that user's data has not been stored in the User Data table. // If this is the case, then the conversation will need to be created for that user. var storedUserDataEntity = await this.userDataRepository.GetAsync( UserDataTableNames.UserDataPartition, incomingUserDataEntity.AadId); // These blocks are used to determine the conversation Id to be used when sending the notification. string conversationId = null; if (storedUserDataEntity != null) { /* * Case where the user's data was stored in the User Data table so their conversation Id is * known. Update the user's entry in the User Data table, though, with the incoming user data * because the incoming user data may hold more information than what is already stored for * that user, such as their name. */ // Set the conversation Id to be used when sending the notification. conversationId = storedUserDataEntity.ConversationId; // Set the conversation Id to ensure it is not removed from the User Data table on the update. incomingUserDataEntity.ConversationId = storedUserDataEntity.ConversationId; incomingUserDataEntity.PartitionKey = UserDataTableNames.UserDataPartition; incomingUserDataEntity.RowKey = incomingUserDataEntity.AadId; await this.userDataRepository.InsertOrMergeAsync(incomingUserDataEntity); } else { /* * Falling into this block means that the user data and the conversation Id for this user * has not been stored. Because of this, the conversation needs to be created and that * conversation Id needs to be stored for that user for later use. */ var createConversationResponse = await this.createUserConversationService.CreateConversationAsync( userDataEntity : incomingUserDataEntity, maxNumberOfAttempts : this.maxNumberOfAttempts, log : log); if (createConversationResponse.ResultType == CreateUserConversationResultType.Succeeded) { // Set the conversation Id to be used when sending the notification. conversationId = createConversationResponse.ConversationId; // Store the newly created conversation Id so the create conversation // request will not need to be made again for the user for future notifications. incomingUserDataEntity.ConversationId = createConversationResponse.ConversationId; incomingUserDataEntity.PartitionKey = UserDataTableNames.UserDataPartition; incomingUserDataEntity.RowKey = incomingUserDataEntity.AadId; await this.userDataRepository.InsertOrMergeAsync(incomingUserDataEntity); } else if (createConversationResponse.ResultType == CreateUserConversationResultType.Throttled) { // If the request was attempted the maximum number of allowed attempts and received // all throttling responses, then set the overall delay time for the system so all // other calls will be delayed and add the message back to the queue with a delay to be // attempted later. await this.delaySendingNotificationService.DelaySendingNotificationAsync( sendRetryDelayNumberOfSeconds : this.sendRetryDelayNumberOfSeconds, sendQueueMessageContent : messageContent, log : log); // Signal that the Azure Function should be completed to be attempted later. getSendNotificationParamsResponse.ForceCloseAzureFunction = true; return; } else if (createConversationResponse.ResultType == CreateUserConversationResultType.Failed) { // If the create conversation call failed, save the results, do not attempt the // request again, and end the function. await this.manageResultDataService.ProcessResultDataAsync( notificationId : messageContent.NotificationId, recipientId : incomingUserDataEntity.AadId, totalNumberOfSendThrottles : 0, isStatusCodeFromCreateConversation : true, statusCode : createConversationResponse.StatusCode, allSendStatusCodes : string.Empty, errorMessage : createConversationResponse.ErrorMessage, log : log); // Signal that the Azure Function should be completed. getSendNotificationParamsResponse.ForceCloseAzureFunction = true; return; } } // Set the conversation Id to be used when sending the notification. getSendNotificationParamsResponse.ConversationId = conversationId; } catch (Exception e) { var errorMessage = $"{e.GetType()}: {e.Message}"; log.LogError(e, $"ERROR: {errorMessage}"); throw; } }
/// <summary> /// Checks if the data queue message should be processed. Scenarios it checks for are: /// If the entire system is currently in a throttled state. /// If the notification has already been attempted to be sent to this recipient. /// </summary> /// <param name="messageContent">The data queue message content.</param> /// <param name="sendRetryDelayNumberOfSeconds">The send retry delay number of seconds.</param> /// <param name="log">The logger.</param> /// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns> public async Task <bool> VerifyMessageShouldBeProcessedAsync( SendQueueMessageContent messageContent, double sendRetryDelayNumberOfSeconds, ILogger log) { try { // Fetch the current global sending notification data. This is where data about the overall system's // status is stored e.g. is everything in a delayed state because the bot is being throttled. var globalSendingNotificationDataEntityTask = this.globalSendingNotificationDataRepository .GetGlobalSendingNotificationDataEntityAsync(); // Fetch the sent notification data to verify it has only been initialized and a notification // has not already been sent to this recipient. var existingSentNotificationDataEntityTask = this.sentNotificationDataRepository .GetAsync( partitionKey: messageContent.NotificationId, rowKey: messageContent.RecipientData.RecipientId); await Task.WhenAll( globalSendingNotificationDataEntityTask, existingSentNotificationDataEntityTask); var globalSendingNotificationDataEntity = await globalSendingNotificationDataEntityTask; var existingSentNotificationDataEntity = await existingSentNotificationDataEntityTask; var shouldProceedWithProcessing = true; // If the overall system is in a throttled state and needs to be delayed, // add the message back on the queue with a delay and stop processing the queue message. if (globalSendingNotificationDataEntity?.SendRetryDelayTime != null && DateTime.UtcNow < globalSendingNotificationDataEntity.SendRetryDelayTime) { await this.sendQueue.SendDelayedAsync(messageContent, sendRetryDelayNumberOfSeconds); shouldProceedWithProcessing = false; } // First, verify that the recipient's sent notification data has been stored and initialized. This // verifies a notification is expected to be sent to this recipient. // Next, in order to not send a duplicate notification to this recipient, verify that the StatusCode // in the sent notification data is set to either: // The InitializationStatusCode (likely 0) - this means the notification has not been attempted // to be sent to this recipient. // The FaultedAndRetryingStatusCode (likely -1) - this means the Azure Function previously attempted // to send the notification to this recipient but threw an exception, so sending the // notification should be attempted again. // If it is neither of these scenarios, then complete the function in order to not send a duplicate // notification to this recipient. else if (existingSentNotificationDataEntity == null || (existingSentNotificationDataEntity.StatusCode != SentNotificationDataEntity.InitializationStatusCode && existingSentNotificationDataEntity.StatusCode != SentNotificationDataEntity.FaultedAndRetryingStatusCode)) { shouldProceedWithProcessing = false; } return(shouldProceedWithProcessing); } catch (Exception e) { var errorMessage = $"{e.GetType()}: {e.Message}"; log.LogError(e, $"ERROR: {errorMessage}"); throw; } }
public async Task <IActionResult> UpdateSentNotificationAsync([FromBody] DraftNotification notification) { var containsHiddenMembership = await this.groupsService.ContainsHiddenMembershipAsync(notification.Groups); if (containsHiddenMembership) { return(this.Forbid()); } if (!notification.Validate(this.localizer, out string errorMessage)) { return(this.BadRequest(errorMessage)); } var senttNotificationDataEntity = await this.notificationDataRepository.GetAsync( NotificationDataTableNames.SentNotificationsPartition, notification.Id); var updateSentNotificationDataEntity = new NotificationDataEntity { PartitionKey = NotificationDataTableNames.SentNotificationsPartition, RowKey = notification.Id, Id = notification.Id, Channel = notification.Channel, TemplateID = notification.TemplateID, Title = notification.Title, ImageLink = notification.ImageLink, Summary = notification.Summary, Author = notification.Author, ButtonTitle = notification.ButtonTitle, ButtonLink = notification.ButtonLink, CreatedBy = this.HttpContext.User?.Identity?.Name, CreatedDate = DateTime.UtcNow, SentDate = DateTime.UtcNow, Edited = DateTime.UtcNow, IsDraft = false, Teams = notification.Teams, Rosters = notification.Rosters, Groups = notification.Groups, AllUsers = notification.AllUsers, MessageVersion = senttNotificationDataEntity.MessageVersion, Succeeded = senttNotificationDataEntity.Succeeded, Failed = 0, Throttled = 0, TotalMessageCount = senttNotificationDataEntity.TotalMessageCount, SendingStartedDate = DateTime.UtcNow, Status = NotificationStatus.Sent.ToString(), }; var id = updateSentNotificationDataEntity.Id; var sentNotificationId = await this.notificationDataRepository.UpdateSentNotificationAsync(updateSentNotificationDataEntity, id); await this.sentNotificationUpdateDataRepository.EnsureSentNotificationDataTableExistsAsync(); var sendQueueMessageContent = new SendQueueMessageContent { NotificationId = sentNotificationId, ActivtiyId = null, RecipientData = null, NotificationUpdatePreviewEntity = new NotificationUpdatePreviewEntity { ActionType = "EditNotification", NotificationDataEntity = updateSentNotificationDataEntity, ConversationReferance = null, MessageActivity = null, ServiceUrl = null, AppID = null, }, }; await this.sendQueue.SendAsync(sendQueueMessageContent); return(this.Ok()); }