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
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
2 changes: 2 additions & 0 deletions src/Clockify/ClockifyMessageSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,8 @@ public ClockifyMessageSource(IStringLocalizer<ClockifyMessageSource> localizer)

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
2 changes: 2 additions & 0 deletions src/Clockify/IClockifyMessageSource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ public interface IClockifyMessageSource
string RemindStoppedAlready { get; }
string RemindStopAnswer { get; }
string RemindEntryFill { get; }

string RemindEntryFillYesterday { get; }

string FollowUp { get; }
}
Expand Down
68 changes: 68 additions & 0 deletions src/Clockify/PastDayNotComplete.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Bot.Clockify.Client;
using Bot.Common;
using Bot.Data;
using Bot.Remind;
using Bot.States;

namespace Bot.Clockify
{
public class PastDayNotComplete : INeedRemindService
{
private readonly IClockifyService _clockifyService;
private readonly ITokenRepository _tokenRepository;
private readonly IDateTimeProvider _dateTimeProvider;

public PastDayNotComplete(IClockifyService clockifyService, ITokenRepository tokenRepository,
IDateTimeProvider dateTimeProvider)
{
_clockifyService = clockifyService;
_tokenRepository = tokenRepository;
_dateTimeProvider = dateTimeProvider;
}

public async Task<bool> ReminderIsNeeded(UserProfile userProfile)
{
try
{
var tokenData = await _tokenRepository.ReadAsync(userProfile.ClockifyTokenId!);
string clockifyToken = tokenData.Value;
string userId = userProfile.UserId ?? throw new ArgumentNullException(nameof(userProfile.UserId));
var workspaces = await _clockifyService.GetWorkspacesAsync(clockifyToken);

TimeZoneInfo userTimeZone = userProfile.TimeZone;
var userNow = TimeZoneInfo.ConvertTime(_dateTimeProvider.DateTimeUtcNow(), userTimeZone);

var userStartDay = userNow.Date.AddDays(-1); //Get past day

//Check for weekends. If we got one, go back in time.
while (userStartDay.DayOfWeek == DayOfWeek.Sunday || userStartDay.DayOfWeek == DayOfWeek.Saturday)
{
userStartDay = userStartDay.AddDays(-1); //Go back in time till we have no weekend anymore
}
var userEndDay = userStartDay.AddDays(1); //Add one day to the startDay for a 1 day range

double totalHoursInserted = (await Task.WhenAll(workspaces.Select(ws =>
_clockifyService.GetHydratedTimeEntriesAsync(clockifyToken, ws.Id, userId, userStartDay,
userEndDay))))
.SelectMany(p => p)
.Sum(e =>
{
if (e.TimeInterval.End != null && e.TimeInterval.Start != null)
{
return (e.TimeInterval.End.Value - e.TimeInterval.Start.Value).TotalHours;
}

return 0;
});
return totalHoursInserted < 6;
}
catch (Exception)
{
return false;
}
}
}
}
1 change: 1 addition & 0 deletions src/Clockify/Reports/ReportUtil.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Text;
using Bot.Clockify.Models;
using Microsoft.Bot.Connector;
using Microsoft.Recognizers.Text;

namespace Bot.Clockify.Reports
{
Expand Down
3 changes: 3 additions & 0 deletions src/Common/Resources/Clockify.ClockifyMessageSource.resx
Original file line number Diff line number Diff line change
Expand Up @@ -157,4 +157,7 @@ Please don't exceed one year window</value>
<data name="FollowUp" xml:space="preserve">
<value>Hey 👋 I noticed you never setup a Clockify token...{0}{0}Help me help you! Once you're setup I will assist you in your daily time tracking.</value>
</data>
<data name="RemindEntryFillYesterday_1" xml:space="preserve">
<value>You have not filled in all of your hours for yesterday. Please do so!</value>
</data>
</root>
35 changes: 30 additions & 5 deletions src/Remind/CompositeNeedReminderService.cs
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Bot.Clockify;
using Bot.DIC;
using Bot.States;

namespace Bot.Remind
{
public interface ICompositeNeedReminderService
{
Task<bool> ReminderIsNeeded(UserProfile profile);
Task<SpecificRemindService.ReminderType> ReminderIsNeeded(UserProfile profile);
}

public class CompositeNeedReminderService: ICompositeNeedReminderService
Expand All @@ -19,10 +22,32 @@ public CompositeNeedReminderService(IEnumerable<INeedRemindService> services)
_services = services;
}

public async Task<bool> ReminderIsNeeded(UserProfile profile)
public async Task<SpecificRemindService.ReminderType> ReminderIsNeeded(UserProfile profile)
{
bool[] conditions = await Task.WhenAll(_services.Select(service => service.ReminderIsNeeded(profile)));
return conditions.All(c => c);
var reminder = SpecificRemindService.ReminderType.NoReminder;

//Check every reminder within all services
foreach (var service in _services)
{
var serviceType = typeof(PastDayNotComplete);
var reminderIsNeeded = await service.ReminderIsNeeded(profile);

//Check if the particular reminder was set to true
if (reminderIsNeeded)
{
//The reminder for this service is needed, check why it is needed and set the flags
if (service.GetType() == typeof(PastDayNotComplete)) reminder |= SpecificRemindService.ReminderType.YesterdayReminder;
if (service.GetType() == typeof(TimeSheetNotFullEnough)) reminder |= SpecificRemindService.ReminderType.TodayReminder;
}
else
{
//The reminder for this service is not needed. Therefore we check, what was negative and set the appropriate flag!
if (service.GetType() == typeof(EndOfWorkingDay)) reminder |= SpecificRemindService.ReminderType.OutOfWorkTime;
if (service.GetType() == typeof(UserDidNotSayStop)) reminder |= SpecificRemindService.ReminderType.UserSaidStop;
if (service.GetType() == typeof(NotOnLeave)) reminder |= SpecificRemindService.ReminderType.UserOnLeave;
}
}
return reminder;
}
}
}
5 changes: 3 additions & 2 deletions src/Remind/GenericRemindService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,13 +37,14 @@ public async Task<string> SendReminderAsync(IBotFrameworkHttpAdapter adapter)
{
var reminderCounter = 0;

async Task<bool> ReminderNeeded(UserProfile u) => await _compositeNeedRemindService.ReminderIsNeeded(u);
async Task<SpecificRemindService.ReminderType> ReminderNeeded(UserProfile u) => await _compositeNeedRemindService.ReminderIsNeeded(u);

List<UserProfile> userProfiles = await _userProfilesProvider.GetUserProfilesAsync();

//Fetch all users where the ReminderType is not set to "NoReminder"
List<UserProfile> userToRemind = userProfiles
.Where(u => u.ClockifyTokenId != null && u.ConversationReference != null)
.Where(u => ReminderNeeded(u).Result)
.Where(u => ReminderNeeded(u).Result != SpecificRemindService.ReminderType.NoReminder)
.ToList();

foreach (var userProfile in userToRemind)
Expand Down
11 changes: 11 additions & 0 deletions src/Remind/ISpecificRemindService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System.Threading.Tasks;
using Microsoft.Bot.Builder.Integration.AspNet.Core;

namespace Bot.Remind
{
public interface ISpecificRemindService
{
Task<string> SendReminderAsync(IBotFrameworkHttpAdapter adapter, SpecificRemindService.ReminderType reminderTypes,
bool respectWorkHours);
}
}
Loading