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

Implement some bot stats #219

Merged
merged 8 commits into from Feb 14, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
@@ -1,16 +1,23 @@
using System.Linq;
using System;
using System.Linq;
using System.Threading.Tasks;
using CompatBot.Commands.Attributes;
using CompatBot.Database.Providers;
using CompatBot.Utils;
using DSharpPlus.CommandsNext;
using DSharpPlus.CommandsNext.Attributes;
using DSharpPlus.Entities;
using Microsoft.Extensions.Caching.Memory;

namespace CompatBot.Commands
{
internal class BaseCommandModuleCustom : BaseCommandModule
{
internal static readonly TimeSpan CacheTime = TimeSpan.FromDays(1);
protected static readonly MemoryCache CmdStatCache = new MemoryCache(new MemoryCacheOptions{ExpirationScanFrequency = TimeSpan.FromDays(1)});
internal static readonly MemoryCache ExplainStatCache = new MemoryCache(new MemoryCacheOptions{ExpirationScanFrequency = TimeSpan.FromDays(1)});
internal static readonly MemoryCache GameStatCache = new MemoryCache(new MemoryCacheOptions{ExpirationScanFrequency = TimeSpan.FromDays(1)});

public override async Task BeforeExecutionAsync(CommandContext ctx)
{
var disabledCmds = DisabledCommandsProvider.Get();
Expand All @@ -28,6 +35,10 @@ public override async Task BeforeExecutionAsync(CommandContext ctx)

public override async Task AfterExecutionAsync(CommandContext ctx)
{
var qualifiedName = ctx.Command.QualifiedName;
CmdStatCache.TryGetValue(qualifiedName, out int counter);
CmdStatCache.Set(qualifiedName, ++counter, CacheTime);

if (TriggersTyping(ctx))
await ctx.RemoveReactionAsync(Config.Reactions.PleaseWait).ConfigureAwait(false);

Expand Down
12 changes: 11 additions & 1 deletion CompatBot/Commands/CompatList.cs
Expand Up @@ -18,6 +18,7 @@
using DSharpPlus.Entities;
using DSharpPlus.Interactivity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;

namespace CompatBot.Commands
{
Expand Down Expand Up @@ -283,11 +284,20 @@ private IEnumerable<string> FormatSearchResults(CommandContext ctx, CompatResult
if (returnCode.displayResults)
{
var sortedList = compatResult.GetSortedList();
var searchTerm = request.search ?? @"¯\_(ツ)_/¯";
var searchHits = sortedList.Where(t => t.score > 0.5
|| (t.info.Title?.StartsWith(searchTerm, StringComparison.InvariantCultureIgnoreCase) ?? false)
|| (t.info.AlternativeTitle?.StartsWith(searchTerm, StringComparison.InvariantCultureIgnoreCase) ?? false));
foreach (var title in searchHits.Select(t => t.info?.Title).Distinct())
{
GameStatCache.TryGetValue(title, out int stat);
GameStatCache.Set(title, ++stat, CacheTime);
}
foreach (var resultInfo in sortedList.Take(request.amountRequested))
{
var info = resultInfo.AsString();
#if DEBUG
info = $"`{CompatApiResultUtils.GetScore(request.search, resultInfo.Value):0.000000}` {info}";
info = $"`{CompatApiResultUtils.GetScore(request.search, resultInfo.info):0.000000}` {info}";
#endif
result.AppendLine(info);
}
Expand Down
7 changes: 4 additions & 3 deletions CompatBot/Commands/EventsBaseCommand.cs
Expand Up @@ -443,6 +443,7 @@ private static string FixTimeString(string dateTime)
.Replace("PST", "-08:00")
.Replace("EST", "-05:00")
.Replace("BST", "-03:00")
.Replace("JST", "+09:00")
.Replace("AEST", "+10:00");
}

Expand Down Expand Up @@ -484,13 +485,13 @@ private static string FormatCountdown(TimeSpan timeSpan)
var days = (int)timeSpan.TotalDays;
if (days > 0)
timeSpan -= TimeSpan.FromDays(days);
var hours = (int) timeSpan.TotalHours;
var hours = (int)timeSpan.TotalHours;
if (hours > 0)
timeSpan -= TimeSpan.FromHours(hours);
var mins = (int) timeSpan.TotalMinutes;
var mins = (int)timeSpan.TotalMinutes;
if (mins > 0)
timeSpan -= TimeSpan.FromMinutes(mins);
var secs = (int) timeSpan.TotalSeconds;
var secs = (int)timeSpan.TotalSeconds;
if (days > 0)
result += $"{days} day{(days == 1 ? "" : "s")} ";
if (hours > 0 || days > 0)
Expand Down
3 changes: 3 additions & 0 deletions CompatBot/Commands/Explain.cs
Expand Up @@ -16,6 +16,7 @@
using DSharpPlus.Entities;
using DSharpPlus.Interactivity;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Caching.Memory;

namespace CompatBot.Commands
{
Expand Down Expand Up @@ -104,6 +105,8 @@ public async Task ShowExplanation(CommandContext ctx, [RemainingText, Descriptio
}

var explain = result.explanation;
ExplainStatCache.TryGetValue(explain.Keyword, out int stat);
ExplainStatCache.Set(explain.Keyword, ++stat, CacheTime);
await ctx.Channel.SendMessageAsync(explain.Text, explain.Attachment, explain.AttachmentFilename).ConfigureAwait(false);
return;
}
Expand Down
127 changes: 121 additions & 6 deletions CompatBot/Commands/Misc.cs
Expand Up @@ -6,10 +6,14 @@
using System.Threading.Tasks;
using CompatApiClient.Utils;
using CompatBot.Commands.Attributes;
using CompatBot.Database;
using CompatBot.EventHandlers;
using CompatBot.Utils;
using CompatBot.Utils.ResultFormatters;
using DSharpPlus.CommandsNext;
using DSharpPlus.CommandsNext.Attributes;
using DSharpPlus.Entities;
using Microsoft.Extensions.Caching.Memory;

namespace CompatBot.Commands
{
Expand Down Expand Up @@ -332,15 +336,126 @@ void MakeCustomRoleRating(DiscordMember m)
}
}

[Command("stats"), RequiresBotModRole]
[Command("stats"), Cooldown(1, 10, CooldownBucketType.Global)]
[Description("Use to look at various runtime stats")]
public async Task Stats(CommandContext ctx)
{
var result = new StringBuilder("```")
.AppendLine($"Current uptime : {Config.Uptime.Elapsed}")
.AppendLine($"Github rate limit: {GithubClient.Client.RateLimitRemaining} out of {GithubClient.Client.RateLimit}, will be reset on {GithubClient.Client.RateLimitResetTime:u}")
.Append("```");
await ctx.SendAutosplitMessageAsync(result).ConfigureAwait(false);
var embed = new DiscordEmbedBuilder
{
/*
Title = "Some bot stats",
Description = "Most stats are for the current run only, and are not persistent",
*/
Color = DiscordColor.Purple,
}
.AddField("Current uptime", Config.Uptime.Elapsed.AsShortTimespan(), true)
.AddField("Discord latency", $"{ctx.Client.Ping} ms", true)
.AddField("GitHub rate limit", $"{GithubClient.Client.RateLimitRemaining} out of {GithubClient.Client.RateLimit} calls available\nReset in {(GithubClient.Client.RateLimitResetTime - DateTime.UtcNow).AsShortTimespan()}", true)
.AddField(".NET versions", $"Runtime {System.Runtime.InteropServices.RuntimeEnvironment.GetSystemVersion()}\n{System.Runtime.InteropServices.RuntimeInformation.FrameworkDescription}", true);
AppendPiracyStats(embed);
AppendCmdStats(ctx, embed);
AppendExplainStats(embed);
AppendGameLookupStats(embed);
var ch = await ctx.GetChannelForSpamAsync().ConfigureAwait(false);
await ch.SendMessageAsync(embed: embed).ConfigureAwait(false);
}

private static void AppendPiracyStats(DiscordEmbedBuilder embed)
{
try
{
using (var db = new BotDb())
{
var longestGapBetweenWarning = db.Warning
.Where(w => w.Timestamp.HasValue)
.OrderBy(w => w.Timestamp)
.Pairwise((l, r) => r.Timestamp - l.Timestamp)
.Max();
var yesterday = DateTime.UtcNow.AddDays(-1).Ticks;
var warnCount = db.Warning.Count(w => w.Timestamp > yesterday);
var lastWarn = db.Warning.LastOrDefault()?.Timestamp;
if (lastWarn.HasValue && longestGapBetweenWarning.HasValue)
longestGapBetweenWarning = Math.Max(longestGapBetweenWarning.Value, DateTime.UtcNow.Ticks - lastWarn.Value);
var statsBuilder = new StringBuilder();
if (longestGapBetweenWarning.HasValue)
statsBuilder.AppendLine($@"Longest between warnings: {TimeSpan.FromTicks(longestGapBetweenWarning.Value).AsShortTimespan()}");
if (lastWarn.HasValue)
statsBuilder.AppendLine($@"Time since last warning: {(DateTime.UtcNow - lastWarn.Value.AsUtc()).AsShortTimespan()}");
statsBuilder.Append($"Warnings in the last day: {warnCount}");
if (warnCount == 0)
statsBuilder.Append(" ").Append(BotReactionsHandler.RandomPositiveReaction);
embed.AddField("Warning stats", statsBuilder.ToString().TrimEnd(), true);
}
}
catch (Exception e)
{
Config.Log.Warn(e);
}
}

private static void AppendCmdStats(CommandContext ctx, DiscordEmbedBuilder embed)
{
var commandStats = ctx.CommandsNext.RegisteredCommands.Values
.Select(c => c.QualifiedName)
.Distinct()
.Select(qn => (name: qn, stat: CmdStatCache.Get(qn) as int?))
.Where(t => t.stat.HasValue)
.Distinct()
.OrderByDescending(t => t.stat)
.ToList();
var totalCalls = commandStats.Sum(t => t.stat);
var top = commandStats.Take(5).ToList();
if (top.Any())
{
var statsBuilder = new StringBuilder();
var n = 1;
foreach (var cmdStat in top)
statsBuilder.AppendLine($"{n++}. {cmdStat.name} ({cmdStat.stat} call{(cmdStat.stat == 1 ? "" : "s")}, {cmdStat.stat * 100.0 / totalCalls:0.##}%)");
statsBuilder.AppendLine($"Total commands executed: {totalCalls}");
embed.AddField($"Top {top.Count} recent commands", statsBuilder.ToString().TrimEnd(), true);
}
}

private static void AppendExplainStats(DiscordEmbedBuilder embed)
{
var terms = ExplainStatCache.GetCacheKeys<string>();
var sortedTerms = terms
.Select(t => (term: t, stat: ExplainStatCache.Get(t) as int?))
.Where(t => t.stat.HasValue)
.OrderByDescending(t => t.stat)
.ToList();
var totalExplains = sortedTerms.Sum(t => t.stat);
var top = sortedTerms.Take(5).ToList();
if (top.Any())
{
var statsBuilder = new StringBuilder();
var n = 1;
foreach (var explain in top)
statsBuilder.AppendLine($"{n++}. {explain.term} ({explain.stat} display{(explain.stat == 1 ? "" : "s")}, {explain.stat * 100.0 / totalExplains:0.##}%)");
statsBuilder.AppendLine($"Total explanations shown: {totalExplains}");
embed.AddField($"Top {top.Count} recent explanations", statsBuilder.ToString().TrimEnd(), true);
}
}

private static void AppendGameLookupStats(DiscordEmbedBuilder embed)
{
var gameTitles = GameStatCache.GetCacheKeys<string>();
var sortedTitles = gameTitles
.Select(t => (title: t, stat: GameStatCache.Get(t) as int?))
.Where(t => t.stat.HasValue)
.OrderByDescending(t => t.stat)
.ToList();
var totalLookups = sortedTitles.Sum(t => t.stat);
var top = sortedTitles.Take(5).ToList();
if (top.Any())
{
var statsBuilder = new StringBuilder();
var n = 1;
foreach (var title in top)
statsBuilder.AppendLine($"{n++}. {title.title.Trim(40)} ({title.stat} search{(title.stat == 1 ? "" : "es")}, {title.stat * 100.0 / totalLookups:0.##}%)");
statsBuilder.AppendLine($"Total game lookups: {totalLookups}");
embed.AddField($"Top {top.Count} recent game lookups", statsBuilder.ToString().TrimEnd(), true);
}
}
}
}
4 changes: 4 additions & 0 deletions CompatBot/EventHandlers/BotReactionsHandler.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using CompatBot.Commands.Attributes;
using CompatBot.Utils;
Expand Down Expand Up @@ -66,6 +67,9 @@ internal static class BotReactionsHandler
private static readonly Random rng = new Random();
private static readonly object theDoor = new object();

public static DiscordEmoji RandomNegativeReaction { get { lock (theDoor) return SadReactions[rng.Next(SadReactions.Length)]; } }
public static DiscordEmoji RandomPositiveReaction { get { lock (theDoor) return ThankYouReactions[rng.Next(ThankYouReactions.Length)]; } }

public static async Task OnMessageCreated(MessageCreateEventArgs args)
{
if (DefaultHandlerFilter.IsFluff(args.Message))
Expand Down
12 changes: 9 additions & 3 deletions CompatBot/EventHandlers/IsTheGamePlayableHandler.cs
Expand Up @@ -11,6 +11,7 @@
using CompatBot.Utils.ResultFormatters;
using DSharpPlus.Entities;
using DSharpPlus.EventArgs;
using Microsoft.Extensions.Caching.Memory;

namespace CompatBot.EventHandlers
{
Expand Down Expand Up @@ -86,12 +87,17 @@ public static async Task<(string productCode, TitleInfo info)> LookupGameAsync(D
var status = await Client.GetCompatResultAsync(requestBuilder, Config.Cts.Token).ConfigureAwait(false);
if ((status.ReturnCode == 0 || status.ReturnCode == 2) && status.Results.Any())
{
var (code, info) = status.GetSortedList().First();
var score = CompatApiResultUtils.GetScore(gameTitle, info);
Config.Log.Debug($"Looked up \"{gameTitle}\", got \"{info.Title}\" with score {score}");
var (code, info, score) = status.GetSortedList().First();
Config.Log.Debug($"Looked up \"{gameTitle}\", got \"{info?.Title}\" with score {score}");
if (score < 0.5)
return (null, null);

if (!string.IsNullOrEmpty(info?.Title))
{
BaseCommandModuleCustom.GameStatCache.TryGetValue(info.Title, out int stat);
BaseCommandModuleCustom.GameStatCache.Set(info.Title, ++stat, BaseCommandModuleCustom.CacheTime);
}

return (code, info);
}
}
Expand Down
4 changes: 2 additions & 2 deletions CompatBot/EventHandlers/ProductCodeLookup.cs
Expand Up @@ -6,12 +6,14 @@
using System.Threading.Tasks;
using CompatApiClient;
using CompatApiClient.POCOs;
using CompatBot.Commands;
using CompatBot.Database.Providers;
using CompatBot.Utils;
using CompatBot.Utils.ResultFormatters;
using DSharpPlus;
using DSharpPlus.Entities;
using DSharpPlus.EventArgs;
using Microsoft.Extensions.Caching.Memory;

namespace CompatBot.EventHandlers
{
Expand Down Expand Up @@ -110,9 +112,7 @@ public static List<string> GetProductIds(string input)
public static async Task<DiscordEmbedBuilder> LookupGameInfoAsync(this DiscordClient client, string code, string gameTitle = null, bool forLog = false)
{
if (string.IsNullOrEmpty(code))
{
return TitleInfo.Unknown.AsEmbed(code, gameTitle, forLog);
}

try
{
Expand Down
6 changes: 4 additions & 2 deletions CompatBot/EventHandlers/UnknownCommandHandler.cs
@@ -1,5 +1,4 @@
using System.Collections.Generic;
using System.Linq;
using System.Linq;
using System.Threading.Tasks;
using CompatApiClient.Utils;
using CompatBot.Commands;
Expand All @@ -8,6 +7,7 @@
using CompatBot.Utils.ResultFormatters;
using DSharpPlus.CommandsNext;
using DSharpPlus.CommandsNext.Exceptions;
using Microsoft.Extensions.Caching.Memory;

namespace CompatBot.EventHandlers
{
Expand Down Expand Up @@ -54,6 +54,8 @@ public static async Task OnError(CommandErrorEventArgs e)
await e.Context.RespondAsync(fuzzyNotice).ConfigureAwait(false);
}
var explain = lookup.explanation;
BaseCommandModuleCustom.ExplainStatCache.TryGetValue(explain.Keyword, out int stat);
BaseCommandModuleCustom.ExplainStatCache.Set(explain.Keyword, ++stat, BaseCommandModuleCustom.CacheTime);
await e.Context.Channel.SendMessageAsync(explain.Text, explain.Attachment, explain.AttachmentFilename).ConfigureAwait(false);
return;
}
Expand Down
24 changes: 15 additions & 9 deletions CompatBot/Utils/CompatApiResultUtils.cs
Expand Up @@ -7,20 +7,26 @@ namespace CompatBot.Utils
{
internal static class CompatApiResultUtils
{
public static List<KeyValuePair<string, TitleInfo>> GetSortedList(this CompatResult result)
public static List<(string code, TitleInfo info, double score)> GetSortedList(this CompatResult result)
{
var search = result.RequestBuilder.search;
var sortedList = result.Results.ToList();
if (!string.IsNullOrEmpty(search))
sortedList = sortedList
.OrderByDescending(kvp => GetScore(search, kvp.Value))
.ThenBy(kvp => kvp.Value.Title)
if (string.IsNullOrEmpty(search) || !result.Results.Any())
return result.Results
.OrderBy(kvp => kvp.Value.Title)
.ThenBy(kvp => kvp.Key)
.Select(kvp => (kvp.Key, kvp.Value, 0.0))
.ToList();
if (GetScore(search, sortedList.First().Value) < 0.2)

var sortedList = result.Results
.Select(kvp => (code: kvp.Key, info: kvp.Value, score: GetScore(search, kvp.Value)))
.OrderByDescending(t => t.score)
.ThenBy(t => t.info.Title)
.ThenBy(t => t.code)
.ToList();
if (sortedList.First().score < 0.2)
sortedList = sortedList
.OrderBy(kvp => kvp.Value.Title)
.ThenBy(kvp => kvp.Key)
.OrderBy(kvp => kvp.info.Title)
.ThenBy(kvp => kvp.code)
.ToList();
return sortedList;
}
Expand Down