Skip to content

Commit

Permalink
Merge pull request #219 from 13xforever/vnext
Browse files Browse the repository at this point in the history
Implement some bot stats
  • Loading branch information
13xforever committed Feb 14, 2019
2 parents 96d24bc + 8e9e0d6 commit 3cbfc62
Show file tree
Hide file tree
Showing 15 changed files with 324 additions and 29 deletions.
@@ -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

0 comments on commit 3cbfc62

Please sign in to comment.