Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 17
VisualStudioVersion = 17.10.34814.14
MinimumVisualStudioVersion = 10.0.40219.1
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "BotDailyTaskReminder", "BotDailyTaskReminder\BotDailyTaskReminder.csproj", "{BFDF6B3D-BB62-42DD-88EF-528FFE9FCBAD}"
EndProject
Project("{A9E3F50B-275E-4AF7-ADCE-8BE12D41E305}") = "M365Agent", "M365Agent\M365Agent.ttkproj", "{01E9AC3F-43C7-46DA-89BB-7E70F4B08B5C}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{9268A822-4DDA-4A0B-A2EB-E3551E6CE680}"
ProjectSection(SolutionItems) = preProject
BotDailyTaskReminder.slnLaunch.user = BotDailyTaskReminder.slnLaunch.user
EndProjectSection
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{BFDF6B3D-BB62-42DD-88EF-528FFE9FCBAD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{BFDF6B3D-BB62-42DD-88EF-528FFE9FCBAD}.Debug|Any CPU.Build.0 = Debug|Any CPU
{BFDF6B3D-BB62-42DD-88EF-528FFE9FCBAD}.Release|Any CPU.ActiveCfg = Release|Any CPU
{BFDF6B3D-BB62-42DD-88EF-528FFE9FCBAD}.Release|Any CPU.Build.0 = Release|Any CPU
{01E9AC3F-43C7-46DA-89BB-7E70F4B08B5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{01E9AC3F-43C7-46DA-89BB-7E70F4B08B5C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{01E9AC3F-43C7-46DA-89BB-7E70F4B08B5C}.Debug|Any CPU.Deploy.0 = Debug|Any CPU
{01E9AC3F-43C7-46DA-89BB-7E70F4B08B5C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{01E9AC3F-43C7-46DA-89BB-7E70F4B08B5C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {8F7F570D-74C3-4CF3-AACD-361A3D3F8E37}
EndGlobalSection
EndGlobal
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# TeamsFx files
build
appPackage/build
env/.env.*.user
env/.env.local
appsettings.Development.json
.deployment

# User-specific files
*.user

# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/

# Notification local store
.notification.localstore.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Integration.AspNet.Core;
using Microsoft.Bot.Connector;
using Microsoft.Bot.Schema;

namespace BotDailyTaskReminder
{
/// <summary>
/// Custom adapter that adds error handling to the bot's turn logic.
/// </summary>
public class AdapterWithErrorHandler : CloudAdapter
{
public AdapterWithErrorHandler(IConfiguration configuration, IHttpClientFactory httpClientFactory, ILogger<IBotFrameworkHttpAdapter> logger)
: base(configuration, httpClientFactory, logger)
{
OnTurnError = async (turnContext, exception) =>
{
// Log any leaked exception from the application.
// NOTE: In production environment, consider logging this to
// Azure Application Insights.
logger.LogError(exception, $"[OnTurnError] unhandled error : {exception.Message}");

// Uncomment below commented line for local debugging.
// await turnContext.SendActivityAsync($"Sorry, it looks like something went wrong. Exception Caught: {exception.Message}");

// Send a trace activity, which will be displayed in the Bot Framework Emulator
await SendTraceActivityAsync(turnContext, exception);
};
}

/// <summary>
/// Sends a trace activity to the Bot Framework Emulator in case of an error.
/// Only sends if the bot is running in the Emulator.
/// </summary>
private static async Task SendTraceActivityAsync(ITurnContext turnContext, Exception exception)
{
// Only send a trace activity if we're talking to the Bot Framework Emulator
if (turnContext.Activity.ChannelId == Channels.Emulator)
{
Activity traceActivity = new Activity(ActivityTypes.Trace)
{
Label = "TurnError",
Name = "OnTurnError Trace",
Value = exception.Message,
ValueType = "https://www.botframework.com/schemas/error",
};

// Send a trace activity
await turnContext.SendActivityAsync(traceActivity);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<Project Sdk="Microsoft.NET.Sdk.Web">

<PropertyGroup>
<TargetFramework>net10.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
</PropertyGroup>

<ItemGroup>
<PackageReference Include="AdaptiveCards" Version="3.1.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="10.0.3" />
<PackageReference Include="Microsoft.Bot.Builder.Integration.AspNet.Core" Version="4.23.1" />
<PackageReference Include="Quartz" Version="3.16.1" />
</ItemGroup>

<ItemGroup>
<Content Update="appsettings.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</Content>
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,216 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

using System.Collections.Concurrent;
using AdaptiveCards;
using BotDailyTaskReminder.Models;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Teams;
using Microsoft.Bot.Schema;
using Microsoft.Bot.Schema.Teams;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace BotDailyTaskReminder.Bots
{
/// <summary>
/// Handles incoming bot activities such as messages, task module fetch, and task module submission.
/// </summary>
public class ActivityBot : TeamsActivityHandler
{
private readonly string _applicationBaseUrl;
protected readonly BotState _conversationState;
private readonly ConcurrentDictionary<string, ConversationReference> _conversationReferences;
private readonly ConcurrentDictionary<string, List<SaveTaskDetail>> _taskDetails;

/// <summary>
/// Initializes a new instance of the <see cref="ActivityBot"/> class.
/// </summary>
public ActivityBot(IConfiguration configuration,
ConversationState conversationState,
ConcurrentDictionary<string, ConversationReference> conversationReferences,
ConcurrentDictionary<string, List<SaveTaskDetail>> taskDetails)
{
_conversationReferences = conversationReferences;
_conversationState = conversationState;
_taskDetails = taskDetails;
_applicationBaseUrl = configuration["ApplicationBaseUrl"] ?? throw new NullReferenceException("ApplicationBaseUrl");
}

/// <summary>
/// Handles when a message is addressed to the bot.
/// </summary>
/// <param name="turnContext">The turn context of the message activity.</param>
/// <param name="cancellationToken">Cancellation token for the asynchronous task.</param>
/// <returns>A task that represents the work queued to execute.</returns>
protected override async Task OnMessageActivityAsync(ITurnContext<IMessageActivity> turnContext, CancellationToken cancellationToken)
{
if (turnContext.Activity.Text.ToLower().Trim() == "create-reminder")
{
// Adds the current conversation reference and sends the task scheduling adaptive card
AddConversationReference(turnContext.Activity as Activity);
await turnContext.SendActivityAsync(MessageFactory.Attachment(GetAdaptiveCardForTaskModule()), cancellationToken);
}
}

/// <summary>
/// Handles the completion of a turn, saving any state changes.
/// </summary>
/// <param name="turnContext">The context of the current turn.</param>
/// <param name="cancellationToken">Cancellation token for the asynchronous task.</param>
/// <returns>A task that represents the work queued to execute.</returns>
public override async Task OnTurnAsync(ITurnContext turnContext, CancellationToken cancellationToken = default)
{
await base.OnTurnAsync(turnContext, cancellationToken);

// Save any changes made to conversation state during this turn.
await _conversationState.SaveChangesAsync(turnContext, false, cancellationToken);
}

/// <summary>
/// Invoked when the bot is added to a conversation.
/// Sends a welcome message to new members.
/// </summary>
/// <param name="membersAdded">The members added to the conversation.</param>
/// <param name="turnContext">The context of the current turn.</param>
/// <param name="cancellationToken">Cancellation token for the asynchronous task.</param>
/// <returns>A task that represents the work queued to execute.</returns>
protected override async Task OnMembersAddedAsync(IList<ChannelAccount> membersAdded, ITurnContext<IConversationUpdateActivity> turnContext, CancellationToken cancellationToken)
{
foreach (var member in turnContext.Activity.MembersAdded)
{
if (member.Id != turnContext.Activity.Recipient.Id)
{
// Sends a greeting message when a new user is added
await turnContext.SendActivityAsync(MessageFactory.Text("Hello and welcome! Use the command 'create-reminder' to schedule a recurring task and receive reminders."), cancellationToken);
}
}
}

/// <summary>
/// Handles task module fetch requests.
/// </summary>
/// <param name="turnContext">The context of the turn.</param>
/// <param name="taskModuleRequest">The request payload for the task module.</param>
/// <param name="cancellationToken">Cancellation token for the asynchronous task.</param>
/// <returns>The task module response to send back.</returns>
protected override Task<TaskModuleResponse> OnTeamsTaskModuleFetchAsync(ITurnContext<IInvokeActivity> turnContext, TaskModuleRequest taskModuleRequest, CancellationToken cancellationToken)
{
var asJobject = JObject.FromObject(taskModuleRequest.Data);
var buttonType = (string)asJobject.ToObject<CardTaskFetchValue>()?.Id;
var taskModuleResponse = new TaskModuleResponse();

if (buttonType == "schedule")
{
taskModuleResponse.Task = new TaskModuleContinueResponse
{
Type = "continue",
Value = new TaskModuleTaskInfo
{
Url = _applicationBaseUrl + "/ScheduleTask",
Height = 450,
Width = 450,
Title = "Schedule a task",
},
};
}

return Task.FromResult(taskModuleResponse);
}

/// <summary>
/// Handles task module submission requests.
/// </summary>
/// <param name="turnContext">The context of the turn.</param>
/// <param name="taskModuleRequest">The request payload for the task module.</param>
/// <param name="cancellationToken">Cancellation token for the asynchronous task.</param>
/// <returns>The task module response to send back.</returns>
protected override async Task<TaskModuleResponse> OnTeamsTaskModuleSubmitAsync(ITurnContext<IInvokeActivity> turnContext, TaskModuleRequest taskModuleRequest, CancellationToken cancellationToken)
{
var asJobject = JObject.FromObject(taskModuleRequest.Data);
var taskData = asJobject.ToObject<TaskDetails>();
var title = (string)taskData?.Title;
var description = (string)taskData?.Description;
var dateTime = (DateTime)taskData?.DateTime;
var selectedDaysObject = (JArray)taskData?.SelectedDays;
var selectedDays = selectedDaysObject.ToObject<DayOfWeek[]>();
var date = dateTime.ToLocalTime();

// Prepare task details
var taskDetails = new SaveTaskDetail
{
Description = description,
Title = title,
DateTime = new DateTimeOffset(date.Year, date.Month, date.Day, date.Hour, date.Minute, 0, TimeSpan.Zero),
SelectedDays = selectedDays
};

// Add the task to the task list
_taskDetails.AddOrUpdate("taskDetails", new List<SaveTaskDetail> { taskDetails }, (key, currentTaskList) =>
{
currentTaskList.Add(taskDetails);
return currentTaskList;
});

// Schedule the task
var taskScheduler = new TaskScheduler();
taskScheduler.Start(date.Hour, date.Minute, _applicationBaseUrl, selectedDays);

// Send a success message to the user
await turnContext.SendActivityAsync("Task submitted successfully, you will get a recurring reminder for the task at a scheduled time");

return null;
}

/// <summary>
/// Creates and returns the adaptive card for scheduling a task.
/// </summary>
/// <returns>The adaptive card as an attachment.</returns>
private Attachment GetAdaptiveCardForTaskModule()
{
var card = new AdaptiveCard(new AdaptiveSchemaVersion("1.2"))
{
Body = new List<AdaptiveElement>
{
new AdaptiveTextBlock
{
Text = "Please click here to schedule a recurring task reminder",
Weight = AdaptiveTextWeight.Bolder,
Spacing = AdaptiveSpacing.Medium,
}
},
Actions = new List<AdaptiveAction>
{
new AdaptiveSubmitAction
{
Title = "Schedule task",
Data = new AdaptiveCardAction
{
MsteamsCardAction = new CardAction
{
Type = "task/fetch",
},
Id = "schedule"
},
}
},
};

return new Attachment
{
ContentType = AdaptiveCard.ContentType,
Content = JsonConvert.DeserializeObject(card.ToJson()),
};
}

/// <summary>
/// Adds a conversation reference for the current activity.
/// </summary>
/// <param name="activity">The bot activity.</param>
private void AddConversationReference(Activity activity)
{
var conversationReference = activity.GetConversationReference();
_conversationReferences.AddOrUpdate(conversationReference.User.Id, conversationReference, (key, newValue) => conversationReference);
}
}
}
Loading