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

StandUpStreak - implement internal counter #345

Open
wants to merge 21 commits into
base: develop
Choose a base branch
from
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
1 change: 1 addition & 0 deletions src/HonzaBotner.Database/HonzaBotnerDbContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -27,4 +27,5 @@ protected override void OnModelCreating(ModelBuilder builder)
public DbSet<RoleBinding> RoleBindings { get; set; }
public DbSet<Warning> Warnings { get; set; }
public DbSet<Reminder> Reminders { get; set; }
public DbSet<StandUpStat> StandUpStats { get; set; }
}
17 changes: 17 additions & 0 deletions src/HonzaBotner.Database/StandUpStat.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
using System;
using System.ComponentModel.DataAnnotations;

namespace HonzaBotner.Database;

public class StandUpStat
{
[Key] public ulong UserId { get; set; }
public int Streak { get; set; }
public int LongestStreak { get; set; }
public int Freezes { get; set; }
public DateTime LastDayOfStreak { get; set; }
public int LastDayCompleted { get; set; }
public int LastDayTasks { get; set; }
public int TotalCompleted { get; set; }
public int TotalTasks { get; set; }
}
71 changes: 71 additions & 0 deletions src/HonzaBotner.Discord.Services/EventHandlers/StandUpHandler.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
using System;
using System.Threading.Tasks;
using DSharpPlus;
using DSharpPlus.Entities;
using DSharpPlus.EventArgs;
using HonzaBotner.Discord.EventHandler;
using HonzaBotner.Discord.Services.Options;
using HonzaBotner.Services.Contract;
using HonzaBotner.Services.Contract.Dto;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

namespace HonzaBotner.Discord.Services.EventHandlers;

public class StandUpHandler : IEventHandler<ComponentInteractionCreateEventArgs>
{

private readonly ButtonOptions _buttonOptions;
private readonly IStandUpStatsService _standUpStats;
private readonly ILogger<StandUpHandler> _logger;

public StandUpHandler(
IOptions<ButtonOptions> buttonOptions,
IStandUpStatsService standUpStats,
ILogger<StandUpHandler> logger)
{
_buttonOptions = buttonOptions.Value;
_standUpStats = standUpStats;
_logger = logger;
}

public async Task<EventHandlerResult> Handle(ComponentInteractionCreateEventArgs args)
{
if (args.Id != _buttonOptions.StandUpStatsId)
{
return EventHandlerResult.Continue;
}

_logger.LogDebug("{User} requested stats info", args.User.Username);

StandUpStat? stats = await _standUpStats.GetStreak(args.User.Id);

DiscordInteractionResponseBuilder response = new();
response.AsEphemeral(true);
DiscordEmbedBuilder embed = new DiscordEmbedBuilder().WithAuthor("Stats", iconUrl: args.User.AvatarUrl);

if (stats is null)
{
embed.Description = "Unfortunately you are not in the database yet.\nDatabase updates daily at 8 am";
embed.Color = new Optional<DiscordColor>(DiscordColor.Gold);
response.AddEmbed(embed.Build());

await args.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, response);
return EventHandlerResult.Stop;
}

embed.Description = $"Cool stats for {args.User.Mention}";
embed.AddField("Current streak", stats.Streak.ToString(), true);
embed.AddField("Available freezes", stats.Freezes.ToString(), true);
embed.AddField("Total tasks", stats.TotalTasks.ToString(), true);
embed.AddField("Total completed tasks", stats.TotalCompleted.ToString(), true);
embed.AddField("Last streak update",
"<t:" + ((DateTimeOffset)stats.LastDayOfStreak.AddDays(1)).ToUnixTimeSeconds() + ":D>");
embed.Color = new Optional<DiscordColor>(DiscordColor.Wheat);

response.AddEmbed(embed.Build());
await args.Interaction.CreateResponseAsync(InteractionResponseType.ChannelMessageWithSource, response);
return EventHandlerResult.Stop;
}

}
79 changes: 56 additions & 23 deletions src/HonzaBotner.Discord.Services/Jobs/StandUpJobProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using DSharpPlus;
using DSharpPlus.Entities;
using HonzaBotner.Discord.Services.Helpers;
using HonzaBotner.Discord.Services.Options;
using HonzaBotner.Scheduler.Contract;
using HonzaBotner.Services.Contract;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

Expand All @@ -20,17 +22,24 @@ public class StandUpJobProvider : IJob

private readonly DiscordWrapper _discord;

private readonly CommonCommandOptions _commonOptions;
private readonly StandUpOptions _standUpOptions;
private readonly ButtonOptions _buttonOptions;

private readonly IStandUpStatsService _statsService;

