/// <summary> /// Get general channel Id if scrum channel id does not exist. /// </summary> /// <param name="turnContext">Context object containing information cached for a single turn of conversation with a user.</param> /// <param name="scrumConfigurationDetails">scrum configuration details.</param> /// <returns>Returns general channel Id if scrum channel id does not exist.</returns> private async Task <string> GetValidChannelIdAsync(ITurnContext turnContext, ScrumConfiguration scrumConfigurationDetails) { var teamsChannelInfo = await TeamsInfo.GetTeamChannelsAsync(turnContext, scrumConfigurationDetails.TeamId, CancellationToken.None); var channelInfo = teamsChannelInfo.Where(channel => channel.Id.Equals(scrumConfigurationDetails.ChannelId, StringComparison.OrdinalIgnoreCase)).FirstOrDefault(); if (channelInfo == null) { scrumConfigurationDetails.ChannelId = scrumConfigurationDetails.TeamId; scrumConfigurationDetails.ChannelName = Strings.GeneralChannel; List <ScrumConfiguration> scrumConfigurations = new List <ScrumConfiguration>(); scrumConfigurations.Add(scrumConfigurationDetails); var saveResponse = await this.scrumConfigurationStorageProvider.StoreOrUpdateScrumConfigurationEntitiesAsync(scrumConfigurations); if (!saveResponse) { this.logger.LogError("Error while saving scrum configuration details"); } return(scrumConfigurationDetails.TeamId); } return(scrumConfigurationDetails.ChannelId); }
/// <summary> /// Method ends the existing scrum if running and then sends the start scrum card. /// </summary> /// <param name="scrumConfiguration">Scrum configuration details obtained from storage.</param> /// <returns>A task that ends the existing scrum and sends the start scrum card .</returns> public async Task ScrumStartActivityAsync(ScrumConfiguration scrumConfiguration) { try { string serviceUrl = scrumConfiguration.ServiceUrl; MicrosoftAppCredentials.TrustServiceUrl(serviceUrl); var conversationReference = new ConversationReference() { ChannelId = TeamsBotFrameworkChannelId, Bot = new ChannelAccount() { Id = $"28:{this.microsoftAppCredentials.MicrosoftAppId}" }, ServiceUrl = serviceUrl, Conversation = new ConversationAccount() { ConversationType = Constants.ChannelConversationType, IsGroup = true, Id = scrumConfiguration.ChannelId, TenantId = this.options.Value.TenantId }, }; this.logger.LogInformation($"Sending start scrum card to channelId: {scrumConfiguration.ChannelId}"); await RetryPolicy.ExecuteAsync(async() => { try { await((BotFrameworkAdapter)this.adapter).ContinueConversationAsync( this.microsoftAppCredentials.MicrosoftAppId, conversationReference, async(conversationTurnContext, conversationCancellationToken) => { bool isValidScrum = await this.EndExistingScrumAsync(conversationTurnContext, scrumConfiguration, conversationCancellationToken); if (!isValidScrum) { this.logger.LogInformation("Error while ending the existing scrum."); await conversationTurnContext.SendActivityAsync(this.localizer.GetString(this.localizer.GetString("ErrorMessage"))); } await this.SendScrumStartCardAsync(conversationTurnContext, scrumConfiguration, conversationCancellationToken); }, CancellationToken.None); } catch (Exception ex) { this.logger.LogError(ex, "Error while performing retry logic to send scrum start card."); throw; } }); } catch (Exception ex) { this.logger.LogError(ex, "Error while sending start scrum to channel from background service."); } }
/// <summary> /// Method to validate the existing scrum if already running. /// </summary> /// <param name="turnContext">Context object containing information cached for a single turn of conversation with a user.</param> /// <param name="scrumConfiguration">Values obtained from ScrumConfiguration table.</param> /// <param name="cancellationToken">Propagates notification that operations should be canceled.</param> /// <returns>A task that ends the existing scrum.</returns> private async Task <bool> EndExistingScrumAsync(ITurnContext turnContext, ScrumConfiguration scrumConfiguration, CancellationToken cancellationToken) { // If previous scrum is already running end that and refresh scrum start card. var scrumDetails = await this.scrumStorageProvider.GetScrumsByScrumTeamConfigIdAsync(scrumConfiguration.ScrumTeamConfigId, scrumConfiguration.AadGroupId); if (scrumDetails == null) { return(false); } foreach (var scrumDetail in scrumDetails) { if (!scrumDetail.IsCompleted) { scrumDetail.IsCompleted = true; var savedData = await this.scrumStorageProvider.CreateOrUpdateScrumAsync(scrumDetail); if (!savedData) { this.logger.LogInformation($"Error while updating scrim table from storage for scrumTeamConfigId : {scrumConfiguration.ScrumTeamConfigId}"); return(false); } this.logger.LogInformation($"Getting scrum configuration details which are active. ScrumTeamConfigId: {scrumDetail.ScrumTeamConfigId}"); var scrumConfigurationDetails = await this.scrumConfigurationStorageProvider.GetScrumConfigurationDetailByScrumTeamConfigIdAsync(scrumDetail.ScrumTeamConfigId, scrumDetail.AadGroupId); if (scrumConfigurationDetails == null || !scrumConfigurationDetails.IsActive) { return(false); } // End the existing running scrum and refresh start card with end scrum. var scrumMembers = scrumDetail.MembersActivityIdMap; var membersActivityIdMap = JsonConvert.DeserializeObject <Dictionary <string, string> >(scrumMembers); var updatedScrumSummary = await this.scrumHelper.GetScrumSummaryAsync(scrumDetail.ScrumTeamConfigId, scrumConfiguration.AadGroupId, scrumDetail.ScrumStartCardResponseId, membersActivityIdMap); if (updatedScrumSummary == null) { this.logger.LogInformation($"No data obtained from storage to update summary card for scrumStartCardActivityId : {scrumDetail.ScrumStartCardResponseId}"); continue; } await this.cardHelper.UpdateSummaryCardWithEndScrumAsync(updatedScrumSummary, scrumDetail, scrumConfiguration, membersActivityIdMap, scrumConfigurationDetails.TimeZone, turnContext, cancellationToken); this.logger.LogInformation($"Ended existing running scrum for {scrumDetail.ThreadConversationId}"); } } return(true); }
/// <summary> /// Update the first trail card with user details. /// </summary> /// <param name="scrumSummary">Scrum summary information to be shown on card.</param> /// <param name="scrum">Scrum details.</param> /// <param name="scrumConfiguration">Scrum configuration details.</param> /// <param name="membersActivityIdMap">Members id who are part of the scrum.</param> /// <param name="timeZone">Used to convert scrum start time as per specified time zone.</param> /// <param name="turnContext">Context object containing information cached for a single turn of conversation with a user.</param> /// <param name="cancellationToken">Propagates notification that operations should be canceled.</param> /// <returns>A task of type bool where true represents summary card updated successfully while false indicates failure in updating the summary card.</returns> public async Task <bool> UpdateSummaryCardWithEndScrumAsync(ScrumSummary scrumSummary, Scrum scrum, ScrumConfiguration scrumConfiguration, Dictionary <string, string> membersActivityIdMap, string timeZone, ITurnContext turnContext, CancellationToken cancellationToken) { var activitySummary = MessageFactory.Attachment(ScrumCard.GetScrumStartCard(scrumSummary, membersActivityIdMap, scrumConfiguration?.ScrumTeamConfigId, scrum?.ScrumStartActivityId, this.localizer, timeZone)); var teamsChannelInfo = await TeamsInfo.GetTeamChannelsAsync(turnContext, scrumConfiguration.TeamId, CancellationToken.None); var channelInfo = teamsChannelInfo.Where(channel => channel.Id.Equals(scrumConfiguration.ChannelId, StringComparison.OrdinalIgnoreCase)).FirstOrDefault(); if (channelInfo == null) { return(false); } activitySummary.Id = scrum.ScrumStartCardResponseId; activitySummary.Conversation = new ConversationAccount { Id = $"{scrumConfiguration.ChannelId};messageid={scrum.ScrumStartCardResponseId}", }; this.logger.LogInformation($"Trail card updated for: {scrum.ThreadConversationId} summaryCardActivityId: {scrum.ScrumStartCardResponseId}"); await turnContext?.UpdateActivityAsync(activitySummary, cancellationToken); turnContext.Activity.Conversation = new ConversationAccount { Id = $"{scrumConfiguration.ChannelId};messageid={scrum.ScrumStartCardResponseId}", }; await turnContext.SendActivityAsync(this.localizer.GetString("SuccessMessageAfterEndingScrum"), cancellationToken : cancellationToken); return(true); }
/// <summary> /// Create a new scrum from the input. /// </summary> /// <param name="scrumStartCardResponseId">Activity id of scrum summary card.</param> /// <param name="scrumCardId">Activity Id of scrum card.</param> /// <param name="members">JSON serialized member and activity mapping.</param> /// <param name="scrumConfiguration">An instance of scrum configuration details.</param> /// <param name="turnContext">Context object containing information cached for a single turn of conversation with a user.</param> /// <param name="cancellationToken">Propagates notification that operations should be canceled.</param> /// <returns>A task that represents the work queued to execute.</returns> private async Task CreateScrumAsync(string scrumStartCardResponseId, string scrumCardId, string members, ScrumConfiguration scrumConfiguration, ITurnContext turnContext, CancellationToken cancellationToken) { string conversationId = turnContext.Activity.Conversation.Id; try { Scrum scrumEntity = new Scrum { ThreadConversationId = conversationId, ScrumStartActivityId = scrumCardId, IsCompleted = false, MembersActivityIdMap = members, ScrumStartCardResponseId = scrumStartCardResponseId, ScrumTeamConfigId = scrumConfiguration.ScrumTeamConfigId, ScrumId = conversationId, ChannelName = scrumConfiguration.ChannelName, TeamId = scrumConfiguration.TeamId, CreatedOn = DateTime.UtcNow.ToString(Constants.Rfc3339DateTimeFormat, CultureInfo.InvariantCulture), AadGroupId = scrumConfiguration.AadGroupId, }; var savedData = await this.scrumStorageProvider.CreateOrUpdateScrumAsync(scrumEntity); if (!savedData) { await turnContext.SendActivityAsync(this.localizer.GetString("ErrorSavingScrumData"), cancellationToken : cancellationToken); } } catch (Exception ex) { this.logger.LogError(ex, $"For {conversationId}: saving scrum data to table storage failed. {ex.Message}", SeverityLevel.Error); await turnContext.SendActivityAsync(this.localizer.GetString("ErrorMessage"), cancellationToken : cancellationToken); throw; } }
/// <summary> /// Method that sends the start scrum card to the channel. /// </summary> /// <param name="turnContext">Context object containing information cached for a single turn of conversation with a user.</param> /// <param name="scrumConfiguration">scrum configuration details obtained from storage.</param> /// <param name="cancellationToken">Propagates notification that operations should be canceled.</param> /// <returns>A task that sends the start scrum card.</returns> private async Task SendScrumStartCardAsync(ITurnContext turnContext, ScrumConfiguration scrumConfiguration, CancellationToken cancellationToken) { try { string scrumTeamConfigId = scrumConfiguration.ScrumTeamConfigId; if (scrumTeamConfigId != null) { this.logger.LogInformation($"Scrum start for ID: {scrumTeamConfigId}"); var scrumSummary = await this.scrumHelper.GetScrumSummaryAsync(scrumTeamConfigId, scrumConfiguration.AadGroupId); if (scrumSummary == null) { this.logger.LogInformation($"scrum configuration details are deleted from storage."); await turnContext.SendActivityAsync(string.Format(CultureInfo.CurrentCulture, this.localizer.GetString("ErrorScrumDeleted"), scrumConfiguration.ScrumTeamName), cancellationToken : cancellationToken); return; } var scrumStartActivityId = Guid.NewGuid().ToString(); // Fetching the members list based on the teams id: turnContext.Activity.Conversation.Id = scrumConfiguration.TeamId; var scrumMembers = await this.GetValidMembersInScrumAsync(turnContext, scrumConfiguration.UserPrincipalNames, cancellationToken); if (scrumMembers == null) { this.logger.LogInformation($"No scrum members are available to provide the scrum status"); await turnContext.SendActivityAsync(this.localizer.GetString("ErrorNoScrumMembersPresent"), cancellationToken : cancellationToken); return; } var membersActivityIdMap = this.GetActivityIdOfMembersInScrum(scrumMembers); string membersList = JsonConvert.SerializeObject(membersActivityIdMap); // Mentioning the participants involved in the scrum var mentionActivity = this.GetMentionsActivity(scrumMembers); // Check if channel exists. If channel doesn't exist then scrum card will be sent in General channel. scrumConfiguration.ChannelId = await this.GetValidChannelIdAsync(turnContext, scrumConfiguration); // Send the start scrum card turnContext.Activity.Conversation.Id = scrumConfiguration.ChannelId; var attachment = ScrumCard.GetScrumStartCard(scrumSummary, membersActivityIdMap, scrumTeamConfigId, scrumStartActivityId, this.localizer, scrumConfiguration.TimeZone); var scrumStartActivity = MessageFactory.Attachment(attachment); var scrumStartActivityResponse = await turnContext.SendActivityAsync(scrumStartActivity, cancellationToken); // Update the conversation id to send mentioned participants as reply to scrum start card. turnContext.Activity.Conversation = new ConversationAccount { Id = $"{scrumConfiguration.ChannelId};messageid={scrumStartActivityResponse.Id}", }; await turnContext.SendActivityAsync(mentionActivity, cancellationToken); await this.CreateScrumAsync(scrumStartActivityResponse.Id, scrumStartActivityId, membersList, scrumConfiguration, turnContext, cancellationToken); this.logger.LogInformation($"Scrum start details saved to table storage for: {turnContext.Activity.Conversation.Id}"); } } catch (Exception ex) { this.logger.LogError(ex, $"Start scrum failed for {turnContext.Activity.Conversation.Id}: {ex.Message}", SeverityLevel.Error); throw; } }