Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Logout feature added #122

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
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
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,52 @@ Make sure to provide required configuration values. The app currently needs a va
"MicrosoftAppPassword": ""
}
```
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")
* 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

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

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

### 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)

#### 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)

### 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.

### 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
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.
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
20 changes: 2 additions & 18 deletions src/Clockify/EntryFillRemindService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,30 +8,14 @@

namespace Bot.Clockify
{
public class EntryFillRemindService : GenericRemindService
public class EntryFillRemindService : SpecificRemindService
{
private static BotCallbackHandler BotCallbackMaker(Func<string> getResource)
{
return async (turn, token) =>
{
string text = getResource();
if (Uri.IsWellFormedUriString(text, UriKind.RelativeOrAbsolute))
{
// TODO: support other content types
await turn.SendActivityAsync(MessageFactory.Attachment(new Attachment("image/png", text)), token);
}
else
{
await turn.SendActivityAsync(MessageFactory.Text(text), token);
}
};
}

public EntryFillRemindService(IUserProfilesProvider userProfilesProvider, IConfiguration configuration,
ICompositeNeedReminderService compositeNeedRemindService, IClockifyMessageSource messageSource,
ILogger<EntryFillRemindService> logger) :
base(userProfilesProvider, configuration, compositeNeedRemindService,
BotCallbackMaker(() => messageSource.RemindEntryFill), logger)
messageSource, logger)
{
}
}
Expand Down
9 changes: 9 additions & 0 deletions src/Clockify/IClockifyMessageSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ public interface IClockifyMessageSource
string TaskCreation { get; }
string TaskAbort { get; }
string AddEntryFeedback { get; }
string SetWorkingHoursFeedback { get; }
string SetWorkingHoursUnchangedFeedback { get; }
string EntryFillUnderstandingError { get; }
string AmbiguousProjectError { get; }
string ProjectUnrecognized { get; }
Expand All @@ -29,7 +31,14 @@ public interface IClockifyMessageSource
string RemindStoppedAlready { get; }
string RemindStopAnswer { get; }
string RemindEntryFill { get; }

string RemindEntryFillYesterday { get; }

string FollowUp { get; }

string LogoutPrompt { get; }
string LogoutYes { get; }
string LogoutNo { get; }
string LogoutRetryPrompt { get; }
}
}
98 changes: 98 additions & 0 deletions src/Clockify/LogoutDialog.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Bot.Data;
using Bot.States;
using Microsoft.Bot.Builder;
using Microsoft.Bot.Builder.Dialogs;
using Microsoft.Bot.Schema;

namespace Bot.Clockify
{
public class LogoutDialog : ComponentDialog
{
private const string LogoutWaterfall = nameof(LogoutWaterfall);
private readonly UserState _userState;
private readonly IClockifyMessageSource _messageSource;
private readonly ITokenRepository _tokenRepository;

private const string Yes = "yes";
private const string No = "no";

public LogoutDialog(UserState userState, IClockifyMessageSource messageSource, ITokenRepository tokenRepository)
{
_userState = userState;
_messageSource = messageSource;
_tokenRepository = tokenRepository;
AddDialog(new WaterfallDialog(LogoutWaterfall, new List<WaterfallStep>
{
ConfirmationStep,
LogoutStep
}));
AddDialog(new TextPrompt(nameof(ConfirmationStep), LogoutValidator));
Id = nameof(LogoutDialog);
}

private async Task<DialogTurnResult> ConfirmationStep(WaterfallStepContext stepContext,
CancellationToken cancellationToken)
{
var suggestions = new List<CardAction>
{
new CardAction
{
Title = Yes, Type = ActionTypes.MessageBack, Text = Yes, Value = Yes,
DisplayText = Yes
},
new CardAction
{
Title = No, Type = ActionTypes.MessageBack, Text = No, Value = No,
DisplayText = No
}
};
var activity = MessageFactory.Text(_messageSource.LogoutPrompt);
activity.SuggestedActions = new SuggestedActions { Actions = suggestions };
return await stepContext.PromptAsync(nameof(ConfirmationStep), new PromptOptions
{
Prompt = activity,
RetryPrompt = MessageFactory.Text(_messageSource.LogoutRetryPrompt),
}, cancellationToken);
}

private async Task<DialogTurnResult> LogoutStep(WaterfallStepContext stepContext,
CancellationToken cancellationToken)
{
var result = stepContext.Result.ToString();
switch (result?.ToLower())
{
case Yes:
var userProfile =
await StaticUserProfileHelper.GetUserProfileAsync(_userState, stepContext.Context,
cancellationToken);

//Removes the token from the repository! This change reflects immediateley also within all caches
//and also on the remote key vault!
await _tokenRepository.RemoveAsync(userProfile.ClockifyTokenId!);

//Now we can also remove the tokenID from the UserProfile
userProfile.ClockifyTokenId = null;
await stepContext.Context.SendActivityAsync(
MessageFactory.Text(_messageSource.LogoutYes), cancellationToken);
break;
case No:
await stepContext.Context.SendActivityAsync(MessageFactory.Text(_messageSource.LogoutNo),
cancellationToken);
break;
}

return await stepContext.EndDialogAsync(null, cancellationToken);
}

private static Task<bool> LogoutValidator(PromptValidatorContext<string> promptContext,
CancellationToken cancellationToken)
{
string? pValue = promptContext.Recognized.Value;
return Task.FromResult(!string.IsNullOrEmpty(pValue) &&
(pValue.ToLower() == Yes || pValue.ToLower() == No));
}
}
}
Loading