Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
138 changes: 134 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,12 @@ It's simple, just open a chat with the bot either on Teams or Telegram.
## Features:

**Proactive reminders**
It will assist you to complete your due diligence. If you have less than 6 hours
reported on your Clockify (no matter how you put them in) it sends you a reminder.
It will assist you to complete your due diligence. If you have less than a predefined percentage (75% default) of the working
hours you have defined (8h default) reported on your Clockify (no matter how you put them in) it sends you a reminder.
You can choose whether to remind a user for today or for yesterdays timesheet.

**Setting working hours**
A user can set his working hours by himself just writing to the bot.

**Adding time entries with ease**
Add your time flexibly, even indicating projects partially. For example, you can type
Expand Down Expand Up @@ -61,7 +65,7 @@ dotnet --version

### To run this bot

Make sure to provide required configuration values. The app currently needs a value for these properties:
Make sure to provide required configuration values. The app currently needs a value for these properties within you appsettings.json:

```json
{
Expand All @@ -70,10 +74,120 @@ Make sure to provide required configuration values. The app currently needs a va
"LuisAPIHostName": "",
"ProactiveBotApiKey": "",
"KeyVaultName": "",
"MicrosoftAppType": "MultiTenant",
"MicrosoftAppId": "",
"MicrosoftAppPassword": ""
"MicrosoftAppPassword": "",
"BlobStorage": {
"DataConnectionString": "",
"ContainerName": ""
}
}
```

Description:
* LuisAppID: AppId for the app you created within luis.ai
* LuisAPIKey: API Key you set for your LUIS service under portal.azure.com
* LuisAPIHostName: Hostname from portal.azure.com without https://!
* ProactiveBotApiKey: The API Key you need to trigger https://<yourbot>:3978/api/timesheet/remind (pass the API-Key as header info "ProactiveBotApiKey")
* MicrosoftAppType: Should be MultiTenant or SingleTenant
* MicrosoftAppId: The AppId of your WebApplication you get from portal.azure.com
* MicrosoftAppPassword: The password for your application. You also get this from portal.azure.com
* KeyVaultName: the name of your Vault storage for storing the tokens
* BlobStorage: The Storage you will use for storing the user profiles

Important: if you test the bot locally, you should use a reduced set of settings:

```json
{
"LuisAppId": "",
"LuisAPIKey": "",
"LuisAPIHostName": "",
"ProactiveBotApiKey": "",
"BlobStorage": {
"DataConnectionString": "",
"ContainerName": ""
}
}
```
You still need the LUIS service to be active.

#### Optional settings
The bot supports these optional settings.

```json
{
"DEFAULT_WORKING_HOURS": "",
"MINIMUM_HOURS_FILLED_PERCENTAGE": ""
}
```

* DEFAULT_WORKING_HOURS: Defines the usual working hours per day
* MINIMUM_HOURS_FILLED_PERCENTAGE: Defines how much a user must fill per day to not get reminded

If nothing is defined, the bot will use 75% and 8h which leads to a minimum of 6 hours to be reported.

### KeyVault

The KeyVault also needs configuration of environment variables. These variables must be seti within your WebApplication environment settings.

Go to AzurePortal->WebApplication->Configuration
```json
{
"AZURE_CLIENT_ID": "",
"AZURE_TENANT_ID": "",
"AZURE_CLIENT_SECRET": ""
}
```
* AZURE_CLIENT_ID: The ID of the principal user (could be for example the APP ID)
* AZURE_TENANT_ID: The ID of the tenant (could be the tenand ID of your APP)
* AZURE_CLIENT_SECRET: The secret for your CLIENT_ID

Important! Make sure that your CLIENT_ID has access to your KeyVault. You grant access within the azure portal under your KeyVault.
Go to "AccessPolicies" and add a new one. You can choose your APP Registration for example.


### LUIS
For proper operation, you must provide a LUIS model. This can be done at luis.ai

#### Intents
You must create different intents. The intents below are the ones that i've figured out to be the minimum.
Add also @datetimeV2 as a feature.

![images/img1.JPG](images/img1.JPG)

![images/img2.jpg](images/img2.jpg)
![images/img3.jpg](images/img3.jpg)

List of intents:
* Fill (Example: plus 1 hour on test)
* Report (Example: Last two month or today)
* SetWorkingHours (Example: Change my work hours to 8 hours)
* Utilities_Stop (stop reminding me)

#### Entities
You need also at least one additional entity called "WorkedEntity". This stores the project you have worked on.