public StandUpJobProvider(
ILogger<StandUpJobProvider> logger,
DiscordWrapper discord,
IOptions<CommonCommandOptions> commonOptions
IOptions<StandUpOptions> standUpOptions,
IStandUpStatsService statsService,
IOptions<ButtonOptions> buttonOptions
)
{
_logger = logger;
_discord = discord;
_commonOptions = commonOptions.Value;
_standUpOptions = standUpOptions.Value;
_statsService = statsService;
_buttonOptions = buttonOptions.Value;
}

/// <summary>
Expand All @@ -44,32 +53,32 @@ IOptions<CommonCommandOptions> commonOptions
/// [] - normal
/// []! - critical
/// </summary>
private static readonly Regex Regex = new(@"^ *\[ *(?<State>\S*) *\] *(?<Priority>[!])?", RegexOptions.Multiline);
private static readonly Regex TaskRegex = new(@"^ *\[ *(?<State>\S*) *\] *(?<Priority>[!])?",
RegexOptions.Multiline);

private static readonly List<string> OkList = new() { "check", "done", "ok", "✅", "x" };

public string Name => "standup";

public async Task ExecuteAsync(CancellationToken cancellationToken)
{
var today = DateTime.Today; // Fix one point in time.
DateTime yesterday = today.AddDays(-1);
DateTime yesterday = DateTime.Today.AddDays(-1);

try
{
DiscordChannel channel = await _discord.Client.GetChannelAsync(_commonOptions.StandUpChannelId);
DiscordChannel channel = await _discord.Client.GetChannelAsync(_standUpOptions.StandUpChannelId);

var ok = new StandUpStats();
var fail = new StandUpStats();

List<DiscordMessage> messageList = new();
messageList.AddRange(await channel.GetMessagesAsync());
List<DiscordMessage> messageList = (await channel.GetMessagesAsync())
.Where(msg => !msg.Author.IsBot).ToList();

while (messageList.LastOrDefault()?.Timestamp.Date == yesterday)
{
int messagesCount = messageList.Count;
messageList.AddRange(
await channel.GetMessagesBeforeAsync(messageList.Last().Id)
(await channel.GetMessagesBeforeAsync(messageList.Last().Id)).Where(msg => !msg.Author.IsBot)
);

// No new data.
Expand All @@ -79,34 +88,58 @@ await channel.GetMessagesBeforeAsync(messageList.Last().Id)
}
}

foreach (DiscordMessage msg in messageList.Where(msg => msg.Timestamp.Date == yesterday))
foreach (var authorGrouped in messageList.Where(msg => msg.Timestamp.Date == yesterday)
.GroupBy(msg => msg.Author.Id))
{
foreach (Match match in Regex.Matches(msg.Content))
int total = 0;
int completed = 0;

foreach (var msg in authorGrouped)
{
string state = match.Groups["State"].ToString();
string priority = match.Groups["Priority"].ToString();

if (OkList.Any(s => state.Contains(s)))
foreach (Match match in TaskRegex.Matches(msg.Content))
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
foreach (Match match in TaskRegex.Matches(msg.Content))
foreach (var match in TaskRegex.Matches(msg.Content))

to keep it consistent with previous loop.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This actually turned everything red, so I kept it the way it is now.. For some reason once we substitute Mtch for var, it no longer knows that var contains some groups...

{
ok.Increment(priority);
}
else
{
fail.Increment(priority);
total++;
string state = match.Groups["State"].Value;
string priority = match.Groups["Priority"].Value;

if (OkList.Any(s => state.Contains(s)))
{
completed++;
ok.Increment(priority);
}
else
{
fail.Increment(priority);
}
}
}

// Update DB.
if (total != 0)
{
await _statsService.UpdateStats(authorGrouped.Key, completed, total);
}
}

await channel.SendMessageAsync($@"
Stand-up time, <@&{_commonOptions.StandUpRoleId}>!
// Send stats message to channel.
DiscordMessageBuilder message = new DiscordMessageBuilder().WithContent($@"
Stand-up time, <@&{_standUpOptions.StandUpRoleId}>!

Results from <t:{((DateTimeOffset)today.AddDays(-1)).ToUnixTimeSeconds()}:D>:
Results from <t:{((DateTimeOffset)yesterday).ToUnixTimeSeconds()}:D>:
```
all: {ok.Add(fail)}
completed: {ok}
failed: {fail}
```
");
if (_buttonOptions.StandUpStatsId is not null)
{
message.AddComponents(new DiscordButtonComponent(ButtonStyle.Primary, _buttonOptions.StandUpStatsId,
"Get your stats", false, new DiscordComponentEmoji(DiscordEmoji.FromUnicode("🐸"))));
}

await message.SendAsync(channel);
}
catch (Exception e)
{
Expand Down
1 change: 1 addition & 0 deletions src/HonzaBotner.Discord.Services/Options/ButtonOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,6 @@ public class ButtonOptions
public string? VerificationId { get; set; }
public string? StaffVerificationId { get; set; }
public string? StaffRemoveRoleId { get; set; }
public string? StandUpStatsId { get; set; }
public ulong[]? CzechChannelsIds { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,5 @@ public class CommonCommandOptions

public ulong[]? ReactionIgnoreChannels { get; set; }

public ulong StandUpRoleId { get; set; }
public ulong StandUpChannelId { get; set; }

public ulong[] MemberCountAllowlistIds { get; set; } = System.Array.Empty<ulong>();
}
12 changes: 12 additions & 0 deletions src/HonzaBotner.Discord.Services/Options/StandUpOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
namespace HonzaBotner.Discord.Services.Options;

public class StandUpOptions
{
public static string ConfigName => "StandUpOptions";

public ulong StandUpRoleId { get; set; }
public ulong StandUpChannelId { get; set; }

public int DaysToAcquireFreeze { get; set; }
public int TasksCompletedThreshold { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public static IServiceCollection AddCommandOptions(this IServiceCollection servi
services.Configure<PinOptions>(configuration.GetSection(PinOptions.ConfigName));
services.Configure<InfoOptions>(configuration.GetSection(InfoOptions.ConfigName));
services.Configure<ReminderOptions>(configuration.GetSection(ReminderOptions.ConfigName));
services.Configure<StandUpOptions>(configuration.GetSection(StandUpOptions.ConfigName));
services.Configure<ButtonOptions>(configuration.GetSection(ButtonOptions.ConfigName));
services.Configure<BadgeRoleOptions>(configuration.GetSection(BadgeRoleOptions.ConfigName));

Expand Down
11 changes: 11 additions & 0 deletions src/HonzaBotner.Services.Contract/Dto/StandUpStat.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
using System;

namespace HonzaBotner.Services.Contract.Dto;

public record StandUpStat(
ulong UserId, int Streak,
int LongestStreak, int Freezes,
DateTime LastDayOfStreak, int TotalCompleted,
int TotalTasks, int LastDayCompleted,
int LastDayTasks
);
21 changes: 21 additions & 0 deletions src/HonzaBotner.Services.Contract/IStandUpStatsService.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
using System.Threading.Tasks;
using HonzaBotner.Services.Contract.Dto;
namespace HonzaBotner.Services.Contract;

public interface IStandUpStatsService
{
/// <summary>
/// Get StandUp stats for a user with a given userId
/// </summary>
/// <param name="userId">Id of the user</param>
/// <returns>Null if user not in database, else his stats</returns>
Task<StandUpStat?> GetStreak(ulong userId);

/// <summary>
/// Update database record of given user regarding StandUp stats. Should be called just once per day per user.
/// </summary>
/// <param name="userId">Id of the user</param>
/// <param name="completed">Amount of completed tasks yesterday</param>
/// <param name="total">Total amount of tasks yesterday</param>
Task UpdateStats(ulong userId, int completed, int total);
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\HonzaBotner.Discord.Services\HonzaBotner.Discord.Services.csproj" />
<ProjectReference Include="..\HonzaBotner.Scheduler\HonzaBotner.Scheduler.csproj" />
<ProjectReference Include="..\HonzaBotner.Services.Contract\HonzaBotner.Services.Contract.csproj" />
<ProjectReference Include="..\HonzaBotner.Services\HonzaBotner.Services.csproj" />
Expand Down
1 change: 1 addition & 0 deletions src/HonzaBotner.Services/HonzaBotner.Services.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\HonzaBotner.Database\HonzaBotner.Database.csproj" />
<ProjectReference Include="..\HonzaBotner.Discord.Services\HonzaBotner.Discord.Services.csproj" />
<ProjectReference Include="..\HonzaBotner.Discord\HonzaBotner.Discord.csproj" />
<ProjectReference Include="..\HonzaBotner.Services.Contract\HonzaBotner.Services.Contract.csproj" />
</ItemGroup>
Expand Down
1 change: 1 addition & 0 deletions src/HonzaBotner.Services/ServiceCollectionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ public static IServiceCollection AddBotnerServices(this IServiceCollection servi
serviceCollection.AddScoped<IRoleBindingsService, RoleBindingsService>();
serviceCollection.AddScoped<IWarningService, WarningService>();
serviceCollection.AddScoped<IRemindersService, RemindersService>();
serviceCollection.AddScoped<IStandUpStatsService, StandUpStatsService>();

return serviceCollection;
}
Expand Down
Loading