diff --git a/.gitignore b/.gitignore index 40364a4c..f95212c1 100644 --- a/.gitignore +++ b/.gitignore @@ -262,3 +262,4 @@ __pycache__/ launchSettings.json bot.db .vscode/ +thumbs.db diff --git a/CompatApiClient/Client.cs b/CompatApiClient/Client.cs index df6359bc..ddb0ebf3 100644 --- a/CompatApiClient/Client.cs +++ b/CompatApiClient/Client.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using CompatApiClient.Compression; using CompatApiClient.POCOs; +using CompatApiClient.Utils; using Newtonsoft.Json; namespace CompatApiClient @@ -32,39 +33,62 @@ namespace CompatApiClient //todo: cache results public async Task GetCompatResultAsync(RequestBuilder requestBuilder, CancellationToken cancellationToken) { - var message = new HttpRequestMessage(HttpMethod.Get, requestBuilder.Build()); var startTime = DateTime.UtcNow; - CompatResult result; - try + var url = requestBuilder.Build(); + var tries = 0; + do { - var response = await client.SendAsync(message, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false); - result = await response.Content.ReadAsAsync(formatters, cancellationToken).ConfigureAwait(false); - } - catch (Exception e) - { - PrintError(e); - result = new CompatResult{ReturnCode = -2}; - } - result.RequestBuilder = requestBuilder; - result.RequestDuration = DateTime.UtcNow - startTime; - return result; + try + { + using (var message = new HttpRequestMessage(HttpMethod.Get, url)) + using (var response = await client.SendAsync(message, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false)) + try + { + await response.Content.LoadIntoBufferAsync().ConfigureAwait(false); + var result = await response.Content.ReadAsAsync(formatters, cancellationToken).ConfigureAwait(false); + result.RequestBuilder = requestBuilder; + result.RequestDuration = DateTime.UtcNow - startTime; + return result; + } + catch (Exception e) + { + ConsoleLogger.PrintError(e, response, ConsoleColor.Yellow); + } + } + catch (Exception e) + { + ConsoleLogger.PrintError(e, null, ConsoleColor.Yellow); + } + tries++; + } while (tries < 3); + throw new HttpRequestException("Couldn't communicate with the API"); } public async Task GetUpdateAsync(CancellationToken cancellationToken, string commit = "somecommit") { - var message = new HttpRequestMessage(HttpMethod.Get, "https://update.rpcs3.net/?c=" + commit); - UpdateInfo result; - try + var tries = 3; + do { - var response = await client.SendAsync(message, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false); - result = await response.Content.ReadAsAsync(formatters, cancellationToken).ConfigureAwait(false); - } - catch (Exception e) - { - PrintError(e); - result = new UpdateInfo { ReturnCode = -2 }; - } - return result; + try + { + using (var message = new HttpRequestMessage(HttpMethod.Get, "https://update.rpcs3.net/?c=" + commit)) + using (var response = await client.SendAsync(message, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false)) + try + { + return await response.Content.ReadAsAsync(formatters, cancellationToken).ConfigureAwait(false); + } + catch (Exception e) + { + ConsoleLogger.PrintError(e, response, ConsoleColor.Yellow); + } + } + catch (Exception e) + { + ConsoleLogger.PrintError(e, null, ConsoleColor.Yellow); + } + tries++; + } while (tries < 3); + return null; } public async Task GetPrInfoAsync(string pr, CancellationToken cancellationToken) @@ -72,23 +96,33 @@ namespace CompatApiClient if (prInfoCache.TryGetValue(pr, out var result)) return result; - var message = new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/repos/RPCS3/rpcs3/pulls/" + pr); - HttpContent content = null; try { - message.Headers.UserAgent.Add(new ProductInfoHeaderValue("RPCS3CompatibilityBot", "2.0")); - var response = await client.SendAsync(message, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false); - content = response.Content; - await content.LoadIntoBufferAsync().ConfigureAwait(false); - result = await content.ReadAsAsync(formatters, cancellationToken).ConfigureAwait(false); + using (var message = new HttpRequestMessage(HttpMethod.Get, "https://api.github.com/repos/RPCS3/rpcs3/pulls/" + pr)) + { + message.Headers.UserAgent.Add(new ProductInfoHeaderValue("RPCS3CompatibilityBot", "2.0")); + using (var response = await client.SendAsync(message, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false)) + { + try + { + await response.Content.LoadIntoBufferAsync().ConfigureAwait(false); + result = await response.Content.ReadAsAsync(formatters, cancellationToken).ConfigureAwait(false); + } + catch (Exception e) + { + ConsoleLogger.PrintError(e, response); + } + } + } } catch (Exception e) { - PrintError(e); - if (content != null) - try { Console.WriteLine(await content.ReadAsStringAsync().ConfigureAwait(false)); } catch {} + ConsoleLogger.PrintError(e, null); + } + if (result == null) + { int.TryParse(pr, out var prnum); - return new PrInfo{Number = prnum}; + return new PrInfo { Number = prnum }; } lock (prInfoCache) @@ -100,12 +134,5 @@ namespace CompatApiClient return result; } } - - private void PrintError(Exception e) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine("Error communicating with api: " + e.Message); - Console.ResetColor(); - } } } \ No newline at end of file diff --git a/CompatApiClient/POCOs/PrInfo.cs b/CompatApiClient/POCOs/PrInfo.cs index 2cd4bdb4..525d7b8d 100644 --- a/CompatApiClient/POCOs/PrInfo.cs +++ b/CompatApiClient/POCOs/PrInfo.cs @@ -1,8 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace CompatApiClient.POCOs +namespace CompatApiClient.POCOs { public class PrInfo { @@ -16,6 +12,6 @@ namespace CompatApiClient.POCOs public class GithubUser { - public string login; + public string Login; } } diff --git a/CompatApiClient/POCOs/UpdateInfo.cs b/CompatApiClient/POCOs/UpdateInfo.cs index 1036604f..c162f77b 100644 --- a/CompatApiClient/POCOs/UpdateInfo.cs +++ b/CompatApiClient/POCOs/UpdateInfo.cs @@ -1,6 +1,4 @@ -using System; - -namespace CompatApiClient.POCOs +namespace CompatApiClient.POCOs { public class UpdateInfo { diff --git a/CompatApiClient/Utils/ConsoleLogger.cs b/CompatApiClient/Utils/ConsoleLogger.cs new file mode 100644 index 00000000..181b51a2 --- /dev/null +++ b/CompatApiClient/Utils/ConsoleLogger.cs @@ -0,0 +1,28 @@ +using System; +using System.Net.Http; + +namespace CompatApiClient.Utils +{ + public static class ConsoleLogger + { + + public static void PrintError(Exception e, HttpResponseMessage response, ConsoleColor color = ConsoleColor.Red) + { + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine("HTTP error: " + e); + if (response != null) + { + try + { + var msg = response.Content.ReadAsStringAsync().GetAwaiter().GetResult(); + Console.ResetColor(); + Console.WriteLine(response.RequestMessage.RequestUri); + Console.ForegroundColor = ConsoleColor.Yellow; + Console.WriteLine(msg); + } + catch { } + } + Console.ResetColor(); + } + } +} diff --git a/CompatApiClient/Utils/Utils.cs b/CompatApiClient/Utils/Utils.cs index 08afb139..9fc9237b 100644 --- a/CompatApiClient/Utils/Utils.cs +++ b/CompatApiClient/Utils/Utils.cs @@ -1,6 +1,6 @@ using System; -namespace CompatApiClient +namespace CompatApiClient.Utils { public static class Utils { diff --git a/CompatBot/Commands/Antipiracy.cs b/CompatBot/Commands/Antipiracy.cs index d5e73157..cc6dd9be 100644 --- a/CompatBot/Commands/Antipiracy.cs +++ b/CompatBot/Commands/Antipiracy.cs @@ -24,8 +24,9 @@ namespace CompatBot.Commands var result = new StringBuilder("```") .AppendLine("ID | Trigger") .AppendLine("-----------------------------"); - foreach (var item in await BotDb.Instance.Piracystring.ToListAsync().ConfigureAwait(false)) - result.AppendLine($"{item.Id:0000} | {item.String}"); + using (var db = new BotDb()) + foreach (var item in await db.Piracystring.ToListAsync().ConfigureAwait(false)) + result.AppendLine($"{item.Id:0000} | {item.String}"); await ctx.SendAutosplitMessageAsync(result.Append("```")).ConfigureAwait(false); await typingTask; } diff --git a/CompatBot/Commands/Attributes/LimitedToHelpChannel.cs b/CompatBot/Commands/Attributes/LimitedToHelpChannel.cs new file mode 100644 index 00000000..7f16dcc0 --- /dev/null +++ b/CompatBot/Commands/Attributes/LimitedToHelpChannel.cs @@ -0,0 +1,22 @@ +using System; +using System.Threading.Tasks; +using DSharpPlus.CommandsNext; +using DSharpPlus.CommandsNext.Attributes; + +namespace CompatBot.Commands.Attributes +{ + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)] + internal class LimitedToHelpChannel: CheckBaseAttribute + { + public override Task ExecuteCheckAsync(CommandContext ctx, bool help) + { + if (ctx.Channel.IsPrivate || help) + return Task.FromResult(true); + + if (ctx.Channel.Name.Equals("help", StringComparison.InvariantCultureIgnoreCase)) + return Task.FromResult(true); + + return Task.FromResult(false); + } + } +} \ No newline at end of file diff --git a/CompatBot/Commands/Attributes/LimitedToSpamChannel.cs b/CompatBot/Commands/Attributes/LimitedToSpamChannel.cs new file mode 100644 index 00000000..a07b3c63 --- /dev/null +++ b/CompatBot/Commands/Attributes/LimitedToSpamChannel.cs @@ -0,0 +1,22 @@ +using System; +using System.Threading.Tasks; +using DSharpPlus.CommandsNext; +using DSharpPlus.CommandsNext.Attributes; + +namespace CompatBot.Commands.Attributes +{ + [AttributeUsage(AttributeTargets.Method | AttributeTargets.Class, Inherited = false)] + internal class LimitedToSpamChannel: CheckBaseAttribute + { + public override Task ExecuteCheckAsync(CommandContext ctx, bool help) + { + if (ctx.Channel.IsPrivate || help) + return Task.FromResult(true); + + if (ctx.Channel.Name.Contains("spam", StringComparison.InvariantCultureIgnoreCase)) + return Task.FromResult(true); + + return Task.FromResult(false); + } + } +} \ No newline at end of file diff --git a/CompatBot/Commands/CompatList.cs b/CompatBot/Commands/CompatList.cs index 5fab6816..e3c4ad3c 100644 --- a/CompatBot/Commands/CompatList.cs +++ b/CompatBot/Commands/CompatList.cs @@ -5,6 +5,7 @@ using System.Text; using System.Threading.Tasks; using CompatApiClient; using CompatApiClient.POCOs; +using CompatApiClient.Utils; using CompatBot.Utils; using CompatBot.Utils.ResultFormatters; using DSharpPlus; @@ -101,15 +102,13 @@ Example usage: [Command("latest"), Aliases("download")] [Description("Provides links to the latest RPCS3 build")] + [Cooldown(1, 30, CooldownBucketType.Channel)] public async Task Latest(CommandContext ctx) { - var getDmTask = ctx.CreateDmAsync(); - if (ctx.Channel.IsPrivate) - await ctx.TriggerTypingAsync().ConfigureAwait(false); + await ctx.TriggerTypingAsync().ConfigureAwait(false); var info = await client.GetUpdateAsync(Config.Cts.Token).ConfigureAwait(false); var embed = await info.AsEmbedAsync().ConfigureAwait(false); - var dm = await getDmTask.ConfigureAwait(false); - await dm.SendMessageAsync(embed: embed.Build()).ConfigureAwait(false); + await ctx.RespondAsync(embed: embed.Build()).ConfigureAwait(false); } private static string DicToDesc(Dictionary dictionary) @@ -129,7 +128,17 @@ Example usage: { var botChannelTask = ctx.Client.GetChannelAsync(Config.BotChannelId); Console.WriteLine(requestBuilder.Build()); - var result = await client.GetCompatResultAsync(requestBuilder, Config.Cts.Token).ConfigureAwait(false); + CompatResult result; + try + { + result = await client.GetCompatResultAsync(requestBuilder, Config.Cts.Token).ConfigureAwait(false); + } + catch + { + await ctx.RespondAsync(embed: TitleInfo.CommunicationError.AsEmbed(null)).ConfigureAwait(false); + return; + } + var botChannel = await botChannelTask.ConfigureAwait(false); foreach (var msg in FormatSearchResults(ctx, result)) await botChannel.SendAutosplitMessageAsync(msg).ConfigureAwait(false); diff --git a/CompatBot/Commands/Explain.cs b/CompatBot/Commands/Explain.cs index 2930a852..2e5c0beb 100644 --- a/CompatBot/Commands/Explain.cs +++ b/CompatBot/Commands/Explain.cs @@ -28,11 +28,14 @@ namespace CompatBot.Commands return; } - var explanation = await BotDb.Instance.Explanation.FirstOrDefaultAsync(e => e.Keyword == term).ConfigureAwait(false); - if (explanation != null) + using (var db = new BotDb()) { - await ctx.RespondAsync(explanation.Text).ConfigureAwait(false); - return; + var explanation = await db.Explanation.FirstOrDefaultAsync(e => e.Keyword == term).ConfigureAwait(false); + if (explanation != null) + { + await ctx.RespondAsync(explanation.Text).ConfigureAwait(false); + return; + } } term = term.StripQuotes(); @@ -50,11 +53,14 @@ namespace CompatBot.Commands if (hasMention) { term = term.Substring(0, idx).TrimEnd(); - explanation = await BotDb.Instance.Explanation.FirstOrDefaultAsync(e => e.Keyword == term).ConfigureAwait(false); - if (explanation != null) + using (var db = new BotDb()) { - await ctx.RespondAsync(explanation.Text).ConfigureAwait(false); - return; + var explanation = await db.Explanation.FirstOrDefaultAsync(e => e.Keyword == term).ConfigureAwait(false); + if (explanation != null) + { + await ctx.RespondAsync(explanation.Text).ConfigureAwait(false); + return; + } } } } @@ -76,16 +82,22 @@ namespace CompatBot.Commands ctx.RespondAsync("An explanation for the term must be provided") ).ConfigureAwait(false); } - else if (await BotDb.Instance.Explanation.AnyAsync(e => e.Keyword == term).ConfigureAwait(false)) - await Task.WhenAll( - ctx.Message.CreateReactionAsync(Config.Reactions.Failure), - ctx.RespondAsync($"'{term}' is already defined. Use `update` to update an existing term.") - ).ConfigureAwait(false); else { - await BotDb.Instance.Explanation.AddAsync(new Explanation {Keyword = term, Text = explanation}).ConfigureAwait(false); - await BotDb.Instance.SaveChangesAsync().ConfigureAwait(false); - await ctx.Message.CreateReactionAsync(Config.Reactions.Success).ConfigureAwait(false); + using (var db = new BotDb()) + { + if (await db.Explanation.AnyAsync(e => e.Keyword == term).ConfigureAwait(false)) + await Task.WhenAll( + ctx.Message.CreateReactionAsync(Config.Reactions.Failure), + ctx.RespondAsync($"'{term}' is already defined. Use `update` to update an existing term.") + ).ConfigureAwait(false); + else + { + await db.Explanation.AddAsync(new Explanation {Keyword = term, Text = explanation}).ConfigureAwait(false); + await db.SaveChangesAsync().ConfigureAwait(false); + await ctx.Message.CreateReactionAsync(Config.Reactions.Success).ConfigureAwait(false); + } + } } } @@ -96,19 +108,22 @@ namespace CompatBot.Commands [RemainingText, Description("New explanation text")] string explanation) { term = term.StripQuotes(); - var item = await BotDb.Instance.Explanation.FirstOrDefaultAsync(e => e.Keyword == term).ConfigureAwait(false); - if (item == null) + using (var db = new BotDb()) { - await Task.WhenAll( - ctx.Message.CreateReactionAsync(Config.Reactions.Failure), - ctx.RespondAsync($"Term '{term}' is not defined") - ).ConfigureAwait(false); - } - else - { - item.Text = explanation; - await BotDb.Instance.SaveChangesAsync().ConfigureAwait(false); - await ctx.Message.CreateReactionAsync(Config.Reactions.Success).ConfigureAwait(false); + var item = await db.Explanation.FirstOrDefaultAsync(e => e.Keyword == term).ConfigureAwait(false); + if (item == null) + { + await Task.WhenAll( + ctx.Message.CreateReactionAsync(Config.Reactions.Failure), + ctx.RespondAsync($"Term '{term}' is not defined") + ).ConfigureAwait(false); + } + else + { + item.Text = explanation; + await db.SaveChangesAsync().ConfigureAwait(false); + await ctx.Message.CreateReactionAsync(Config.Reactions.Success).ConfigureAwait(false); + } } } @@ -119,19 +134,22 @@ namespace CompatBot.Commands { oldTerm = oldTerm.StripQuotes(); newTerm = newTerm.StripQuotes(); - var item = await BotDb.Instance.Explanation.FirstOrDefaultAsync(e => e.Keyword == oldTerm).ConfigureAwait(false); - if (item == null) + using (var db = new BotDb()) { - await Task.WhenAll( - ctx.Message.CreateReactionAsync(Config.Reactions.Failure), - ctx.RespondAsync($"Term '{oldTerm}' is not defined") - ).ConfigureAwait(false); - } - else - { - item.Keyword = newTerm; - await BotDb.Instance.SaveChangesAsync().ConfigureAwait(false); - await ctx.Message.CreateReactionAsync(Config.Reactions.Success).ConfigureAwait(false); + var item = await db.Explanation.FirstOrDefaultAsync(e => e.Keyword == oldTerm).ConfigureAwait(false); + if (item == null) + { + await Task.WhenAll( + ctx.Message.CreateReactionAsync(Config.Reactions.Failure), + ctx.RespondAsync($"Term '{oldTerm}' is not defined") + ).ConfigureAwait(false); + } + else + { + item.Keyword = newTerm; + await db.SaveChangesAsync().ConfigureAwait(false); + await ctx.Message.CreateReactionAsync(Config.Reactions.Success).ConfigureAwait(false); + } } } @@ -148,24 +166,27 @@ namespace CompatBot.Commands await ctx.Message.CreateReactionAsync(Config.Reactions.Failure).ConfigureAwait(false); } - [Command("list")] + [Command("list"), LimitedToSpamChannel] [Description("List all known terms that could be used for !explain command")] public async Task List(CommandContext ctx) { await ctx.TriggerTypingAsync().ConfigureAwait(false); - var keywords = await BotDb.Instance.Explanation.Select(e => e.Keyword).OrderBy(t => t).ToListAsync().ConfigureAwait(false); - if (keywords.Count == 0) - await ctx.RespondAsync("Nothing has been defined yet").ConfigureAwait(false); - else - try - { - foreach (var embed in new EmbedPager().BreakInEmbeds(new DiscordEmbedBuilder {Title = "Defined terms", Color = Config.Colors.Help}, keywords)) - await ctx.RespondAsync(embed: embed).ConfigureAwait(false); - } - catch (Exception e) - { - ctx.Client.DebugLogger.LogMessage(LogLevel.Error, "", e.ToString(), DateTime.Now); - } + using (var db = new BotDb()) + { + var keywords = await db.Explanation.Select(e => e.Keyword).OrderBy(t => t).ToListAsync().ConfigureAwait(false); + if (keywords.Count == 0) + await ctx.RespondAsync("Nothing has been defined yet").ConfigureAwait(false); + else + try + { + foreach (var embed in new EmbedPager().BreakInEmbeds(new DiscordEmbedBuilder {Title = "Defined terms", Color = Config.Colors.Help}, keywords)) + await ctx.RespondAsync(embed: embed).ConfigureAwait(false); + } + catch (Exception e) + { + ctx.Client.DebugLogger.LogMessage(LogLevel.Error, "", e.ToString(), DateTime.Now); + } + } } @@ -174,19 +195,22 @@ namespace CompatBot.Commands public async Task Remove(CommandContext ctx, [RemainingText, Description("Term to remove")] string term) { term = term.StripQuotes(); - var item = await BotDb.Instance.Explanation.FirstOrDefaultAsync(e => e.Keyword == term).ConfigureAwait(false); - if (item == null) + using (var db = new BotDb()) { - await Task.WhenAll( - ctx.Message.CreateReactionAsync(Config.Reactions.Failure), - ctx.RespondAsync($"Term '{term}' is not defined") - ).ConfigureAwait(false); - } - else - { - BotDb.Instance.Explanation.Remove(item); - await BotDb.Instance.SaveChangesAsync().ConfigureAwait(false); - await ctx.Message.CreateReactionAsync(Config.Reactions.Success).ConfigureAwait(false); + var item = await db.Explanation.FirstOrDefaultAsync(e => e.Keyword == term).ConfigureAwait(false); + if (item == null) + { + await Task.WhenAll( + ctx.Message.CreateReactionAsync(Config.Reactions.Failure), + ctx.RespondAsync($"Term '{term}' is not defined") + ).ConfigureAwait(false); + } + else + { + db.Explanation.Remove(item); + await db.SaveChangesAsync().ConfigureAwait(false); + await ctx.Message.CreateReactionAsync(Config.Reactions.Success).ConfigureAwait(false); + } } } diff --git a/CompatBot/Commands/Misc.cs b/CompatBot/Commands/Misc.cs index d5b792fd..e39492af 100644 --- a/CompatBot/Commands/Misc.cs +++ b/CompatBot/Commands/Misc.cs @@ -77,30 +77,47 @@ namespace CompatBot.Commands } [Command("roll")] - [Description("Generates a random number between 1 and N (default 10). Can also roll dices like `2d6`")] - public async Task Roll(CommandContext ctx, [Description("Some positive number or a dice")] string something) + [Description("Generates a random number between 1 and N. Can also roll dices like `2d6`. Default is 1d6")] + public Task Roll(CommandContext ctx) + { + return Roll(ctx, 6); + } + + [Command("roll")] + public async Task Roll(CommandContext ctx, [Description("Some positive number")] int maxValue) + { + string result = null; + if (maxValue > 1) + lock (rng) result = (rng.Next(maxValue) + 1).ToString(); + if (string.IsNullOrEmpty(result)) + await ctx.Message.CreateReactionAsync(DiscordEmoji.FromUnicode("💩")).ConfigureAwait(false); + else + await ctx.RespondAsync(result).ConfigureAwait(false); + } + + [Command("roll")] + public async Task Roll(CommandContext ctx, [Description("Dices to roll (i.e. 2d6 for two 6-sided dices)")] string dices) { var result = ""; - switch (something) + if (dices is string dice && Regex.IsMatch(dice, @"\d+d\d+")) { - case string val when int.TryParse(val, out var maxValue) && maxValue > 1: - lock (rng) result = (rng.Next(maxValue) + 1).ToString(); - break; - case string dice when Regex.IsMatch(dice, @"\d+d\d+"): - var typingTask = ctx.TriggerTypingAsync(); - var diceParts = dice.Split('d', StringSplitOptions.RemoveEmptyEntries); - if (int.TryParse(diceParts[0], out var num) && int.TryParse(diceParts[1], out var face) && - 0 < num && num < 101 && - 1 < face && face < 1001) + var typingTask = ctx.TriggerTypingAsync(); + var diceParts = dice.Split('d', StringSplitOptions.RemoveEmptyEntries); + if (int.TryParse(diceParts[0], out var num) && int.TryParse(diceParts[1], out var face) && + 0 < num && num < 101 && + 1 < face && face < 1001) + { + List rolls; + lock (rng) rolls = Enumerable.Range(0, num).Select(_ => rng.Next(face) + 1).ToList(); + if (rolls.Count > 1) { - List rolls; - lock (rng) rolls = Enumerable.Range(0, num).Select(_ => rng.Next(face) + 1).ToList(); result = "Total: " + rolls.Sum(); - if (rolls.Count > 1) - result += "\nRolls: " + string.Join(' ', rolls); + result += "\nRolls: " + string.Join(' ', rolls); } - await typingTask.ConfigureAwait(false); - break; + else + result = rolls.Sum().ToString(); + } + await typingTask.ConfigureAwait(false); } if (string.IsNullOrEmpty(result)) await ctx.Message.CreateReactionAsync(DiscordEmoji.FromUnicode("💩")).ConfigureAwait(false); diff --git a/CompatBot/Commands/Sudo.Fix.cs b/CompatBot/Commands/Sudo.Fix.cs index 04bce78d..62530190 100644 --- a/CompatBot/Commands/Sudo.Fix.cs +++ b/CompatBot/Commands/Sudo.Fix.cs @@ -28,18 +28,21 @@ namespace CompatBot.Commands try { var @fixed = 0; - foreach (var warning in BotDb.Instance.Warning) - if (!string.IsNullOrEmpty(warning.FullReason)) - { - var match = Timestamp.Match(warning.FullReason); - if (match.Success && DateTime.TryParse(match.Groups["date"].Value, out var timestamp)) + using (var db = new BotDb()) + { + foreach (var warning in db.Warning) + if (!string.IsNullOrEmpty(warning.FullReason)) { - warning.Timestamp = timestamp.Ticks; - warning.FullReason = warning.FullReason.Substring(match.Groups["cutout"].Value.Length); - @fixed++; + var match = Timestamp.Match(warning.FullReason); + if (match.Success && DateTime.TryParse(match.Groups["date"].Value, out var timestamp)) + { + warning.Timestamp = timestamp.Ticks; + warning.FullReason = warning.FullReason.Substring(match.Groups["cutout"].Value.Length); + @fixed++; + } } - } - await BotDb.Instance.SaveChangesAsync().ConfigureAwait(false); + await db.SaveChangesAsync().ConfigureAwait(false); + } await ctx.RespondAsync($"Fixed {@fixed} records").ConfigureAwait(false); } catch (Exception e) @@ -57,16 +60,19 @@ namespace CompatBot.Commands try { var @fixed = 0; - foreach (var warning in BotDb.Instance.Warning) + using (var db = new BotDb()) { - var newReason = await FixChannelMentionAsync(ctx, warning.Reason).ConfigureAwait(false); - if (newReason != warning.Reason) + foreach (var warning in db.Warning) { - warning.Reason = newReason; - @fixed++; + var newReason = await FixChannelMentionAsync(ctx, warning.Reason).ConfigureAwait(false); + if (newReason != warning.Reason) + { + warning.Reason = newReason; + @fixed++; + } } + await db.SaveChangesAsync().ConfigureAwait(false); } - await BotDb.Instance.SaveChangesAsync().ConfigureAwait(false); await ctx.RespondAsync($"Fixed {@fixed} records").ConfigureAwait(false); } catch (Exception e) diff --git a/CompatBot/Commands/Warnings.ListGroup.cs b/CompatBot/Commands/Warnings.ListGroup.cs index 3d5d112f..a2b340a5 100644 --- a/CompatBot/Commands/Warnings.ListGroup.cs +++ b/CompatBot/Commands/Warnings.ListGroup.cs @@ -2,7 +2,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -using CompatApiClient; +using CompatApiClient.Utils; using CompatBot.Commands.Attributes; using CompatBot.Database; using CompatBot.Database.Providers; @@ -57,18 +57,22 @@ namespace CompatBot.Commands var result = new StringBuilder("Warning count per user:").AppendLine("```") .AppendLine(header) .AppendLine("".PadLeft(header.Length, '-')); - var query = from warn in BotDb.Instance.Warning - group warn by warn.DiscordId into userGroup - let row = new { discordId = userGroup.Key, count = userGroup.Count() } - orderby row.count descending - select row; - foreach (var row in query) + using (var db = new BotDb()) { - var username = await ctx.GetUserNameAsync(row.discordId).ConfigureAwait(false); - result.Append($"{username,-25} | "); - if (ctx.Channel.IsPrivate) - result.Append($"{row.discordId,-18} | "); - result.AppendLine($"{row.count,2}"); + var query = from warn in db.Warning + group warn by warn.DiscordId + into userGroup + let row = new {discordId = userGroup.Key, count = userGroup.Count()} + orderby row.count descending + select row; + foreach (var row in query) + { + var username = await ctx.GetUserNameAsync(row.discordId).ConfigureAwait(false); + result.Append($"{username,-25} | "); + if (ctx.Channel.IsPrivate) + result.Append($"{row.discordId,-18} | "); + result.AppendLine($"{row.count,2}"); + } } await ctx.SendAutosplitMessageAsync(result.Append("```")).ConfigureAwait(false); } @@ -85,21 +89,24 @@ namespace CompatBot.Commands var result = new StringBuilder("Last issued warnings:").AppendLine("```") .AppendLine(header) .AppendLine("".PadLeft(header.Length, '-')); - var query = from warn in BotDb.Instance.Warning - orderby warn.Id descending - select warn; - foreach (var row in query.Take(number)) + using (var db = new BotDb()) { - var username = await ctx.GetUserNameAsync(row.DiscordId).ConfigureAwait(false); - var modname = await ctx.GetUserNameAsync(row.IssuerId, defaultName: "Unknown mod").ConfigureAwait(false); - result.Append($"{row.Id:00000} | {username,-25} | "); - if (ctx.Channel.IsPrivate) - result.Append($"{row.DiscordId,-18} | "); - var timestamp = row.Timestamp.HasValue ? new DateTime(row.Timestamp.Value, DateTimeKind.Utc).ToString("u") : null; - result.Append($"{modname,-15} | {timestamp,-20} | {row.Reason}"); - if (ctx.Channel.IsPrivate) - result.Append(" | ").Append(row.FullReason); - result.AppendLine(); + var query = from warn in db.Warning + orderby warn.Id descending + select warn; + foreach (var row in query.Take(number)) + { + var username = await ctx.GetUserNameAsync(row.DiscordId).ConfigureAwait(false); + var modname = await ctx.GetUserNameAsync(row.IssuerId, defaultName: "Unknown mod").ConfigureAwait(false); + result.Append($"{row.Id:00000} | {username,-25} | "); + if (ctx.Channel.IsPrivate) + result.Append($"{row.DiscordId,-18} | "); + var timestamp = row.Timestamp.HasValue ? new DateTime(row.Timestamp.Value, DateTimeKind.Utc).ToString("u") : null; + result.Append($"{modname,-15} | {timestamp,-20} | {row.Reason}"); + if (ctx.Channel.IsPrivate) + result.Append(" | ").Append(row.FullReason); + result.AppendLine(); + } } await ctx.SendAutosplitMessageAsync(result.Append("```")).ConfigureAwait(false); } diff --git a/CompatBot/Commands/Warnings.cs b/CompatBot/Commands/Warnings.cs index aaf9a109..f97fd8ee 100644 --- a/CompatBot/Commands/Warnings.cs +++ b/CompatBot/Commands/Warnings.cs @@ -2,7 +2,7 @@ using System.Linq; using System.Text; using System.Threading.Tasks; -using CompatApiClient; +using CompatApiClient.Utils; using CompatBot.Commands.Attributes; using CompatBot.Database; using CompatBot.Utils; @@ -53,9 +53,13 @@ namespace CompatBot.Commands public async Task Remove(CommandContext ctx, [Description("Warning IDs to remove separated with space")] params int[] ids) { var typingTask = ctx.TriggerTypingAsync(); - var warningsToRemove = await BotDb.Instance.Warning.Where(w => ids.Contains(w.Id)).ToListAsync().ConfigureAwait(false); - BotDb.Instance.Warning.RemoveRange(warningsToRemove); - var removedCount = await BotDb.Instance.SaveChangesAsync().ConfigureAwait(false); + int removedCount; + using (var db = new BotDb()) + { + var warningsToRemove = await db.Warning.Where(w => ids.Contains(w.Id)).ToListAsync().ConfigureAwait(false); + db.Warning.RemoveRange(warningsToRemove); + removedCount = await db.SaveChangesAsync().ConfigureAwait(false); + } (DiscordEmoji reaction, string msg) result = removedCount == ids.Length ? (Config.Reactions.Success, $"Warning{(ids.Length == 1 ? "" : "s")} successfully removed!") : (Config.Reactions.Failure, $"Removed {removedCount} items, but was asked to remove {ids.Length}"); @@ -80,9 +84,13 @@ namespace CompatBot.Commands { var typingTask = ctx.TriggerTypingAsync(); //var removed = await BotDb.Instance.Database.ExecuteSqlCommandAsync($"DELETE FROM `warning` WHERE `discord_id`={userId}").ConfigureAwait(false); - var warningsToRemove = await BotDb.Instance.Warning.Where(w => w.DiscordId == userId).ToListAsync().ConfigureAwait(false); - BotDb.Instance.Warning.RemoveRange(warningsToRemove); - var removed = await BotDb.Instance.SaveChangesAsync().ConfigureAwait(false); + int removed; + using (var db = new BotDb()) + { + var warningsToRemove = await db.Warning.Where(w => w.DiscordId == userId).ToListAsync().ConfigureAwait(false); + db.Warning.RemoveRange(warningsToRemove); + removed = await db.SaveChangesAsync().ConfigureAwait(false); + } await Task.WhenAll( ctx.RespondAsync($"{removed} warning{(removed == 1 ? "" : "s")} successfully removed!"), ctx.Message.CreateReactionAsync(Config.Reactions.Success), @@ -111,9 +119,13 @@ namespace CompatBot.Commands } try { - await BotDb.Instance.Warning.AddAsync(new Warning {DiscordId = userId, IssuerId = issuer.Id, Reason = reason, FullReason = fullReason ?? "", Timestamp = DateTime.UtcNow.Ticks}).ConfigureAwait(false); - await BotDb.Instance.SaveChangesAsync().ConfigureAwait(false); - var count = await BotDb.Instance.Warning.CountAsync(w => w.DiscordId == userId).ConfigureAwait(false); + int count; + using (var db = new BotDb()) + { + await db.Warning.AddAsync(new Warning {DiscordId = userId, IssuerId = issuer.Id, Reason = reason, FullReason = fullReason ?? "", Timestamp = DateTime.UtcNow.Ticks}).ConfigureAwait(false); + await db.SaveChangesAsync().ConfigureAwait(false); + count = await db.Warning.CountAsync(w => w.DiscordId == userId).ConfigureAwait(false); + } await message.RespondAsync($"User warning saved! User currently has {count} warning{(count % 10 == 1 && count % 100 != 11 ? "" : "s")}!").ConfigureAwait(false); if (count > 1) await ListUserWarningsAsync(client, message, userId, userName).ConfigureAwait(false); @@ -130,7 +142,9 @@ namespace CompatBot.Commands private static async Task ListUserWarningsAsync(DiscordClient client, DiscordMessage message, ulong userId, string userName, bool skipIfOne = true) { await message.Channel.TriggerTypingAsync().ConfigureAwait(false); - var count = await BotDb.Instance.Warning.CountAsync(w => w.DiscordId == userId).ConfigureAwait(false); + int count; + using (var db = new BotDb()) + count = await db.Warning.CountAsync(w => w.DiscordId == userId).ConfigureAwait(false); if (count == 0) { await message.RespondAsync(userName + " has no warnings, is a standup citizen, and a pillar of this community").ConfigureAwait(false); @@ -148,15 +162,16 @@ namespace CompatBot.Commands header += " | Full reason"; result.AppendLine(header) .AppendLine("".PadLeft(header.Length, '-')); - foreach (var warning in BotDb.Instance.Warning.Where(w => w.DiscordId == userId)) - { - var issuerName = warning.IssuerId == 0 ? "" : await client.GetUserNameAsync(message.Channel, warning.IssuerId, isPrivate, "unknown mod").ConfigureAwait(false); - var timestamp = warning.Timestamp.HasValue ? new DateTime(warning.Timestamp.Value, DateTimeKind.Utc).ToString("u") : null; - result.Append($"{warning.Id:00000} | {issuerName,-15} | {timestamp,-20} | {warning.Reason}"); - if (isPrivate) - result.Append(" | ").Append(warning.FullReason); - result.AppendLine(); - } + using(var db = new BotDb()) + foreach (var warning in db.Warning.Where(w => w.DiscordId == userId)) + { + var issuerName = warning.IssuerId == 0 ? "" : await client.GetUserNameAsync(message.Channel, warning.IssuerId, isPrivate, "unknown mod").ConfigureAwait(false); + var timestamp = warning.Timestamp.HasValue ? new DateTime(warning.Timestamp.Value, DateTimeKind.Utc).ToString("u") : null; + result.Append($"{warning.Id:00000} | {issuerName,-15} | {timestamp,-20} | {warning.Reason}"); + if (isPrivate) + result.Append(" | ").Append(warning.FullReason); + result.AppendLine(); + } await message.Channel.SendAutosplitMessageAsync(result.Append("```")).ConfigureAwait(false); } } diff --git a/CompatBot/CompatBot.csproj b/CompatBot/CompatBot.csproj index 34298d9a..6f0e87fa 100644 --- a/CompatBot/CompatBot.csproj +++ b/CompatBot/CompatBot.csproj @@ -15,9 +15,9 @@ - - - + + + @@ -28,6 +28,7 @@ + diff --git a/CompatBot/Config.cs b/CompatBot/Config.cs index f93f339d..1885a444 100644 --- a/CompatBot/Config.cs +++ b/CompatBot/Config.cs @@ -14,6 +14,7 @@ namespace CompatBot public static readonly ulong BotLogId = 436972161572536329; public static readonly ulong BotRulesChannelId = 311894275015049216; public static readonly ulong BotAdminId = 267367850706993152; + public static readonly ulong ThumbnailSpamId = 474163354232029197; public static readonly int ProductCodeLookupHistoryThrottle = 7; diff --git a/CompatBot/Database/BotDb.cs b/CompatBot/Database/BotDb.cs index 0caf3dbd..5ae5beaf 100644 --- a/CompatBot/Database/BotDb.cs +++ b/CompatBot/Database/BotDb.cs @@ -1,5 +1,4 @@ -using System; -using System.ComponentModel.DataAnnotations; +using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using CompatApiClient; using Microsoft.EntityFrameworkCore; @@ -8,9 +7,6 @@ namespace CompatBot.Database { internal class BotDb: DbContext { - private static readonly Lazy instance = new Lazy(() => new BotDb()); - public static BotDb Instance => instance.Value; - public DbSet Moderator { get; set; } public DbSet Piracystring { get; set; } public DbSet Warning { get; set; } diff --git a/CompatBot/Database/DbImporter.cs b/CompatBot/Database/DbImporter.cs index a9bb2f90..aaea997f 100644 --- a/CompatBot/Database/DbImporter.cs +++ b/CompatBot/Database/DbImporter.cs @@ -10,11 +10,11 @@ namespace CompatBot.Database { internal static class DbImporter { - public static async Task UpgradeAsync(BotDb dbContext, CancellationToken cancellationToken) + public static async Task UpgradeAsync(DbContext dbContext, CancellationToken cancellationToken) { try { - Console.WriteLine("Upgrading database if needed..."); + Console.WriteLine($"Upgrading {dbContext.GetType().Name} database if needed..."); await dbContext.Database.MigrateAsync(cancellationToken).ConfigureAwait(false); } catch (SqliteException e) @@ -23,14 +23,17 @@ namespace CompatBot.Database Console.WriteLine(e.Message); Console.WriteLine("Database upgrade failed, probably importing an unversioned one."); Console.ResetColor(); + if (!(dbContext is BotDb botDb)) + return false; + Console.WriteLine("Trying to apply a manual fixup..."); try { - await ImportAsync(dbContext, cancellationToken).ConfigureAwait(false); + await ImportAsync(botDb, cancellationToken).ConfigureAwait(false); Console.ForegroundColor = ConsoleColor.Green; Console.WriteLine("Manual fixup worked great. Let's try migrations again..."); Console.ResetColor(); - await dbContext.Database.MigrateAsync(cancellationToken).ConfigureAwait(false); + await botDb.Database.MigrateAsync(cancellationToken).ConfigureAwait(false); } catch (Exception ex) @@ -42,9 +45,9 @@ namespace CompatBot.Database return false; } } - if (!await dbContext.Moderator.AnyAsync(m => m.DiscordId == Config.BotAdminId, cancellationToken).ConfigureAwait(false)) + if (dbContext is BotDb botDb2 && !await botDb2.Moderator.AnyAsync(m => m.DiscordId == Config.BotAdminId, cancellationToken).ConfigureAwait(false)) { - await dbContext.Moderator.AddAsync(new Moderator {DiscordId = Config.BotAdminId, Sudoer = true}, cancellationToken).ConfigureAwait(false); + await botDb2.Moderator.AddAsync(new Moderator {DiscordId = Config.BotAdminId, Sudoer = true}, cancellationToken).ConfigureAwait(false); await dbContext.SaveChangesAsync(cancellationToken).ConfigureAwait(false); } Console.WriteLine("Database is ready."); diff --git a/CompatBot/Database/Migrations/20180709153348_InitialCreate.Designer.cs b/CompatBot/Database/Migrations/BotDb/20180709153348_InitialCreate.Designer.cs similarity index 100% rename from CompatBot/Database/Migrations/20180709153348_InitialCreate.Designer.cs rename to CompatBot/Database/Migrations/BotDb/20180709153348_InitialCreate.Designer.cs diff --git a/CompatBot/Database/Migrations/20180709153348_InitialCreate.cs b/CompatBot/Database/Migrations/BotDb/20180709153348_InitialCreate.cs similarity index 100% rename from CompatBot/Database/Migrations/20180709153348_InitialCreate.cs rename to CompatBot/Database/Migrations/BotDb/20180709153348_InitialCreate.cs diff --git a/CompatBot/Database/Migrations/20180709154128_Explanations.Designer.cs b/CompatBot/Database/Migrations/BotDb/20180709154128_Explanations.Designer.cs similarity index 100% rename from CompatBot/Database/Migrations/20180709154128_Explanations.Designer.cs rename to CompatBot/Database/Migrations/BotDb/20180709154128_Explanations.Designer.cs diff --git a/CompatBot/Database/Migrations/20180709154128_Explanations.cs b/CompatBot/Database/Migrations/BotDb/20180709154128_Explanations.cs similarity index 100% rename from CompatBot/Database/Migrations/20180709154128_Explanations.cs rename to CompatBot/Database/Migrations/BotDb/20180709154128_Explanations.cs diff --git a/CompatBot/Database/Migrations/20180719122730_WarningTimestamp.Designer.cs b/CompatBot/Database/Migrations/BotDb/20180719122730_WarningTimestamp.Designer.cs similarity index 100% rename from CompatBot/Database/Migrations/20180719122730_WarningTimestamp.Designer.cs rename to CompatBot/Database/Migrations/BotDb/20180719122730_WarningTimestamp.Designer.cs diff --git a/CompatBot/Database/Migrations/20180719122730_WarningTimestamp.cs b/CompatBot/Database/Migrations/BotDb/20180719122730_WarningTimestamp.cs similarity index 100% rename from CompatBot/Database/Migrations/20180719122730_WarningTimestamp.cs rename to CompatBot/Database/Migrations/BotDb/20180719122730_WarningTimestamp.cs diff --git a/CompatBot/Database/Migrations/BotDbModelSnapshot.cs b/CompatBot/Database/Migrations/BotDb/BotDbModelSnapshot.cs similarity index 100% rename from CompatBot/Database/Migrations/BotDbModelSnapshot.cs rename to CompatBot/Database/Migrations/BotDb/BotDbModelSnapshot.cs diff --git a/CompatBot/Database/Migrations/ThumbnailDb/20180801095653_InitialCreate.Designer.cs b/CompatBot/Database/Migrations/ThumbnailDb/20180801095653_InitialCreate.Designer.cs new file mode 100644 index 00000000..f020b631 --- /dev/null +++ b/CompatBot/Database/Migrations/ThumbnailDb/20180801095653_InitialCreate.Designer.cs @@ -0,0 +1,82 @@ +// +using CompatBot.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace CompatBot.Migrations +{ + [DbContext(typeof(ThumbnailDb))] + [Migration("20180801095653_InitialCreate")] + partial class InitialCreate + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.1-rtm-30846"); + + modelBuilder.Entity("CompatBot.Database.State", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id"); + + b.Property("Locale") + .HasColumnName("locale"); + + b.Property("Timestamp") + .HasColumnName("timestamp"); + + b.HasKey("Id") + .HasName("id"); + + b.HasIndex("Locale") + .IsUnique() + .HasName("state_locale"); + + b.HasIndex("Timestamp") + .HasName("state_timestamp"); + + b.ToTable("state"); + }); + + modelBuilder.Entity("CompatBot.Database.Thumbnail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id"); + + b.Property("ContentId") + .HasColumnName("content_id"); + + b.Property("EmbeddableUrl") + .HasColumnName("embeddable_url"); + + b.Property("ProductCode") + .IsRequired() + .HasColumnName("product_code"); + + b.Property("Timestamp") + .HasColumnName("timestamp"); + + b.Property("Url") + .HasColumnName("url"); + + b.HasKey("Id") + .HasName("id"); + + b.HasIndex("ProductCode") + .IsUnique() + .HasName("thumbnail_product_code"); + + b.HasIndex("Timestamp") + .HasName("thumbnail_timestamp"); + + b.ToTable("thumbnail"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CompatBot/Database/Migrations/ThumbnailDb/20180801095653_InitialCreate.cs b/CompatBot/Database/Migrations/ThumbnailDb/20180801095653_InitialCreate.cs new file mode 100644 index 00000000..64499623 --- /dev/null +++ b/CompatBot/Database/Migrations/ThumbnailDb/20180801095653_InitialCreate.cs @@ -0,0 +1,72 @@ +using Microsoft.EntityFrameworkCore.Migrations; + +namespace CompatBot.Migrations +{ + public partial class InitialCreate : Migration + { + protected override void Up(MigrationBuilder migrationBuilder) + { + migrationBuilder.CreateTable( + name: "state", + columns: table => new + { + id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + locale = table.Column(nullable: true), + timestamp = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("id", x => x.id); + }); + + migrationBuilder.CreateTable( + name: "thumbnail", + columns: table => new + { + id = table.Column(nullable: false) + .Annotation("Sqlite:Autoincrement", true), + product_code = table.Column(nullable: false), + content_id = table.Column(nullable: true), + url = table.Column(nullable: true), + embeddable_url = table.Column(nullable: true), + timestamp = table.Column(nullable: false) + }, + constraints: table => + { + table.PrimaryKey("id", x => x.id); + }); + + migrationBuilder.CreateIndex( + name: "state_locale", + table: "state", + column: "locale", + unique: true); + + migrationBuilder.CreateIndex( + name: "state_timestamp", + table: "state", + column: "timestamp"); + + migrationBuilder.CreateIndex( + name: "thumbnail_product_code", + table: "thumbnail", + column: "product_code", + unique: true); + + migrationBuilder.CreateIndex( + name: "thumbnail_timestamp", + table: "thumbnail", + column: "timestamp"); + } + + protected override void Down(MigrationBuilder migrationBuilder) + { + migrationBuilder.DropTable( + name: "state"); + + migrationBuilder.DropTable( + name: "thumbnail"); + } + } +} diff --git a/CompatBot/Database/Migrations/ThumbnailDb/ThumbnailDbModelSnapshot.cs b/CompatBot/Database/Migrations/ThumbnailDb/ThumbnailDbModelSnapshot.cs new file mode 100644 index 00000000..84930561 --- /dev/null +++ b/CompatBot/Database/Migrations/ThumbnailDb/ThumbnailDbModelSnapshot.cs @@ -0,0 +1,80 @@ +// +using CompatBot.Database; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; + +namespace CompatBot.Migrations +{ + [DbContext(typeof(ThumbnailDb))] + partial class ThumbnailDbModelSnapshot : ModelSnapshot + { + protected override void BuildModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "2.1.1-rtm-30846"); + + modelBuilder.Entity("CompatBot.Database.State", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id"); + + b.Property("Locale") + .HasColumnName("locale"); + + b.Property("Timestamp") + .HasColumnName("timestamp"); + + b.HasKey("Id") + .HasName("id"); + + b.HasIndex("Locale") + .IsUnique() + .HasName("state_locale"); + + b.HasIndex("Timestamp") + .HasName("state_timestamp"); + + b.ToTable("state"); + }); + + modelBuilder.Entity("CompatBot.Database.Thumbnail", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnName("id"); + + b.Property("ContentId") + .HasColumnName("content_id"); + + b.Property("EmbeddableUrl") + .HasColumnName("embeddable_url"); + + b.Property("ProductCode") + .IsRequired() + .HasColumnName("product_code"); + + b.Property("Timestamp") + .HasColumnName("timestamp"); + + b.Property("Url") + .HasColumnName("url"); + + b.HasKey("Id") + .HasName("id"); + + b.HasIndex("ProductCode") + .IsUnique() + .HasName("thumbnail_product_code"); + + b.HasIndex("Timestamp") + .HasName("thumbnail_timestamp"); + + b.ToTable("thumbnail"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/CompatBot/Database/PrimaryKeyConvention.cs b/CompatBot/Database/PrimaryKeyConvention.cs index 0d5cd48e..32531b11 100644 --- a/CompatBot/Database/PrimaryKeyConvention.cs +++ b/CompatBot/Database/PrimaryKeyConvention.cs @@ -25,7 +25,8 @@ namespace CompatBot.Database foreach (var entity in modelBuilder.Model.GetEntityTypes()) { var pk = entity.GetKeys().FirstOrDefault(k => k.IsPrimaryKey()); - entity.RemoveKey(pk.Properties); + if (pk != null) + entity.RemoveKey(pk.Properties); } } } diff --git a/CompatBot/Database/Providers/ModProvider.cs b/CompatBot/Database/Providers/ModProvider.cs index a52fd251..219536ac 100644 --- a/CompatBot/Database/Providers/ModProvider.cs +++ b/CompatBot/Database/Providers/ModProvider.cs @@ -8,11 +8,12 @@ namespace CompatBot.Database.Providers internal static class ModProvider { private static readonly Dictionary mods; + private static readonly BotDb db = new BotDb(); public static ReadOnlyDictionary Mods => new ReadOnlyDictionary(mods); static ModProvider() { - mods = BotDb.Instance.Moderator.ToDictionary(m => m.DiscordId, m => m); + mods = db.Moderator.ToDictionary(m => m.DiscordId, m => m); } public static bool IsMod(ulong userId) => mods.ContainsKey(userId); @@ -24,7 +25,6 @@ namespace CompatBot.Database.Providers if (IsMod(userId)) return false; - var db = BotDb.Instance; var result = await db.Moderator.AddAsync(new Moderator {DiscordId = userId}).ConfigureAwait(false); await db.SaveChangesAsync().ConfigureAwait(false); lock (mods) @@ -41,7 +41,6 @@ namespace CompatBot.Database.Providers if (!mods.TryGetValue(userId, out var mod)) return false; - var db = BotDb.Instance; db.Moderator.Remove(mod); await db.SaveChangesAsync().ConfigureAwait(false); lock (mods) @@ -60,7 +59,7 @@ namespace CompatBot.Database.Providers return false; mod.Sudoer = true; - await BotDb.Instance.SaveChangesAsync().ConfigureAwait(false); + await db.SaveChangesAsync().ConfigureAwait(false); return true; } @@ -70,7 +69,7 @@ namespace CompatBot.Database.Providers return false; mod.Sudoer = false; - await BotDb.Instance.SaveChangesAsync().ConfigureAwait(false); + await db.SaveChangesAsync().ConfigureAwait(false); return true; } } diff --git a/CompatBot/Database/Providers/PiracyStringProvider.cs b/CompatBot/Database/Providers/PiracyStringProvider.cs index 5fe05830..da556e4b 100644 --- a/CompatBot/Database/Providers/PiracyStringProvider.cs +++ b/CompatBot/Database/Providers/PiracyStringProvider.cs @@ -9,13 +9,14 @@ namespace CompatBot.Database.Providers { internal static class PiracyStringProvider { + private static readonly BotDb db = new BotDb(); private static readonly object SyncObj = new object(); private static readonly List PiracyStrings; private static AhoCorasickDoubleArrayTrie matcher; static PiracyStringProvider() { - PiracyStrings = BotDb.Instance.Piracystring.Select(ps => ps.String).ToList(); + PiracyStrings = db.Piracystring.Select(ps => ps.String).ToList(); RebuildMatcher(); } @@ -32,7 +33,6 @@ namespace CompatBot.Database.Providers PiracyStrings.Add(trigger); RebuildMatcher(); } - var db = BotDb.Instance; await db.Piracystring.AddAsync(new Piracystring {String = trigger}).ConfigureAwait(false); await db.SaveChangesAsync().ConfigureAwait(false); return true; @@ -40,7 +40,6 @@ namespace CompatBot.Database.Providers public static async Task RemoveAsync(int id) { - var db = BotDb.Instance; var dbItem = await db.Piracystring.FirstOrDefaultAsync(ps => ps.Id == id).ConfigureAwait(false); if (dbItem == null) return false; diff --git a/CompatBot/Database/Providers/ScrapeStateProvider.cs b/CompatBot/Database/Providers/ScrapeStateProvider.cs new file mode 100644 index 00000000..5488be78 --- /dev/null +++ b/CompatBot/Database/Providers/ScrapeStateProvider.cs @@ -0,0 +1,73 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +namespace CompatBot.Database.Providers +{ + internal static class ScrapeStateProvider + { + private static readonly TimeSpan CheckInterval = TimeSpan.FromDays(15); + + public static bool IsFresh(long timestamp) + { + return IsFresh(new DateTime(timestamp, DateTimeKind.Utc)); + } + + public static bool IsFresh(DateTime timestamp) + { + return timestamp.Add(CheckInterval) > DateTime.UtcNow; + } + + public static bool IsFresh(string locale, string containerId = null) + { + var id = GetId(locale, containerId); + using (var db = new ThumbnailDb()) + { + var timestamp = string.IsNullOrEmpty(id) ? db.State.OrderBy(s => s.Timestamp).FirstOrDefault() : db.State.FirstOrDefault(s => s.Locale == id); + if (timestamp?.Timestamp is long checkDate && checkDate > 0) + return IsFresh(new DateTime(checkDate, DateTimeKind.Utc)); + } + return false; + } + + public static async Task SetLastRunTimestampAsync(string locale, string containerId = null) + { + if (string.IsNullOrEmpty(locale)) + throw new ArgumentException(nameof(locale)); + + var id = GetId(locale, containerId); + using (var db = new ThumbnailDb()) + { + var timestamp = db.State.FirstOrDefault(s => s.Locale == id); + if (timestamp == null) + db.State.Add(new State {Locale = id, Timestamp = DateTime.UtcNow.Ticks}); + else + timestamp.Timestamp = DateTime.UtcNow.Ticks; + await db.SaveChangesAsync().ConfigureAwait(false); + } + } + + public static async Task CleanAsync(CancellationToken cancellationToken) + { + using (var db = new ThumbnailDb()) + { + var latestTimestamp = db.State.OrderByDescending(s => s.Timestamp).FirstOrDefault()?.Timestamp; + if (!latestTimestamp.HasValue) + return; + + var cutOff = new DateTime(latestTimestamp.Value, DateTimeKind.Utc).Add(-CheckInterval); + var oldItems = db.State.Where(s => s.Timestamp < cutOff.Ticks); + db.State.RemoveRange(oldItems); + await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false); + } + } + + private static string GetId(string locale, string containerId) + { + if (string.IsNullOrEmpty(locale) || string.IsNullOrEmpty(containerId)) + return locale; + return $"{locale} - {containerId}"; + } + } +} diff --git a/CompatBot/Database/Providers/ThumbnailProvider.cs b/CompatBot/Database/Providers/ThumbnailProvider.cs new file mode 100644 index 00000000..72a13fca --- /dev/null +++ b/CompatBot/Database/Providers/ThumbnailProvider.cs @@ -0,0 +1,53 @@ +using System; +using System.IO; +using System.Linq; +using System.Net.Http; +using System.Threading.Tasks; +using DSharpPlus; +using Microsoft.EntityFrameworkCore; + +namespace CompatBot.Database.Providers +{ + internal static class ThumbnailProvider + { + public static async Task GetThumbnailUrlAsync(this DiscordClient client, string productCode) + { + using (var db = new ThumbnailDb()) + { + var thumb = await db.Thumbnail.FirstOrDefaultAsync(t => t.ProductCode == productCode.ToUpperInvariant()).ConfigureAwait(false); + //todo: add search task if not found + if (thumb?.EmbeddableUrl is string embeddableUrl && !string.IsNullOrEmpty(embeddableUrl)) + return embeddableUrl; + + if (thumb?.Url is string url && !string.IsNullOrEmpty(url)) + { + if (!string.IsNullOrEmpty(Path.GetExtension(url))) + { + thumb.EmbeddableUrl = url; + await db.SaveChangesAsync().ConfigureAwait(false); + return url; + } + + try + { + using (var httpClient = new HttpClient()) + using (var img = await httpClient.GetStreamAsync(url).ConfigureAwait(false)) + { + var spam = await client.GetChannelAsync(Config.ThumbnailSpamId).ConfigureAwait(false); + //var message = await spam.SendFileAsync(img, (thumb.ContentId ?? thumb.ProductCode) + ".jpg").ConfigureAwait(false); + var message = await spam.SendFileAsync((thumb.ContentId ?? thumb.ProductCode) + ".jpg", img).ConfigureAwait(false); + thumb.EmbeddableUrl = message.Attachments.First().Url; + await db.SaveChangesAsync().ConfigureAwait(false); + return thumb.EmbeddableUrl; + } + } + catch (Exception e) + { + client.DebugLogger.LogMessage(LogLevel.Warning, "", e.ToString(), DateTime.Now); + } + } + } + return null; + } + } +} diff --git a/CompatBot/Database/ThumbnailDb.cs b/CompatBot/Database/ThumbnailDb.cs new file mode 100644 index 00000000..0be06371 --- /dev/null +++ b/CompatBot/Database/ThumbnailDb.cs @@ -0,0 +1,50 @@ +using System.ComponentModel.DataAnnotations; +using CompatApiClient; +using Microsoft.EntityFrameworkCore; + +namespace CompatBot.Database +{ + internal class ThumbnailDb: DbContext + { + public DbSet State { get; set; } + public DbSet Thumbnail { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder.UseSqlite("Data Source=thumbs.db"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + //configure indices + modelBuilder.Entity().HasIndex(s => s.Locale).IsUnique().HasName("state_locale"); + modelBuilder.Entity().HasIndex(s => s.Timestamp).HasName("state_timestamp"); + modelBuilder.Entity().HasIndex(m => m.ProductCode).IsUnique().HasName("thumbnail_product_code"); + modelBuilder.Entity().HasIndex(m => m.Timestamp).HasName("thumbnail_timestamp"); + + //configure default policy of Id being the primary key + modelBuilder.ConfigureDefaultPkConvention(); + + //configure name conversion for all configured entities from CamelCase to snake_case + modelBuilder.ConfigureMapping(NamingStyles.Underscore); + } + } + + internal class State + { + public int Id { get; set; } + public string Locale { get; set; } + public long Timestamp { get; set; } + } + + internal class Thumbnail + { + public int Id { get; set; } + [Required] + public string ProductCode { get; set; } + public string ContentId { get; set; } + public string Url { get; set; } + public string EmbeddableUrl { get; set; } + public long Timestamp { get; set; } + } +} diff --git a/CompatBot/EventHandlers/AntipiracyMonitor.cs b/CompatBot/EventHandlers/AntipiracyMonitor.cs index 1e3a3bf2..e0559c3f 100644 --- a/CompatBot/EventHandlers/AntipiracyMonitor.cs +++ b/CompatBot/EventHandlers/AntipiracyMonitor.cs @@ -1,6 +1,6 @@ using System; using System.Threading.Tasks; -using CompatApiClient; +using CompatApiClient.Utils; using CompatBot.Commands; using CompatBot.Database.Providers; using CompatBot.Utils; diff --git a/CompatBot/EventHandlers/LogInfoHandler.cs b/CompatBot/EventHandlers/LogInfoHandler.cs index 57c6358f..66de5dc2 100644 --- a/CompatBot/EventHandlers/LogInfoHandler.cs +++ b/CompatBot/EventHandlers/LogInfoHandler.cs @@ -2,7 +2,7 @@ using System.IO.Pipelines; using System.Linq; using System.Threading.Tasks; -using CompatApiClient; +using CompatApiClient.Utils; using CompatBot.Commands; using CompatBot.EventHandlers.LogParsing; using CompatBot.EventHandlers.LogParsing.POCOs; diff --git a/CompatBot/EventHandlers/LogParsing/LogParser.LogSections.cs b/CompatBot/EventHandlers/LogParsing/LogParser.LogSections.cs index 79672f93..70fb43e4 100644 --- a/CompatBot/EventHandlers/LogParsing/LogParser.LogSections.cs +++ b/CompatBot/EventHandlers/LogParsing/LogParser.LogSections.cs @@ -1,10 +1,10 @@ using System.Collections.Generic; using System.Collections.Specialized; -using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using CompatBot.Database.Providers; using CompatBot.EventHandlers.LogParsing.POCOs; +using CompatBot.Utils; namespace CompatBot.EventHandlers.LogParsing { @@ -20,7 +20,7 @@ namespace CompatBot.EventHandlers.LogParsing * If trigger is matched, then the associated reges will be run on THE WHOLE sliding window * If any data was captured, it will be stored in the current collection of items with the key of the explicit capture group of regex * - * Due to limitations, REGEX can't contain anything other than ASCII (triggers CAN however) + * Due to limitations, REGEX can't contain anything other than ASCII (including triggers) * */ private static readonly List LogSections = new List @@ -105,7 +105,7 @@ namespace CompatBot.EventHandlers.LogParsing ["GL RENDERER:"] = new Regex(@"GL RENDERER: (?.*?)\r?\n", DefaultOptions), ["GL VERSION:"] = new Regex(@"GL VERSION:(\d|\.|\s|\w|-)* (?(\d+\.)*\d+)\r?\n", DefaultOptions), ["texel buffer size reported:"] = new Regex(@"RSX: Supported texel buffer size reported: (?\d*?) bytes", DefaultOptions), - ["·F "] = new Regex(@"F \d+:\d+:\d+\.\d+ {.+?} (?.*?(\:\W*\r?\n\(.*?)*)\r?$", DefaultOptions), + ["F "] = new Regex(@"F \d+:\d+:\d+\.\d+ {.+?} (?.*?(\:\W*\r?\n\(.*?)*)\r?$", DefaultOptions), ["Failed to load RAP file:"] = new Regex(@"Failed to load RAP file: (?.*?)\r?$", DefaultOptions), ["Rap file not found:"] = new Regex(@"Rap file not found: (?.*?)\r?$", DefaultOptions), }, @@ -119,7 +119,7 @@ namespace CompatBot.EventHandlers.LogParsing if (await PiracyStringProvider.FindTriggerAsync(line).ConfigureAwait(false) is string match) { state.PiracyTrigger = match; - state.PiracyContext = Utf8.GetString(Encoding.ASCII.GetBytes(line)); + state.PiracyContext = line.ToUtf8(); state.Error = LogParseState.ErrorCode.PiracyDetected; } } diff --git a/CompatBot/EventHandlers/LogParsing/LogParser.StateMachineGenerator.cs b/CompatBot/EventHandlers/LogParsing/LogParser.StateMachineGenerator.cs index 0085ae59..ede114fe 100644 --- a/CompatBot/EventHandlers/LogParsing/LogParser.StateMachineGenerator.cs +++ b/CompatBot/EventHandlers/LogParsing/LogParser.StateMachineGenerator.cs @@ -2,10 +2,10 @@ using System.Collections.Generic; using System.Collections.ObjectModel; using System.Linq; -using System.Text; using System.Text.RegularExpressions; using System.Threading.Tasks; using CompatBot.EventHandlers.LogParsing.POCOs; +using CompatBot.Utils; using NReco.Text; namespace CompatBot.EventHandlers.LogParsing @@ -15,7 +15,6 @@ namespace CompatBot.EventHandlers.LogParsing internal partial class LogParser { private static readonly ReadOnlyCollection SectionParsers; - private static readonly Encoding Utf8 = new UTF8Encoding(false); static LogParser() { @@ -26,14 +25,14 @@ namespace CompatBot.EventHandlers.LogParsing { OnLineCheckAsync = sectionDescription.OnNewLineAsync ?? ((l, s) => Task.CompletedTask), OnSectionEnd = sectionDescription.OnSectionEnd, - EndTrigger = Encoding.ASCII.GetString(Utf8.GetBytes(sectionDescription.EndTrigger)), + EndTrigger = sectionDescription.EndTrigger.ToLatin8BitEncoding(), }; // the idea here is to construct Aho-Corasick parser that will look for any data marker and run the associated regex to extract the data into state if (sectionDescription.Extractors?.Count > 0) { var act = new AhoCorasickDoubleArrayTrie>(sectionDescription.Extractors.Select(extractorPair => new SectionAction( - Encoding.ASCII.GetString(Utf8.GetBytes(extractorPair.Key)), + extractorPair.Key.ToLatin8BitEncoding(), (buffer, state) => OnExtractorHit(buffer, extractorPair.Value, state) ) ), true); @@ -54,7 +53,7 @@ namespace CompatBot.EventHandlers.LogParsing #if DEBUG Console.WriteLine($"regex {group.Name} = {group.Value}"); #endif - state.WipCollection[group.Name] = Utf8.GetString(Encoding.ASCII.GetBytes(group.Value)); + state.WipCollection[group.Name] = group.Value.ToUtf8(); } } diff --git a/CompatBot/EventHandlers/LogsAsTextMonitor.cs b/CompatBot/EventHandlers/LogsAsTextMonitor.cs index 8431d6fc..e030fdd6 100644 --- a/CompatBot/EventHandlers/LogsAsTextMonitor.cs +++ b/CompatBot/EventHandlers/LogsAsTextMonitor.cs @@ -1,7 +1,7 @@ using System; +using System.Linq; using System.Text.RegularExpressions; using System.Threading.Tasks; -using CompatBot.Utils; using DSharpPlus.Entities; using DSharpPlus.EventArgs; @@ -22,7 +22,7 @@ namespace CompatBot.EventHandlers if (!"help".Equals(args.Channel.Name, StringComparison.InvariantCultureIgnoreCase)) return; - if ((args.Message.Author as DiscordMember)?.Roles.IsWhitelisted() ?? false) + if ((args.Message.Author as DiscordMember)?.Roles.Any() ?? false) return; if (LogLine.IsMatch(args.Message.Content)) diff --git a/CompatBot/EventHandlers/ProductCodeLookup.cs b/CompatBot/EventHandlers/ProductCodeLookup.cs index 16fb90f1..d0f24b7b 100644 --- a/CompatBot/EventHandlers/ProductCodeLookup.cs +++ b/CompatBot/EventHandlers/ProductCodeLookup.cs @@ -6,6 +6,7 @@ using System.Text.RegularExpressions; using System.Threading.Tasks; using CompatApiClient; using CompatApiClient.POCOs; +using CompatBot.Database.Providers; using CompatBot.Utils; using CompatBot.Utils.ResultFormatters; using DSharpPlus; @@ -18,7 +19,7 @@ namespace CompatBot.EventHandlers internal static class ProductCodeLookup { // see http://www.psdevwiki.com/ps3/Productcode - private static readonly Regex ProductCode = new Regex(@"(?(?:[BPSUVX][CL]|P[ETU]|NP)[AEHJKPUIX][ABSM])[ \-]?(?\d{5})", RegexOptions.Compiled | RegexOptions.IgnoreCase); + public static readonly Regex ProductCode = new Regex(@"(?(?:[BPSUVX][CL]|P[ETU]|NP)[AEHJKPUIX][ABSM])[ \-]?(?\d{5})", RegexOptions.Compiled | RegexOptions.IgnoreCase); private static readonly Client compatClient = new Client(); private static readonly AhoCorasickDoubleArrayTrie ChillCheck = new AhoCorasickDoubleArrayTrie(new[] {"shut up", "hush", "chill"}.ToDictionary(s => s, s => s), true); @@ -111,7 +112,10 @@ namespace CompatBot.EventHandlers return TitleInfo.Unknown.AsEmbed(code, footer); if (result.Results.TryGetValue(code, out var info)) - return info.AsEmbed(code, footer); + { + var thumbnailUrl = await client.GetThumbnailUrlAsync(code).ConfigureAwait(false); + return info.AsEmbed(code, footer, thumbnailUrl); + } return TitleInfo.Unknown.AsEmbed(code, footer); } diff --git a/CompatBot/Program.cs b/CompatBot/Program.cs index cb8bf73f..79fbd6f8 100644 --- a/CompatBot/Program.cs +++ b/CompatBot/Program.cs @@ -4,9 +4,10 @@ using CompatBot.Commands; using CompatBot.Commands.Converters; using CompatBot.Database; using CompatBot.EventHandlers; +using CompatBot.ThumbScrapper; using DSharpPlus; using DSharpPlus.CommandsNext; -using DSharpPlus.Entities; +using Microsoft.Extensions.DependencyInjection; namespace CompatBot { @@ -20,9 +21,15 @@ namespace CompatBot return; } - if (!await DbImporter.UpgradeAsync(BotDb.Instance, Config.Cts.Token)) - return; + using (var db = new BotDb()) + if (!await DbImporter.UpgradeAsync(db, Config.Cts.Token)) + return; + using (var db = new ThumbnailDb()) + if (!await DbImporter.UpgradeAsync(db, Config.Cts.Token)) + return; + + var psnScrappingTask = new PsnScraper().Run(Config.Cts.Token); var config = new DiscordConfiguration { @@ -34,7 +41,7 @@ namespace CompatBot using (var client = new DiscordClient(config)) { - var commands = client.UseCommandsNext(new CommandsNextConfiguration {StringPrefixes = new[] {Config.CommandPrefix}}); + var commands = client.UseCommandsNext(new CommandsNextConfiguration {StringPrefixes = new[] {Config.CommandPrefix}, Services = new ServiceCollection().BuildServiceProvider()}); commands.RegisterConverter(new CustomDiscordChannelConverter()); commands.RegisterCommands(); commands.RegisterCommands(); @@ -91,6 +98,7 @@ namespace CompatBot await Task.Delay(TimeSpan.FromMinutes(1), Config.Cts.Token).ContinueWith(dt => {/* in case it was cancelled */}).ConfigureAwait(false); } } + await psnScrappingTask.ConfigureAwait(false); Console.WriteLine("Exiting"); } } diff --git a/CompatBot/ThumbScrapper/PsnScraper.cs b/CompatBot/ThumbScrapper/PsnScraper.cs new file mode 100644 index 00000000..918f26a0 --- /dev/null +++ b/CompatBot/ThumbScrapper/PsnScraper.cs @@ -0,0 +1,329 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using System.Threading; +using System.Threading.Tasks; +using CompatBot.Database; +using CompatBot.Database.Providers; +using CompatBot.EventHandlers; +using PsnClient.POCOs; +using PsnClient.Utils; + +namespace CompatBot.ThumbScrapper +{ + internal class PsnScraper + { + private static readonly PsnClient.Client Client = new PsnClient.Client(); + private static readonly Regex ContentIdMatcher = new Regex(@"(?(?\w\w)(?\d{4}))-(?(?\w{4})(?\d{5}))_(?\d\d)-(?