![images/img4.jpg](images/img4.jpg)

### Commands

The bot understands different commands:
* logout / log out -> removes the clockify token from the user profile and also from the KeyVault

### Auto reminder

The auto reminder is triggered by an endpoint. You have to call [GET] http://localhost:3978/api/timesheet/remind and pass ProactiveBotApiKey within the header and pass as value the "ProactiveBotApiKey" value.
There are additional parameters possible to pass:

* ?type: possible values: today, yesterday -> defines whether you want to remind the user to the todays timesheet or the one from yesterday
* ?respectWorkingHours: possible values: true, false -> defines whether you want to remind the user even outside of the defined working hours.

Example: .../api/timesheet/remind?type=today&respectWorkingHours=false

### Clockify
The first time you contact the bot, he will ask you for your clockify API-Key and stores it within the KeyVault.

### Run

Then run the bot. For example, from a terminal:

Expand Down Expand Up @@ -114,6 +228,22 @@ dotnet run

### Deploy the bot to Azure

Basically, you need to do the following:
* Create a resource group
* Create a service plan (for your web apps, choose windows as platform)
* Create a new Azure Bot (choose MultiTenant and Create new Microsoft App ID)
* Create a new KeyVault
* Create a new StorageAccount
* Create a new container within your StorageAccount
* Create a new LUIS service (together with authoring)
* Go to luis.ai and create a new LUIS app connected to your previously created LUIS service
* Create the luis.ai model at luis.ai
* Go to your bot instance within your resource group and connect the desired channels
* Go to your KeyVault and grant access to it for your WebApp registration
* Copy all required information for the appsettings.json and for the WebApp env vars.
* Set all the variables accordingly
* Within your IDE, publish your bot to your azure webapp (you can also download the publish profile from the azure portal)

To learn more about deploying a bot to Azure, see [Deploy your bot to Azure](https://aka.ms/azuredeployment) for a complete list of deployment instructions.

## Further reading
Expand Down
Binary file added images/img1.JPG
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/img2.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/img3.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/img4.jpg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
29 changes: 14 additions & 15 deletions src/Bot.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -4,30 +4,29 @@
<TargetFramework>netcoreapp3.1</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
<UserSecretsId>918ee82f-de39-4de6-94b7-fdfbf265d38a</UserSecretsId>
</PropertyGroup>


<ItemGroup>
<PackageReference Include="AdaptiveCards" Version="2.3.0" />
<PackageReference Include="AdaptiveCards.Templating" Version="1.1.0" />
<PackageReference Include="Azure.Identity" Version="1.4.0" />
<PackageReference Include="AdaptiveCards" Version="2.7.3" />
<PackageReference Include="AdaptiveCards.Templating" Version="1.2.2" />
<PackageReference Include="Azure.Identity" Version="1.5.0" />
<PackageReference Include="Azure.Security.KeyVault.Secrets" Version="4.2.0" />
<PackageReference Include="Clockify.Net" Version="1.11.0" />
<PackageReference Include="F23.StringSimilarity" Version="3.1.0" />
<PackageReference Include="Clockify.Net" Version="1.13.0" />
<PackageReference Include="F23.StringSimilarity" Version="4.1.0" />
<PackageReference Include="FluentDateTime" Version="2.0.0" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.18.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.1" />
<PackageReference Include="Microsoft.Bot.Builder" Version="4.9.3" />
<PackageReference Include="Microsoft.Bot.Builder.AI.Luis" Version="4.9.3" />
<PackageReference Include="Microsoft.Bot.Builder.Azure" Version="4.9.3" />
<PackageReference Include="Microsoft.Bot.Builder.Dialogs" Version="4.9.3" />
<PackageReference Include="Microsoft.Bot.Builder.Integration.AspNet.Core" Version="4.9.3" />
<PackageReference Include="Microsoft.ApplicationInsights.AspNetCore" Version="2.20.0" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.22" />
<PackageReference Include="Microsoft.Bot.Builder" Version="4.15.1" />
<PackageReference Include="Microsoft.Bot.Builder.AI.Luis" Version="4.15.1" />
<PackageReference Include="Microsoft.Bot.Builder.Azure" Version="4.15.1" />
<PackageReference Include="Microsoft.Bot.Builder.Dialogs" Version="4.15.1" />
<PackageReference Include="Microsoft.Bot.Builder.Integration.AspNet.Core" Version="4.15.1" />
<PackageReference Include="Microsoft.Recognizers.Text.DateTime" Version="1.4.0" />
<PackageReference Include="Microsoft.Bot.Builder.AI.Luis" Version="4.9.3" />
<PackageReference Include="Microsoft.Bot.Builder.Integration.AspNet.Core" Version="4.9.3" />
<PackageReference Include="System.Net.NameResolution" Version="4.3.0" />
<PackageReference Include="TimeZoneConverter" Version="3.5.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.19" />
<PackageReference Include="Microsoft.Extensions.Caching.Memory" Version="3.1.22" />
</ItemGroup>

<ItemGroup>
Expand Down
41 changes: 35 additions & 6 deletions src/Clockify/ClockifyController.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
using System.Threading.Tasks;
using System;
using System.Linq;
using System.Threading.Tasks;
using Bot.Remind;
using Bot.Security;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Azure.Cosmos.Linq;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Integration.AspNet.Core;

Expand All @@ -11,18 +14,19 @@ namespace Bot.Clockify
public class ClockifyController : ControllerBase
{
private readonly IProactiveBotApiKeyValidator _proactiveBotApiKeyValidator;
private readonly IRemindService _entryFillRemindService;
private readonly ISpecificRemindService _entryFillRemindService;
private readonly IBotFrameworkHttpAdapter _adapter;
private readonly IFollowUpService _followUpService;

public ClockifyController(IBotFrameworkHttpAdapter adapter,
IProactiveBotApiKeyValidator proactiveBotApiKeyValidator, IRemindServiceResolver remindServiceResolver,
IProactiveBotApiKeyValidator proactiveBotApiKeyValidator,
ISpecificRemindServiceResolver specificRemindServiceResolver,
IFollowUpService followUpService)
{
_adapter = adapter;
_proactiveBotApiKeyValidator = proactiveBotApiKeyValidator;
_followUpService = followUpService;
_entryFillRemindService = remindServiceResolver.Resolve("EntryFill");
_entryFillRemindService = specificRemindServiceResolver.Resolve("EntryFill");
}

[Route("api/timesheet/remind")]
Expand All @@ -32,7 +36,32 @@ public async Task<string> GetTimesheetRemindAsync()
string apiToken = ProactiveApiKeyUtil.Extract(Request);
_proactiveBotApiKeyValidator.Validate(apiToken);

return await _entryFillRemindService.SendReminderAsync(_adapter);
//Only use TodayReminder as default to be compatible to the old behaviour of the endpoint
var typesToRemind = SpecificRemindService.ReminderType.TodayReminder;

bool respectWorkingHours = true;

//Check, whether we should disturb the employee even if it is the mid of the day
if (Request.Query.ContainsKey("respectWorkingHours"))
{
if (Request.Query["respectWorkingHours"].Contains("true")) respectWorkingHours = true;
if (Request.Query["respectWorkingHours"].Contains("false")) respectWorkingHours = false;
}

//Check for additional query parameters. If there are available, we will only remind those reminders
if (Request.Query.ContainsKey("type"))
{
var requestedReminderTypes = Request.Query["type"];
//Check for the specific teminder types
typesToRemind = SpecificRemindService.ReminderType.NoReminder;
if (requestedReminderTypes.Contains("yesterday"))
typesToRemind |= SpecificRemindService.ReminderType.YesterdayReminder;

if (requestedReminderTypes.Contains("today"))
typesToRemind |= SpecificRemindService.ReminderType.TodayReminder;
}

return await _entryFillRemindService.SendReminderAsync(_adapter, typesToRemind, respectWorkingHours);
}

[Route("api/follow-up")]
Expand All @@ -42,7 +71,7 @@ public async Task<string> SendFollowUpAsync()
string apiToken = ProactiveApiKeyUtil.Extract(Request);
_proactiveBotApiKeyValidator.Validate(apiToken);

var followedUsers = await _followUpService.SendFollowUpAsync((BotAdapter)_adapter);
var followedUsers = await _followUpService.SendFollowUpAsync((BotAdapter)_adapter);

return $"Sent follow up to {followedUsers.Count} users";
}
Expand Down
27 changes: 24 additions & 3 deletions src/Clockify/ClockifyHandler.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Bot.Clockify.Fill;
using Bot.Clockify.Reports;
using Bot.Clockify.User;
using Bot.Common.Recognizer;
using Bot.States;
using Bot.Supports;
Expand All @@ -15,25 +18,33 @@ public class ClockifyHandler : IBotHandler
{
private readonly EntryFillDialog _fillDialog;
private readonly ReportDialog _reportDialog;
private readonly UserSettingsDialog _userSettingsDialog;
private readonly StopReminderDialog _stopReminderDialog;
private readonly ClockifySetupDialog _clockifySetupDialog;
private readonly LogoutDialog _logoutDialog;
private readonly DialogSet _dialogSet;
private readonly IStatePropertyAccessor<DialogState> _dialogState;

private readonly IEnumerable<string> _logoutIntent = new HashSet<string> { "log out", "logout" };

public ClockifyHandler(EntryFillDialog fillDialog, ReportDialog reportDialog,
StopReminderDialog stopReminderDialog, ConversationState conversationState,
ClockifySetupDialog clockifySetupDialog)
StopReminderDialog stopReminderDialog, UserSettingsDialog userSettingsDialog, ConversationState conversationState,
ClockifySetupDialog clockifySetupDialog, LogoutDialog logoutDialog)
{
_dialogState = conversationState.CreateProperty<DialogState>("ClockifyDialogState");
_fillDialog = fillDialog;
_reportDialog = reportDialog;
_userSettingsDialog = userSettingsDialog;
_stopReminderDialog = stopReminderDialog;
_clockifySetupDialog = clockifySetupDialog;
_logoutDialog = logoutDialog;
_dialogSet = new DialogSet(_dialogState)
.Add(_fillDialog)
.Add(_stopReminderDialog)
.Add(_userSettingsDialog)
.Add(_reportDialog)
.Add(_clockifySetupDialog);
.Add(_clockifySetupDialog)
.Add(_logoutDialog);
}

public async Task<bool> Handle(ITurnContext turnContext, CancellationToken cancellationToken,
Expand All @@ -47,11 +58,21 @@ public async Task<bool> Handle(ITurnContext turnContext, CancellationToken cance
var dialogContext = await _dialogSet.CreateContextAsync(turnContext, cancellationToken);

if (await RunClockifySetupIfNeeded(turnContext, cancellationToken, userProfile)) return true;

//Check for fixed intents without using LUIS
if (_logoutIntent.Contains(turnContext.Activity.Text))
{
await dialogContext.BeginDialogAsync(_logoutDialog.Id, cancellationToken: cancellationToken);
return true;
}

try
{
switch (luisResult.TopIntentWithMinScore())
{
case TimeSurveyBotLuis.Intent.SetWorkingHours:
await dialogContext.BeginDialogAsync(_userSettingsDialog.Id, luisResult, cancellationToken);
return true;
case TimeSurveyBotLuis.Intent.Report:
await dialogContext.BeginDialogAsync(_reportDialog.Id, luisResult, cancellationToken);
return true;
Expand Down
9 changes: 9 additions & 0 deletions src/Clockify/ClockifyMessageSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,8 @@ public ClockifyMessageSource(IStringLocalizer<ClockifyMessageSource> localizer)
public string TaskCreation => GetString(nameof(TaskCreation));
public string TaskAbort => GetString(nameof(TaskAbort));
public string AddEntryFeedback => GetString(nameof(AddEntryFeedback));
public string SetWorkingHoursFeedback => GetString(nameof(SetWorkingHoursFeedback));
public string SetWorkingHoursUnchangedFeedback => GetString(nameof(SetWorkingHoursUnchangedFeedback));
public string EntryFillUnderstandingError => GetString(nameof(EntryFillUnderstandingError));
public string AmbiguousProjectError => GetString(nameof(AmbiguousProjectError));
public string ProjectUnrecognized => GetString(nameof(ProjectUnrecognized));
Expand All @@ -40,12 +42,19 @@ public ClockifyMessageSource(IStringLocalizer<ClockifyMessageSource> localizer)
public string ReportDateRangeExceedOneYear => GetString(nameof(ReportDateRangeExceedOneYear));

public string FollowUp => GetString(nameof(FollowUp));

public string LogoutPrompt => GetString(nameof(LogoutPrompt));
public string LogoutYes => GetString(nameof(LogoutYes));
public string LogoutNo => GetString(nameof(LogoutNo));
public string LogoutRetryPrompt => GetString(nameof(LogoutRetryPrompt));

public string RemindStoppedAlready => GetString(nameof(RemindStoppedAlready));
public string RemindStopAnswer => GetString(nameof(RemindStopAnswer));

public string RemindEntryFill => GetString(nameof(RemindEntryFill));

public string RemindEntryFillYesterday => GetString(nameof(RemindEntryFillYesterday));

private string GetString(string name)
{
if (!_localizer[name].ResourceNotFound) return _localizer[name].Value;
Expand Down
Loading