scrape only full game lists in PSN stores, also cache title names

new isssue detections for log parser
consistent reaction with emoji only / text when can't
ability to disable commands at runtime (fixes #56)
command to check for game updates
various other bugfixes
This commit is contained in:
13xforever 2018-08-05 19:36:16 +05:00 committed by Roberto Anić Banić
parent fbad33ea13
commit 998c27c966
43 changed files with 1268 additions and 342 deletions

View File

@ -7,20 +7,18 @@ using CompatBot.Database.Providers;
using CompatBot.Utils;
using DSharpPlus.CommandsNext;
using DSharpPlus.CommandsNext.Attributes;
using DSharpPlus.Entities;
using Microsoft.EntityFrameworkCore;
namespace CompatBot.Commands
{
[Group("piracy"), RequiresBotModRole, RequiresDm]
[Group("piracy"), RequiresBotModRole, RequiresDm, TriggersTyping]
[Description("Used to manage piracy filters **in DM**")]
internal sealed class Antipiracy: BaseCommandModule
internal sealed class Antipiracy: BaseCommandModuleCustom
{
[Command("list"), Aliases("show")]
[Description("Lists all filters")]
public async Task List(CommandContext ctx)
{
var typingTask = ctx.TriggerTypingAsync();
var result = new StringBuilder("```")
.AppendLine("ID | Trigger")
.AppendLine("-----------------------------");
@ -28,23 +26,17 @@ namespace CompatBot.Commands
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;
}
[Command("add")]
[Description("Adds a new piracy filter trigger")]
public async Task Add(CommandContext ctx, [RemainingText, Description("A plain string to match")] string trigger)
{
var typingTask = ctx.TriggerTypingAsync();
var wasSuccessful = await PiracyStringProvider.AddAsync(trigger).ConfigureAwait(false);
(DiscordEmoji reaction, string msg) result = wasSuccessful
? (Config.Reactions.Success, "New trigger successfully saved!")
: (Config.Reactions.Failure, "Trigger already defined.");
await Task.WhenAll(
ctx.RespondAsync(result.msg),
ctx.Message.CreateReactionAsync(result.reaction),
typingTask
).ConfigureAwait(false);
if (wasSuccessful)
await ctx.ReactWithAsync(Config.Reactions.Success, "New trigger successfully saved!").ConfigureAwait(false);
else
await ctx.ReactWithAsync(Config.Reactions.Failure, "Trigger already defined.").ConfigureAwait(false);
if (wasSuccessful)
await List(ctx).ConfigureAwait(false);
}
@ -53,19 +45,14 @@ namespace CompatBot.Commands
[Description("Removes a piracy filter trigger")]
public async Task Remove(CommandContext ctx, [Description("Filter ids to remove separated with spaces")] params int[] ids)
{
var typingTask = ctx.TriggerTypingAsync();
(DiscordEmoji reaction, string msg) result = (Config.Reactions.Success, $"Trigger{(ids.Length == 1 ? "" : "s")} successfully removed!");
var failedIds = new List<int>();
foreach (var id in ids)
if (!await PiracyStringProvider.RemoveAsync(id).ConfigureAwait(false))
failedIds.Add(id);
if (failedIds.Count > 0)
result = (Config.Reactions.Failure, "Some ids couldn't be removed: " + string.Join(", ", failedIds));
await Task.WhenAll(
ctx.RespondAsync(result.msg),
ctx.Message.CreateReactionAsync(result.reaction),
typingTask
).ConfigureAwait(false);
await ctx.RespondAsync("Some ids couldn't be removed: " + string.Join(", ", failedIds)).ConfigureAwait(false);
else
await ctx.ReactWithAsync(Config.Reactions.Success, $"Trigger{(ids.Length == 1 ? "" : "s")} successfully removed!").ConfigureAwait(false);
await List(ctx).ConfigureAwait(false);
}
}

View File

@ -1,4 +1,7 @@
using System.Threading.Tasks;
using System;
using System.Threading.Tasks;
using CompatBot.Utils;
using DSharpPlus;
using DSharpPlus.CommandsNext;
using DSharpPlus.CommandsNext.Attributes;
using DSharpPlus.Entities;
@ -21,16 +24,18 @@ namespace CompatBot.Commands.Attributes
public override async Task<bool> ExecuteCheckAsync(CommandContext ctx, bool help)
{
var result = await IsAllowed(ctx, help);
//await ctx.RespondAsync($"Check for {GetType().Name} resulted in {result}").ConfigureAwait(false);
#if DEBUG
ctx.Client.DebugLogger.LogMessage(LogLevel.Debug, "", $"Check for {GetType().Name} resulted in {result}", DateTime.Now);
#endif
if (result)
{
if (ReactOnSuccess != null && !help)
await ctx.Message.CreateReactionAsync(ReactOnSuccess).ConfigureAwait(false);
await ctx.ReactWithAsync(ReactOnSuccess).ConfigureAwait(false);
}
else
{
if (ReactOnFailure != null && !help)
await ctx.Message.CreateReactionAsync(ReactOnFailure).ConfigureAwait(false);
await ctx.ReactWithAsync(ReactOnFailure, $"{ReactOnFailure} {ctx.Message.Author.Mention} you do not have required permissions, this incident will be reported").ConfigureAwait(false);
}
return result;
}

View File

@ -0,0 +1,23 @@
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 TriggersTyping: CheckBaseAttribute
{
public bool InDmOnly { get; set; }
public override async Task<bool> ExecuteCheckAsync(CommandContext ctx, bool help)
{
if (help)
return true;
if (!InDmOnly || ctx.Channel.IsPrivate)
await ctx.TriggerTypingAsync().ConfigureAwait(false);
return true;
}
}
}

View File

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using CompatApiClient;
using CompatApiClient.POCOs;
using CompatApiClient.Utils;
using CompatBot.Commands.Attributes;
using CompatBot.Utils;
using CompatBot.Utils.ResultFormatters;
using DSharpPlus;
@ -15,7 +16,7 @@ using DSharpPlus.Entities;
namespace CompatBot.Commands
{
internal sealed class CompatList : BaseCommandModule
internal sealed class CompatList : BaseCommandModuleCustom
{
private static readonly Client client = new Client();
@ -84,15 +85,13 @@ Example usage:
await DoRequestAndRespond(ctx, requestBuilder).ConfigureAwait(false);
}
[Command("filters")]
[Command("filters"), TriggersTyping(InDmOnly = true)]
[Description("Provides information about available filters for the !top command")]
public async Task Filters(CommandContext ctx)
{
var getDmTask = ctx.CreateDmAsync();
if (ctx.Channel.IsPrivate)
await ctx.TriggerTypingAsync().ConfigureAwait(false);
var embed = new DiscordEmbedBuilder {Description = "List of recognized tokens in each filter category", Color = Config.Colors.Help}
.AddField("Regions", DicToDesc(ApiConfig.Regions))
//.AddField("Regions", DicToDesc(ApiConfig.Regions))
.AddField("Statuses", DicToDesc(ApiConfig.Statuses))
.AddField("Release types", DicToDesc(ApiConfig.ReleaseTypes))
.Build();
@ -100,12 +99,11 @@ Example usage:
await dm.SendMessageAsync(embed: embed).ConfigureAwait(false);
}
[Command("latest"), Aliases("download")]
[Command("latest"), Aliases("download"), TriggersTyping]
[Description("Provides links to the latest RPCS3 build")]
[Cooldown(1, 30, CooldownBucketType.Channel)]
public async Task Latest(CommandContext ctx)
{
await ctx.TriggerTypingAsync().ConfigureAwait(false);
var info = await client.GetUpdateAsync(Config.Cts.Token).ConfigureAwait(false);
var embed = await info.AsEmbedAsync().ConfigureAwait(false);
await ctx.RespondAsync(embed: embed.Build()).ConfigureAwait(false);

View File

@ -0,0 +1,24 @@
using System.Threading.Tasks;
using CompatBot.Commands.Attributes;
using CompatBot.Database.Providers;
using DSharpPlus.CommandsNext;
using DSharpPlus.CommandsNext.Attributes;
using DSharpPlus.Entities;
namespace CompatBot.Commands
{
internal class BaseCommandModuleCustom : BaseCommandModule
{
public override async Task BeforeExecutionAsync(CommandContext ctx)
{
var disabledCmds = DisabledCommandsProvider.Get();
if (disabledCmds.Contains(ctx.Command.QualifiedName) && !disabledCmds.Contains("*"))
{
await ctx.RespondAsync(embed: new DiscordEmbedBuilder {Color = Config.Colors.Maintenance, Description = "Command is currently disabled"}).ConfigureAwait(false);
throw new DSharpPlus.CommandsNext.Exceptions.ChecksFailedException(ctx.Command, ctx, new CheckBaseAttribute[] {new RequiresDm()});
}
await base.BeforeExecutionAsync(ctx).ConfigureAwait(false);
}
}
}

View File

@ -1,6 +1,7 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using CompatApiClient.Utils;
using CompatBot.Commands.Attributes;
using CompatBot.Database;
using CompatBot.Utils;
@ -16,7 +17,7 @@ namespace CompatBot.Commands
[Group("explain"), Aliases("botsplain", "define")]
[Cooldown(1, 3, CooldownBucketType.Channel)]
[Description("Used to manage and show explanations")]
internal sealed class Explain: BaseCommandModule
internal sealed class Explain: BaseCommandModuleCustom
{
[GroupCommand]
public async Task ShowExplanation(CommandContext ctx, [RemainingText, Description("Term to explain")] string term)
@ -65,7 +66,7 @@ namespace CompatBot.Commands
}
}
await ctx.Message.CreateReactionAsync(Config.Reactions.Failure).ConfigureAwait(false);
await ctx.RespondAsync($"Unknown term `{term.Sanitize()}`. Use `!explain list` to look at defined terms").ConfigureAwait(false);
}
[Command("add"), RequiresBotModRole]
@ -76,26 +77,18 @@ namespace CompatBot.Commands
{
term = term.StripQuotes();
if (string.IsNullOrEmpty(explanation))
{
await Task.WhenAll(
ctx.Message.CreateReactionAsync(Config.Reactions.Failure),
ctx.RespondAsync("An explanation for the term must be provided")
).ConfigureAwait(false);
}
await ctx.ReactWithAsync(Config.Reactions.Failure, "An explanation for the term must be provided").ConfigureAwait(false);
else
{
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);
await ctx.ReactWithAsync(Config.Reactions.Failure, $"`{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);
await ctx.ReactWithAsync(Config.Reactions.Success, $"`{term}` was successfully added").ConfigureAwait(false);
}
}
}
@ -112,17 +105,12 @@ namespace CompatBot.Commands
{
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);
}
await ctx.ReactWithAsync(Config.Reactions.Failure, $"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);
await ctx.ReactWithAsync(Config.Reactions.Success, "Term was updated").ConfigureAwait(false);
}
}
}
@ -138,17 +126,12 @@ namespace CompatBot.Commands
{
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);
}
await ctx.ReactWithAsync(Config.Reactions.Failure, $"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);
await ctx.ReactWithAsync(Config.Reactions.Success, $"Renamed `{oldTerm}` to `{newTerm}`").ConfigureAwait(false);
}
}
}
@ -163,14 +146,13 @@ namespace CompatBot.Commands
if ("to".Equals(to, StringComparison.InvariantCultureIgnoreCase))
await Rename(ctx, oldTerm, newTerm).ConfigureAwait(false);
else
await ctx.Message.CreateReactionAsync(Config.Reactions.Failure).ConfigureAwait(false);
await ctx.ReactWithAsync(Config.Reactions.Failure).ConfigureAwait(false);
}
[Command("list"), LimitedToSpamChannel]
[Command("list"), LimitedToSpamChannel, TriggersTyping]
[Description("List all known terms that could be used for !explain command")]
public async Task List(CommandContext ctx)
{
await ctx.TriggerTypingAsync().ConfigureAwait(false);
using (var db = new BotDb())
{
var keywords = await db.Explanation.Select(e => e.Keyword).OrderBy(t => t).ToListAsync().ConfigureAwait(false);
@ -199,17 +181,12 @@ namespace CompatBot.Commands
{
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);
}
await ctx.ReactWithAsync(Config.Reactions.Failure, $"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);
await ctx.ReactWithAsync(Config.Reactions.Success, $"Removed `{term}`").ConfigureAwait(false);
}
}
}

View File

@ -3,16 +3,18 @@ using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using CompatBot.Commands.Attributes;
using CompatBot.Utils;
using DSharpPlus;
using DSharpPlus.CommandsNext;
using DSharpPlus.CommandsNext.Attributes;
using DSharpPlus.CommandsNext.Converters;
using DSharpPlus.Entities;
using org.mariuszgromada.math.mxparser;
namespace CompatBot.Commands
{
internal sealed class Misc: BaseCommandModule
internal sealed class Misc: BaseCommandModuleCustom
{
private readonly Random rng = new Random();
@ -41,27 +43,40 @@ namespace CompatBot.Commands
"So-so"
};
private static readonly HashSet<string> Me = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase)
{
"I", "me", "myself", "moi", "self"
};
[Command("credits"), Aliases("about")]
[Description("Author Credit")]
public async Task Credits(CommandContext ctx)
{
var embed = new DiscordEmbedBuilder
DiscordEmoji hcorion;
try
{
Title = "RPCS3 Compatibility Bot",
Url = "https://github.com/RPCS3/discord-bot",
Description = "Made by:\n" +
"  Roberto Anić Banić aka nicba1010\n" +
"  13xforever".FixSpaces(),
Color = DiscordColor.Purple,
};
hcorion = DiscordEmoji.FromName(ctx.Client, ":hcorion:");
}
catch
{
hcorion = DiscordEmoji.FromUnicode("🍁");
}
var embed = new DiscordEmbedBuilder
{
Title = "RPCS3 Compatibility Bot",
Url = "https://github.com/RPCS3/discord-bot",
Color = DiscordColor.Purple,
}.AddField("Made by", "🇭🇷 Roberto Anić Banić aka nicba1010\n" +
"💮 13xforever".FixSpaces())
.AddField("People who ~~broke~~ helped test the bot", "🐱 Juhn\n" +
$"{hcorion} hcorion");
await ctx.RespondAsync(embed: embed.Build());
}
[Command("math")]
[Command("math"), TriggersTyping]
[Description("Math, here you go Juhn")]
public async Task Math(CommandContext ctx, [RemainingText, Description("Math expression")] string expression)
{
var typing = ctx.TriggerTypingAsync();
var result = @"Something went wrong ¯\\_(ツ)\_/¯" + "\nMath is hard, yo";
try
{
@ -73,35 +88,28 @@ namespace CompatBot.Commands
ctx.Client.DebugLogger.LogMessage(LogLevel.Warning, "", "Math failed: " + e.Message, DateTime.Now);
}
await ctx.RespondAsync(result).ConfigureAwait(false);
await typing.ConfigureAwait(false);
}
[Command("roll")]
[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)
[Description("Generates a random number between 1 and maxValue. Can also roll dices like `2d6`. Default is 1d6")]
public async Task Roll(CommandContext ctx, [Description("Some positive natural number")] int maxValue = 6, [Description("Optional text"), RemainingText] string comment = null)
{
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);
await ctx.ReactWithAsync(DiscordEmoji.FromUnicode("💩"), $"How is {maxValue} a positive natural number?").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)
public async Task Roll(CommandContext ctx, [Description("Dices to roll (i.e. 2d6 for two 6-sided dices)")] string dices, [Description("Optional text"), RemainingText] string comment = null)
{
var result = "";
if (dices is string dice && Regex.IsMatch(dice, @"\d+d\d+"))
{
var typingTask = ctx.TriggerTypingAsync();
await ctx.TriggerTypingAsync().ConfigureAwait(false);
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 &&
@ -117,10 +125,9 @@ namespace CompatBot.Commands
else
result = rolls.Sum().ToString();
}
await typingTask.ConfigureAwait(false);
}
if (string.IsNullOrEmpty(result))
await ctx.Message.CreateReactionAsync(DiscordEmoji.FromUnicode("💩")).ConfigureAwait(false);
await ctx.ReactWithAsync(DiscordEmoji.FromUnicode("💩"), "Invalid dice description passed").ConfigureAwait(false);
else
await ctx.RespondAsync(result).ConfigureAwait(false);
}
@ -130,7 +137,8 @@ namespace CompatBot.Commands
public async Task EightBall(CommandContext ctx, [RemainingText, Description("A yes/no question")] string question)
{
string answer;
lock (rng) answer = EightBallAnswers[rng.Next(EightBallAnswers.Count)];
lock (rng)
answer = EightBallAnswers[rng.Next(EightBallAnswers.Count)];
await ctx.RespondAsync(answer).ConfigureAwait(false);
}
@ -138,8 +146,30 @@ namespace CompatBot.Commands
[Description("Gives an ~~unrelated~~ expert judgement on the matter at hand")]
public async Task Rate(CommandContext ctx, [RemainingText, Description("Something to rate")] string whatever)
{
string answer;
lock (rng) answer = RateAnswers[rng.Next(RateAnswers.Count)];
var choices = RateAnswers;
var whateverParts = whatever.Split(' ', StringSplitOptions.RemoveEmptyEntries) ?? new string[0];
if (whatever is string neko && (neko.Contains("neko", StringComparison.InvariantCultureIgnoreCase) || neko.Contains("272032356922032139")))
{
choices = RateAnswers.Concat(Enumerable.Repeat("Ugh", 100)).ToList();
if (await new DiscordUserConverter().ConvertAsync("272032356922032139", ctx).ConfigureAwait(false) is Optional<DiscordUser> user && user.HasValue)
whatever = user.Value.Id.ToString();
}
else if (whatever is string sonic && sonic.Contains("sonic"))
{
choices = RateAnswers.Concat(Enumerable.Repeat("💩 out of 🦔", 100)).Concat(new []{"Sonic out of 🦔", "Sonic out of 10"}).ToList();
whatever = "Sonic";
}
else if (whateverParts.Length == 1)
{
if (Me.Contains(whateverParts[0]))
whatever = ctx.Message.Author.Id.ToString();
else if (await new DiscordUserConverter().ConvertAsync(whateverParts[0], ctx).ConfigureAwait(false) is Optional<DiscordUser> user && user.HasValue)
whatever = user.Value.Id.ToString();
}
whatever = DateTime.UtcNow.ToString("yyyyMMdd") + whatever?.Trim();
var seed = whatever.GetHashCode(StringComparison.CurrentCultureIgnoreCase);
var seededRng = new Random(seed);
var answer = choices[seededRng.Next(choices.Count)];
await ctx.RespondAsync(answer).ConfigureAwait(false);
}
}

View File

@ -0,0 +1,44 @@
using System.Linq;
using System.Threading.Tasks;
using CompatApiClient.Utils;
using CompatBot.EventHandlers;
using CompatBot.Utils;
using CompatBot.Utils.ResultFormatters;
using DSharpPlus.CommandsNext;
using DSharpPlus.CommandsNext.Attributes;
using PsnClient;
namespace CompatBot.Commands
{
internal sealed partial class Psn
{
private static readonly Client Client = new Client();
[Group("check")]
[Description("Commands to check for various stuff on PSN")]
public sealed class Check : BaseCommandModuleCustom
{
[Command("updates"), Aliases("update")]
[Description("Checks if specified product has any updates")]
public async Task Updates(CommandContext ctx, [RemainingText, Description("Product ID such as BLUS12345")] string productId)
{
productId = ProductCodeLookup.GetProductIds(productId).FirstOrDefault();
if (string.IsNullOrEmpty(productId))
{
await ctx.ReactWithAsync(Config.Reactions.Failure, $"`{productId.Sanitize()}` is not a valid product ID").ConfigureAwait(false);
return;
}
var updateInfo = await Client.GetTitleUpdatesAsync(productId, Config.Cts.Token).ConfigureAwait(false);
if (updateInfo?.Tag?.Packages?.Length > 0)
{
var embed = await updateInfo.AsEmbedAsync(ctx.Client).ConfigureAwait(false);
await ctx.RespondAsync(embed: embed).ConfigureAwait(false);
return;
}
await ctx.RespondAsync("No updates were found").ConfigureAwait(false);
}
}
}
}

63
CompatBot/Commands/Psn.cs Normal file
View File

@ -0,0 +1,63 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using CompatBot.Commands.Attributes;
using CompatBot.Database;
using CompatBot.Utils;
using DSharpPlus;
using DSharpPlus.CommandsNext;
using DSharpPlus.CommandsNext.Attributes;
namespace CompatBot.Commands
{
[Group("psn")]
[Description("Commands related to PSN metadata")]
internal sealed partial class Psn: BaseCommandModuleCustom
{
[Command("fix"), RequiresBotModRole]
[Description("Reset thumbnail cache for specified product")]
public async Task Fix(CommandContext ctx, [Description("Product ID to reset")] string productId)
{
var linksToRemove = new List<(string contentId, string link)>();
using (var db = new ThumbnailDb())
{
var items = db.Thumbnail.Where(i => i.ProductCode == productId && !string.IsNullOrEmpty(i.EmbeddableUrl));
foreach (var thumb in items)
{
linksToRemove.Add((thumb.ContentId, thumb.EmbeddableUrl));
thumb.EmbeddableUrl = null;
}
await db.SaveChangesAsync(Config.Cts.Token).ConfigureAwait(false);
}
await TryDeleteThumbnailCache(ctx, linksToRemove).ConfigureAwait(false);
await ctx.RespondAsync($"Removed {linksToRemove.Count} cached links").ConfigureAwait(false);
}
private static async Task TryDeleteThumbnailCache(CommandContext ctx, List<(string contentId, string link)> linksToRemove)
{
var contentIds = linksToRemove.ToDictionary(l => l.contentId, l => l.link);
try
{
var channel = await ctx.Client.GetChannelAsync(Config.ThumbnailSpamId).ConfigureAwait(false);
var messages = await channel.GetMessagesAsync(1000).ConfigureAwait(false);
foreach (var msg in messages)
if (contentIds.TryGetValue(msg.Content, out var lnk) && msg.Attachments.Any(a => a.Url == lnk))
{
try
{
await msg.DeleteAsync().ConfigureAwait(false);
}
catch (Exception e)
{
ctx.Client.DebugLogger.LogMessage(LogLevel.Warning, "", "Couldn't delete cached thumbnail image: " + e, DateTime.Now);
}
}
}
catch (Exception e)
{
ctx.Client.DebugLogger.LogMessage(LogLevel.Warning, "", e.ToString(), DateTime.Now);
}
}
}
}

View File

@ -0,0 +1,198 @@
using System;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using CompatBot.Commands.Attributes;
using CompatBot.Database.Providers;
using CompatBot.Utils;
using DSharpPlus;
using DSharpPlus.CommandsNext;
using DSharpPlus.CommandsNext.Attributes;
namespace CompatBot.Commands
{
internal partial class Sudo
{
[Group]
[Description("Used to enabe and disable bot commands at runtime")]
public sealed class Commands : BaseCommandModule
{
[Command("list"), Aliases("show"), TriggersTyping]
[Description("Lists the disabled commands")]
public async Task List(CommandContext ctx)
{
var list = DisabledCommandsProvider.Get();
if (list.Count > 0)
{
var result = new StringBuilder("Currently disabled commands:").AppendLine().AppendLine("```");
foreach (var cmd in list)
result.AppendLine(cmd);
await ctx.SendAutosplitMessageAsync(result.Append("```")).ConfigureAwait(false);
}
else
await ctx.RespondAsync("All commands are enabled").ConfigureAwait(false);
}
[Command("disable"), Aliases("add")]
[Description("Disables the specified command")]
public async Task Disable(CommandContext ctx, [RemainingText, Description("Fully qualified command to disable, e.g. `explain add` or `sudo mod *`")] string command)
{
var isPrefix = command.EndsWith('*');
if (isPrefix)
command = command.TrimEnd('*', ' ');
if (string.IsNullOrEmpty(command) && !isPrefix)
{
await ctx.ReactWithAsync(Config.Reactions.Failure, "You need to specify the command").ConfigureAwait(false);
return;
}
if (command.StartsWith(ctx.Command.Parent.QualifiedName))
{
await ctx.ReactWithAsync(Config.Reactions.Failure, "Cannot disable command management commands").ConfigureAwait(false);
return;
}
var cmd = GetCommand(ctx, command);
if (isPrefix)
{
if (cmd == null && !string.IsNullOrEmpty(command))
{
await ctx.ReactWithAsync(Config.Reactions.Failure, $"Unknown group `{command}`").ConfigureAwait(false);
return;
}
try
{
if (cmd == null)
foreach (var c in ctx.CommandsNext.RegisteredCommands.Values)
DisableSubcommands(ctx, c);
else
DisableSubcommands(ctx, cmd);
if (ctx.Command.Parent.QualifiedName.StartsWith(command))
await ctx.RespondAsync("Some subcommands cannot be disabled").ConfigureAwait(false);
else
await ctx.ReactWithAsync(Config.Reactions.Success, $"Disabled `{command}` and all subcommands").ConfigureAwait(false);
await List(ctx).ConfigureAwait(false);
}
catch (Exception e)
{
ctx.Client.DebugLogger.LogMessage(LogLevel.Error, "", e.ToString(), DateTime.Now);
await ctx.RespondAsync("Error while disabling the group").ConfigureAwait(false);
}
}
else
{
if (cmd == null)
{
await ctx.ReactWithAsync(Config.Reactions.Failure, $"Unknown command `{command}`").ConfigureAwait(false);
return;
}
command = cmd.QualifiedName;
DisabledCommandsProvider.Disable(command);
await ctx.ReactWithAsync(Config.Reactions.Success, $"Disabled `{command}`").ConfigureAwait(false);
}
}
[Command("enable"), Aliases("reenable", "remove", "delete", "del", "clear")]
[Description("Enables the specified command")]
public async Task Enable(CommandContext ctx, [RemainingText, Description("Fully qualified command to enable, e.g. `explain add` or `sudo mod *`")] string command)
{
if (command == "*")
{
DisabledCommandsProvider.Clear();
await ctx.ReactWithAsync(Config.Reactions.Success, "Enabled all the commands").ConfigureAwait(false);
return;
}
var isPrefix = command.EndsWith('*');
if (isPrefix)
command = command.TrimEnd('*', ' ');
if (string.IsNullOrEmpty(command))
{
await ctx.ReactWithAsync(Config.Reactions.Failure, "You need to specify the command").ConfigureAwait(false);
return;
}
var cmd = GetCommand(ctx, command);
if (isPrefix)
{
if (cmd == null)
{
await ctx.ReactWithAsync(Config.Reactions.Failure, $"Unknown group `{command}`").ConfigureAwait(false);
return;
}
try
{
EnableSubcommands(ctx, cmd);
await ctx.ReactWithAsync(Config.Reactions.Success, $"Enabled `{command}` and all subcommands").ConfigureAwait(false);
await List(ctx).ConfigureAwait(false);
}
catch (Exception e)
{
ctx.Client.DebugLogger.LogMessage(LogLevel.Error, "", e.ToString(), DateTime.Now);
await ctx.RespondAsync("Error while enabling the group").ConfigureAwait(false);
}
}
else
{
if (cmd == null)
{
await ctx.ReactWithAsync(Config.Reactions.Failure, $"Unknown command `{command}`").ConfigureAwait(false);
return;
}
command = cmd.QualifiedName;
DisabledCommandsProvider.Enable(command);
await ctx.ReactWithAsync(Config.Reactions.Success, $"Enabled `{command}`").ConfigureAwait(false);
}
}
private static Command GetCommand(CommandContext ctx, string qualifiedName)
{
if (string.IsNullOrEmpty(qualifiedName))
return null;
var groups = ctx.CommandsNext.RegisteredCommands.Values;
Command result = null;
foreach (var cmdPart in qualifiedName.Split(' ', StringSplitOptions.RemoveEmptyEntries))
{
if (groups.FirstOrDefault(g => g.Name == cmdPart || g.Aliases.Any(a => a == cmdPart)) is Command c)
{
result = c;
if (c is CommandGroup subGroup)
groups = subGroup.Children;
}
else
return null;
}
return result;
}
private static void DisableSubcommands(CommandContext ctx, Command cmd)
{
if (cmd.QualifiedName.StartsWith(ctx.Command.Parent.QualifiedName))
return;
DisabledCommandsProvider.Disable(cmd.QualifiedName);
if (cmd is CommandGroup group)
foreach (var subCmd in group.Children)
DisableSubcommands(ctx, subCmd);
}
private static void EnableSubcommands(CommandContext ctx, Command cmd)
{
if (cmd.QualifiedName.StartsWith(ctx.Command.Parent.QualifiedName))
return;
DisabledCommandsProvider.Enable(cmd.QualifiedName);
if (cmd is CommandGroup group)
foreach (var subCmd in group.Children)
EnableSubcommands(ctx, subCmd);
}
}
}
}

View File

@ -3,6 +3,7 @@ using System.Diagnostics;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using CompatBot.Commands.Attributes;
using CompatBot.Utils;
using DSharpPlus.CommandsNext;
using DSharpPlus.CommandsNext.Attributes;
@ -15,13 +16,12 @@ namespace CompatBot.Commands
[Group("bot")]
[Description("Commands to manage the bot instance")]
public sealed class Bot: BaseCommandModule
public sealed partial class Bot: BaseCommandModuleCustom
{
[Command("version")]
[Command("version"), TriggersTyping]
[Description("Returns currently checked out bot commit")]
public async Task Version(CommandContext ctx)
{
var typingTask = ctx.TriggerTypingAsync();
using (var git = new Process
{
StartInfo = new ProcessStartInfo("git", "log -1 --oneline")
@ -39,14 +39,12 @@ namespace CompatBot.Commands
if (!string.IsNullOrEmpty(stdout))
await ctx.RespondAsync("```" + stdout + "```").ConfigureAwait(false);
}
await typingTask.ConfigureAwait(false);
}
[Command("restart"), Aliases("update")]
[Command("restart"), Aliases("update"), TriggersTyping]
[Description("Restarts bot and pulls newest commit")]
public async Task Restart(CommandContext ctx)
{
var typingTask = ctx.TriggerTypingAsync();
if (lockObj.Wait(0))
{
try
@ -94,7 +92,6 @@ namespace CompatBot.Commands
}
else
await ctx.RespondAsync("Update is already in progress").ConfigureAwait(false);
await typingTask.ConfigureAwait(false);
}
[Command("stop"), Aliases("exit", "shutdown", "terminate")]

View File

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using CompatBot.Commands.Attributes;
using CompatBot.Commands.Converters;
using CompatBot.Database;
using DSharpPlus;
@ -18,13 +19,12 @@ namespace CompatBot.Commands
[Group("fix"), Hidden]
[Description("Commands to fix various stuff")]
public sealed class Fix: BaseCommandModule
public sealed class Fix: BaseCommandModuleCustom
{
[Command("timestamps")]
[Command("timestamps"), TriggersTyping]
[Description("Fixes `timestamp` column in the `warning` table")]
public async Task Timestamps(CommandContext ctx)
{
await ctx.TriggerTypingAsync().ConfigureAwait(false);
try
{
var @fixed = 0;
@ -52,11 +52,10 @@ namespace CompatBot.Commands
}
}
[Command("channels")]
[Command("channels"), TriggersTyping]
[Description("Fixes channel mentions in `warning` table")]
public async Task Channels(CommandContext ctx)
{
await ctx.TriggerTypingAsync().ConfigureAwait(false);
try
{
var @fixed = 0;

View File

@ -1,5 +1,6 @@
using System.Text;
using System.Threading.Tasks;
using CompatBot.Commands.Attributes;
using CompatBot.Database.Providers;
using CompatBot.Utils;
using DSharpPlus.CommandsNext;
@ -12,106 +13,83 @@ namespace CompatBot.Commands
{
[Group("mod")]
[Description("Used to manage bot moderators")]
public sealed class Mod : BaseCommandModule
public sealed class Mod : BaseCommandModuleCustom
{
[Command("add")]
[Command("add"), TriggersTyping]
[Description("Adds a new moderator")]
public async Task Add(CommandContext ctx, [Description("Discord user to add to the bot mod list")] DiscordMember user)
{
var typingTask = ctx.TriggerTypingAsync();
(DiscordEmoji reaction, string msg) result =
await ModProvider.AddAsync(user.Id).ConfigureAwait(false)
? (Config.Reactions.Success, $"{user.Mention} was successfully added as moderator, you now have access to editing the piracy trigger list and other useful things! " +
"I will send you the available commands to your message box!")
: (Config.Reactions.Failure, $"{user.Mention} is already a moderator");
await Task.WhenAll(
ctx.Message.CreateReactionAsync(result.reaction),
ctx.RespondAsync(result.msg),
typingTask
).ConfigureAwait(false);
if (await ModProvider.AddAsync(user.Id).ConfigureAwait(false))
{
await ctx.ReactWithAsync(Config.Reactions.Success,
$"{user.Mention} was successfully added as moderator!\n" +
"Try using `!help` to see new commands available to you"
).ConfigureAwait(false);
}
else
await ctx.ReactWithAsync(Config.Reactions.Failure, $"{user.Mention} is already a moderator").ConfigureAwait(false);
}
[Command("remove"), Aliases("delete", "del")]
[Command("remove"), Aliases("delete", "del"), TriggersTyping]
[Description("Removes a moderator")]
public async Task Remove(CommandContext ctx, [Description("Discord user to remove from the bot mod list")] DiscordMember user)
{
var typingTask = ctx.TriggerTypingAsync();
(DiscordEmoji reaction, string msg) result;
if (user.Id == Config.BotAdminId)
{
result = (Config.Reactions.Denied, $"{ctx.Message.Author.Mention} why would you even try this?! Alerting {user.Mention}");
var dm = await user.CreateDmChannelAsync().ConfigureAwait(false);
await dm.SendMessageAsync($@"Just letting you know that {ctx.Message.Author.Mention} just tried to strip you off of your mod role ¯\_(ツ)_/¯").ConfigureAwait(false);
await ctx.ReactWithAsync(Config.Reactions.Denied, $"{ctx.Message.Author.Mention} why would you even try this?! Alerting {user.Mention}", true).ConfigureAwait(false);
}
else if (await ModProvider.RemoveAsync(user.Id).ConfigureAwait(false))
result = (Config.Reactions.Success, $"{user.Mention} removed as moderator!");
await ctx.ReactWithAsync(Config.Reactions.Success, $"{user.Mention} removed as moderator!").ConfigureAwait(false);
else
result = (Config.Reactions.Failure, $"{user.Mention} is not a moderator");
await Task.WhenAll(
ctx.Message.CreateReactionAsync(result.reaction),
ctx.RespondAsync(result.msg),
typingTask
).ConfigureAwait(false);
await ctx.ReactWithAsync(Config.Reactions.Failure, $"{user.Mention} is not a moderator").ConfigureAwait(false);
}
[Command("list"), Aliases("show")]
[Command("list"), Aliases("show"), TriggersTyping]
[Description("Lists all moderators")]
public async Task List(CommandContext ctx)
{
var typingTask = ctx.TriggerTypingAsync();
var list = new StringBuilder("```");
foreach (var mod in ModProvider.Mods.Values)
list.AppendLine($"{await ctx.GetUserNameAsync(mod.DiscordId),-32} | {(mod.Sudoer ? "sudo" : "not sudo")}");
await ctx.SendAutosplitMessageAsync(list.Append("```")).ConfigureAwait(false);
await typingTask.ConfigureAwait(false);
}
[Command("sudo")]
[Command("sudo"), TriggersTyping]
[Description("Makes a moderator a sudoer")]
public async Task Sudo(CommandContext ctx, [Description("Discord user on the moderator list to grant the sudoer rights to")] DiscordMember moderator)
{
var typingTask = ctx.TriggerTypingAsync();
(DiscordEmoji reaction, string msg) result;
if (ModProvider.IsMod(moderator.Id))
{
result = await ModProvider.MakeSudoerAsync(moderator.Id).ConfigureAwait(false)
? (Config.Reactions.Success, $"{moderator.Mention} is now a sudoer")
: (Config.Reactions.Failure, $"{moderator.Mention} is already a sudoer");
if (await ModProvider.MakeSudoerAsync(moderator.Id).ConfigureAwait(false))
await ctx.ReactWithAsync(Config.Reactions.Success, $"{moderator.Mention} is now a sudoer").ConfigureAwait(false);
else
await ctx.ReactWithAsync(Config.Reactions.Failure, $"{moderator.Mention} is already a sudoer").ConfigureAwait(false);
}
else
result = (Config.Reactions.Failure, $"{moderator.Mention} is not a moderator (yet)");
await Task.WhenAll(
ctx.Message.CreateReactionAsync(result.reaction),
ctx.RespondAsync(result.msg),
typingTask
).ConfigureAwait(false);
await ctx.ReactWithAsync(Config.Reactions.Failure, $"{moderator.Mention} is not a moderator (yet)").ConfigureAwait(false);
}
[Command("unsudo")]
[Command("unsudo"), TriggersTyping]
[Description("Makes a sudoer a regular moderator")]
public async Task Unsudo(CommandContext ctx, [Description("Discord user on the moderator list to strip the sudoer rights from")] DiscordMember sudoer)
{
var typingTask = ctx.TriggerTypingAsync();
(DiscordEmoji reaction, string msg) result;
if (sudoer.Id == Config.BotAdminId)
{
result = (Config.Reactions.Denied, $"{ctx.Message.Author.Mention} why would you even try this?! Alerting {sudoer.Mention}");
var dm = await sudoer.CreateDmChannelAsync().ConfigureAwait(false);
await dm.SendMessageAsync($@"Just letting you know that {ctx.Message.Author.Mention} just tried to strip you off of your sudo permissions ¯\_(ツ)_/¯").ConfigureAwait(false);
await ctx.ReactWithAsync(Config.Reactions.Denied, $"{ctx.Message.Author.Mention} why would you even try this?! Alerting {sudoer.Mention}", true).ConfigureAwait(false);
}
else if (ModProvider.IsMod(sudoer.Id))
{
result = await ModProvider.UnmakeSudoerAsync(sudoer.Id).ConfigureAwait(false)
? (Config.Reactions.Success, $"{sudoer.Mention} is no longer a sudoer")
: (Config.Reactions.Failure, $"{sudoer.Mention} is not a sudoer");
if (await ModProvider.UnmakeSudoerAsync(sudoer.Id).ConfigureAwait(false))
await ctx.ReactWithAsync(Config.Reactions.Success, $"{sudoer.Mention} is no longer a sudoer").ConfigureAwait(false);
else
await ctx.ReactWithAsync(Config.Reactions.Failure, $"{sudoer.Mention} is not a sudoer").ConfigureAwait(false);
}
else
result = (Config.Reactions.Failure, $"{sudoer.Mention} is not even a moderator!");
await Task.WhenAll(
ctx.Message.CreateReactionAsync(result.reaction),
ctx.RespondAsync(result.msg),
typingTask
).ConfigureAwait(false);
await ctx.ReactWithAsync(Config.Reactions.Failure, $"{sudoer.Mention} is not even a moderator!").ConfigureAwait(false);
}
}
}

View File

@ -8,7 +8,7 @@ namespace CompatBot.Commands
{
[Group("sudo"), RequiresBotSudoerRole]
[Description("Used to manage bot moderators and sudoers")]
internal sealed partial class Sudo : BaseCommandModule
internal sealed partial class Sudo : BaseCommandModuleCustom
{
[Command("say"), Priority(10)]
[Description("Make bot say things, optionally in a specific channel")]

View File

@ -15,43 +15,36 @@ namespace CompatBot.Commands
{
internal sealed partial class Warnings
{
[Group("list"), Aliases("show")]
[Group("list"), Aliases("show"), TriggersTyping]
[Description("Allows to list warnings in various ways. Users can only see their own warnings.")]
public class ListGroup : BaseCommandModule
public class ListGroup : BaseCommandModuleCustom
{
[GroupCommand, Priority(10)]
[Description("Show warning list for a user. Default is to show warning list for yourself")]
public async Task List(CommandContext ctx, [Description("Discord user to list warnings for")] DiscordUser user)
{
var typingTask = ctx.TriggerTypingAsync();
if (await CheckListPermissionAsync(ctx, user.Id).ConfigureAwait(false))
await ListUserWarningsAsync(ctx.Client, ctx.Message, user.Id, user.Username.Sanitize(), false);
await typingTask.ConfigureAwait(false);
}
[GroupCommand]
public async Task List(CommandContext ctx, [Description("Id of the user to list warnings for")] ulong userId)
{
var typingTask = ctx.TriggerTypingAsync();
if (await CheckListPermissionAsync(ctx, userId).ConfigureAwait(false))
await ListUserWarningsAsync(ctx.Client, ctx.Message, userId, $"<@{userId}>", false);
await typingTask.ConfigureAwait(false);
}
[GroupCommand]
[Description("List your own warning list")]
public async Task List(CommandContext ctx)
{
var typingTask = ctx.TriggerTypingAsync();
await List(ctx, ctx.Message.Author).ConfigureAwait(false);
await typingTask.ConfigureAwait(false);
}
[Command("users"), RequiresBotModRole]
[Command("users"), RequiresBotModRole, TriggersTyping]
[Description("List users with warnings, sorted from most warned to least")]
public async Task Users(CommandContext ctx)
{
await ctx.TriggerTypingAsync().ConfigureAwait(false);
var userIdColumn = ctx.Channel.IsPrivate ? $"{"User ID",-18} | " : "";
var header = $"{"User",-25} | {userIdColumn}Count";
var result = new StringBuilder("Warning count per user:").AppendLine("```")
@ -77,11 +70,10 @@ namespace CompatBot.Commands
await ctx.SendAutosplitMessageAsync(result.Append("```")).ConfigureAwait(false);
}
[Command("recent"), Aliases("last", "all"), RequiresBotModRole]
[Command("recent"), Aliases("last", "all"), RequiresBotModRole, TriggersTyping]
[Description("Shows last issued warnings in chronological order")]
public async Task Last(CommandContext ctx, [Description("Optional number of items to show. Default is 10")] int number = 10)
{
await ctx.TriggerTypingAsync().ConfigureAwait(false);
if (number < 1)
number = 10;
var userIdColumn = ctx.Channel.IsPrivate ? $"{"User ID",-18} | " : "";
@ -116,10 +108,7 @@ namespace CompatBot.Commands
if (userId == ctx.Message.Author.Id || ModProvider.IsMod(ctx.Message.Author.Id))
return true;
await Task.WhenAll(
ctx.Message.CreateReactionAsync(Config.Reactions.Denied),
ctx.RespondAsync("Regular users can only view their own warnings")
).ConfigureAwait(false);
await ctx.ReactWithAsync(Config.Reactions.Denied, "Regular users can only view their own warnings").ConfigureAwait(false);
return false;
}
}

View File

@ -16,7 +16,7 @@ namespace CompatBot.Commands
{
[Group("warn")]
[Description("Command used to manage warnings")]
internal sealed partial class Warnings: BaseCommandModule
internal sealed partial class Warnings: BaseCommandModuleCustom
{
[GroupCommand] //attributes on overloads do not work, so no easy permission checks
[Description("Command used to issue a new warning")]
@ -26,12 +26,11 @@ namespace CompatBot.Commands
if (!await new RequiresBotModRole().ExecuteCheckAsync(ctx, false).ConfigureAwait(false))
return;
var typingTask = ctx.TriggerTypingAsync();
await ctx.TriggerTypingAsync().ConfigureAwait(false);
if (await AddAsync(ctx, user.Id, user.Username.Sanitize(), ctx.Message.Author, reason).ConfigureAwait(false))
await ctx.Message.CreateReactionAsync(Config.Reactions.Success).ConfigureAwait(false);
await ctx.ReactWithAsync(Config.Reactions.Success).ConfigureAwait(false);
else
await ctx.Message.CreateReactionAsync(Config.Reactions.Failure).ConfigureAwait(false);
await typingTask;
await ctx.ReactWithAsync(Config.Reactions.Failure, "Couldn't save the warning, please try again").ConfigureAwait(false);
}
[GroupCommand, RequiresBotModRole]
@ -40,19 +39,17 @@ namespace CompatBot.Commands
if (!await new RequiresBotModRole().ExecuteCheckAsync(ctx, false).ConfigureAwait(false))
return;
var typingTask = ctx.TriggerTypingAsync();
await ctx.TriggerTypingAsync().ConfigureAwait(false);
if (await AddAsync(ctx, userId, $"<@{userId}>", ctx.Message.Author, reason).ConfigureAwait(false))
await ctx.Message.CreateReactionAsync(Config.Reactions.Success).ConfigureAwait(false);
await ctx.ReactWithAsync(Config.Reactions.Success).ConfigureAwait(false);
else
await ctx.Message.CreateReactionAsync(Config.Reactions.Failure).ConfigureAwait(false);
await typingTask;
await ctx.ReactWithAsync(Config.Reactions.Failure, "Couldn't save the warning, please try again").ConfigureAwait(false);
}
[Command("remove"), Aliases("delete", "del"), RequiresBotModRole]
[Command("remove"), Aliases("delete", "del"), RequiresBotModRole, TriggersTyping]
[Description("Removes specified warnings")]
public async Task Remove(CommandContext ctx, [Description("Warning IDs to remove separated with space")] params int[] ids)
{
var typingTask = ctx.TriggerTypingAsync();
int removedCount;
using (var db = new BotDb())
{
@ -60,14 +57,10 @@ namespace CompatBot.Commands
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}");
await Task.WhenAll(
ctx.RespondAsync(result.msg),
ctx.Message.CreateReactionAsync(result.reaction),
typingTask
).ConfigureAwait(false);
if (removedCount == ids.Length)
await ctx.RespondAsync($"Warning{(ids.Length == 1 ? "" : "s")} successfully removed!").ConfigureAwait(false);
else
await ctx.RespondAsync($"Removed {removedCount} items, but was asked to remove {ids.Length}").ConfigureAwait(false);
}
[Command("clear"), RequiresBotModRole]
@ -77,12 +70,11 @@ namespace CompatBot.Commands
return Clear(ctx, user.Id);
}
[Command("clear"), RequiresBotModRole]
[Command("clear"), RequiresBotModRole, TriggersTyping]
public async Task Clear(CommandContext ctx, [Description("User ID to clear warnings for")] ulong userId)
{
try
{
var typingTask = ctx.TriggerTypingAsync();
//var removed = await BotDb.Instance.Database.ExecuteSqlCommandAsync($"DELETE FROM `warning` WHERE `discord_id`={userId}").ConfigureAwait(false);
int removed;
using (var db = new BotDb())
@ -91,11 +83,7 @@ namespace CompatBot.Commands
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),
typingTask
).ConfigureAwait(false);
await ctx.RespondAsync($"{removed} warning{(removed == 1 ? "" : "s")} successfully removed!").ConfigureAwait(false);
}
catch (Exception e)
{

View File

@ -1,8 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.1</TargetFramework>
<TieredCompilation>true</TieredCompilation>
<RootNamespace>CompatBot</RootNamespace>
</PropertyGroup>

View File

@ -11,6 +11,7 @@ namespace CompatBot.Database
public DbSet<Piracystring> Piracystring { get; set; }
public DbSet<Warning> Warning { get; set; }
public DbSet<Explanation> Explanation { get; set; }
public DbSet<DisabledCommand> DisabledCommands { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
@ -21,12 +22,10 @@ namespace CompatBot.Database
{
//configure indices
modelBuilder.Entity<Moderator>().HasIndex(m => m.DiscordId).IsUnique().HasName("moderator_discord_id");
modelBuilder.Entity<Piracystring>().HasIndex(ps => ps.String).IsUnique().HasName("piracystring_string");
modelBuilder.Entity<Warning>().HasIndex(w => w.DiscordId).HasName("warning_discord_id");
modelBuilder.Entity<Explanation>().HasIndex(e => e.Keyword).IsUnique().HasName("explanation_keyword");
modelBuilder.Entity<DisabledCommand>().HasIndex(e => e.Command).IsUnique().HasName("disabled_command_command");
//configure default policy of Id being the primary key
modelBuilder.ConfigureDefaultPkConvention();
@ -70,4 +69,11 @@ namespace CompatBot.Database
[Required]
public string Text { get; set; }
}
internal class DisabledCommand
{
public int Id { get; set; }
[Required]
public string Command { get; set; }
}
}

View File

@ -0,0 +1,142 @@
// <auto-generated />
using System;
using CompatBot.Database;
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Infrastructure;
using Microsoft.EntityFrameworkCore.Migrations;
using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace CompatBot.Database.Migrations
{
[DbContext(typeof(BotDb))]
[Migration("20180804225045_DisabledCommands")]
partial class DisabledCommands
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
{
#pragma warning disable 612, 618
modelBuilder
.HasAnnotation("ProductVersion", "2.1.1-rtm-30846");
modelBuilder.Entity("CompatBot.Database.DisabledCommand", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("id");
b.Property<string>("Command")
.IsRequired()
.HasColumnName("command");
b.HasKey("Id")
.HasName("id");
b.HasIndex("Command")
.IsUnique()
.HasName("disabled_command_command");
b.ToTable("disabled_commands");
});
modelBuilder.Entity("CompatBot.Database.Explanation", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("id");
b.Property<string>("Keyword")
.IsRequired()
.HasColumnName("keyword");
b.Property<string>("Text")
.IsRequired()
.HasColumnName("text");
b.HasKey("Id")
.HasName("id");
b.HasIndex("Keyword")
.IsUnique()
.HasName("explanation_keyword");
b.ToTable("explanation");
});
modelBuilder.Entity("CompatBot.Database.Moderator", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("id");
b.Property<ulong>("DiscordId")
.HasColumnName("discord_id");
b.Property<bool>("Sudoer")
.HasColumnName("sudoer");
b.HasKey("Id")
.HasName("id");
b.HasIndex("DiscordId")
.IsUnique()
.HasName("moderator_discord_id");
b.ToTable("moderator");
});
modelBuilder.Entity("CompatBot.Database.Piracystring", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("id");
b.Property<string>("String")
.IsRequired()
.HasColumnName("string")
.HasColumnType("varchar(255)");
b.HasKey("Id")
.HasName("id");
b.HasIndex("String")
.IsUnique()
.HasName("piracystring_string");
b.ToTable("piracystring");
});
modelBuilder.Entity("CompatBot.Database.Warning", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("id");
b.Property<ulong>("DiscordId")
.HasColumnName("discord_id");
b.Property<string>("FullReason")
.IsRequired()
.HasColumnName("full_reason");
b.Property<ulong>("IssuerId")
.HasColumnName("issuer_id");
b.Property<string>("Reason")
.IsRequired()
.HasColumnName("reason");
b.Property<long?>("Timestamp")
.HasColumnName("timestamp");
b.HasKey("Id")
.HasName("id");
b.HasIndex("DiscordId")
.HasName("warning_discord_id");
b.ToTable("warning");
});
#pragma warning restore 612, 618
}
}
}

View File

@ -0,0 +1,35 @@
using Microsoft.EntityFrameworkCore.Migrations;
namespace CompatBot.Database.Migrations
{
public partial class DisabledCommands : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "disabled_commands",
columns: table => new
{
id = table.Column<int>(nullable: false)
.Annotation("Sqlite:Autoincrement", true),
command = table.Column<string>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("id", x => x.id);
});
migrationBuilder.CreateIndex(
name: "disabled_command_command",
table: "disabled_commands",
column: "command",
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "disabled_commands");
}
}
}

View File

@ -16,6 +16,26 @@ namespace CompatBot.Database.Migrations
modelBuilder
.HasAnnotation("ProductVersion", "2.1.1-rtm-30846");
modelBuilder.Entity("CompatBot.Database.DisabledCommand", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnName("id");
b.Property<string>("Command")
.IsRequired()
.HasColumnName("command");
b.HasKey("Id")
.HasName("id");
b.HasIndex("Command")
.IsUnique()
.HasName("disabled_command_command");
b.ToTable("disabled_commands");
});
modelBuilder.Entity("CompatBot.Database.Explanation", b =>
{
b.Property<int>("Id")

View File

@ -8,7 +8,7 @@ using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
namespace CompatBot.Migrations
{
[DbContext(typeof(ThumbnailDb))]
[Migration("20180801095653_InitialCreate")]
[Migration("20180804120920_InitialCreate")]
partial class InitialCreate
{
protected override void BuildTargetModel(ModelBuilder modelBuilder)
@ -54,6 +54,9 @@ namespace CompatBot.Migrations
b.Property<string>("EmbeddableUrl")
.HasColumnName("embeddable_url");
b.Property<string>("Name")
.HasColumnName("name");
b.Property<string>("ProductCode")
.IsRequired()
.HasColumnName("product_code");

View File

@ -28,6 +28,7 @@ namespace CompatBot.Migrations
.Annotation("Sqlite:Autoincrement", true),
product_code = table.Column<string>(nullable: false),
content_id = table.Column<string>(nullable: true),
name = table.Column<string>(nullable: true),
url = table.Column<string>(nullable: true),
embeddable_url = table.Column<string>(nullable: true),
timestamp = table.Column<long>(nullable: false)

View File

@ -52,6 +52,9 @@ namespace CompatBot.Migrations
b.Property<string>("EmbeddableUrl")
.HasColumnName("embeddable_url");
b.Property<string>("Name")
.HasColumnName("name");
b.Property<string>("ProductCode")
.IsRequired()
.HasColumnName("product_code");

View File

@ -0,0 +1,60 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace CompatBot.Database.Providers
{
internal static class DisabledCommandsProvider
{
private static readonly HashSet<string> DisabledCommands = new HashSet<string>(StringComparer.InvariantCultureIgnoreCase);
static DisabledCommandsProvider()
{
lock (DisabledCommands)
using (var db = new BotDb())
foreach (var cmd in db.DisabledCommands.ToList())
DisabledCommands.Add(cmd.Command);
}
public static HashSet<string> Get() => DisabledCommands;
public static void Disable(string command)
{
lock (DisabledCommands)
if (DisabledCommands.Add(command))
using (var db = new BotDb())
{
db.DisabledCommands.Add(new DisabledCommand {Command = command});
db.SaveChanges();
}
}
public static void Enable(string command)
{
lock (DisabledCommands)
if (DisabledCommands.Remove(command))
using (var db = new BotDb())
{
var cmd = db.DisabledCommands.FirstOrDefault(c => c.Command == command);
if (cmd == null)
return;
db.DisabledCommands.Remove(cmd);
db.SaveChanges();
}
}
public static void Clear()
{
lock (DisabledCommands)
{
DisabledCommands.Clear();
using (var db = new BotDb())
{
db.DisabledCommands.RemoveRange(db.DisabledCommands);
db.SaveChanges();
}
}
}
}
}

View File

@ -31,11 +31,18 @@ namespace CompatBot.Database.Providers
try
{
using (var httpClient = new HttpClient())
using (var img = await httpClient.GetStreamAsync(url).ConfigureAwait(false))
using (var imgStream = await httpClient.GetStreamAsync(url).ConfigureAwait(false))
using (var memStream = new MemoryStream())
{
await imgStream.CopyToAsync(memStream).ConfigureAwait(false);
// minimum jpg size is 119 bytes, png is 67 bytes
if (memStream.Length < 64)
return null;
memStream.Seek(0, SeekOrigin.Begin);
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);
//var message = await spam.SendFileAsync(memStream, (thumb.ContentId ?? thumb.ProductCode) + ".jpg").ConfigureAwait(false);
var contentName = (thumb.ContentId ?? thumb.ProductCode);
var message = await spam.SendFileAsync(contentName + ".jpg", memStream, contentName).ConfigureAwait(false);
thumb.EmbeddableUrl = message.Attachments.First().Url;
await db.SaveChangesAsync().ConfigureAwait(false);
return thumb.EmbeddableUrl;
@ -49,5 +56,14 @@ namespace CompatBot.Database.Providers
}
return null;
}
public static string GetTitleName(string productCode)
{
using (var db = new ThumbnailDb())
{
var thumb = db.Thumbnail.FirstOrDefault(t => t.ProductCode == productCode.ToUpperInvariant());
return thumb?.Name;
}
}
}
}

View File

@ -43,6 +43,7 @@ namespace CompatBot.Database
[Required]
public string ProductCode { get; set; }
public string ContentId { get; set; }
public string Name { get; set; }
public string Url { get; set; }
public string EmbeddableUrl { get; set; }
public long Timestamp { get; set; }

View File

@ -38,7 +38,7 @@ namespace CompatBot.EventHandlers.LogParsing
Extractors = new Dictionary<string, Regex>
{
["Serial:"] = new Regex(@"Serial: (?<serial>[A-z]{4}\d{5})\r?$", DefaultOptions),
["Path:"] = new Regex(@"Path: ((?<win_path>\w:/)|(?<lin_path>/[^/])).*?\r?$", DefaultOptions),
["Path:"] = new Regex(@"Path: ((?<win_path>\w:/)|(?<lin_path>/[^/]))(.*(?<hdd_game_path>/dev_hdd0/game/.*)/USRDIR.*?|.*?)\r?$", DefaultOptions),
["custom config:"] = new Regex("custom config: (?<custom_config>.*?)\r?$", DefaultOptions),
},
OnNewLineAsync = PiracyCheckAsync,

View File

@ -63,8 +63,8 @@ namespace CompatBot.EventHandlers.LogParsing
else if (result.IsCompleted)
await FlushAllLinesAsync(result.Buffer, currentSectionLines, state).ConfigureAwait(false);
var sectionStart = currentSectionLines.Count == 0 ? buffer : currentSectionLines.First.Value;
reader.AdvanceTo(sectionStart.Start, buffer.End);
totalReadBytes += result.Buffer.Slice(0, sectionStart.Start).Length;
reader.AdvanceTo(sectionStart.Start, buffer.End);
if (totalReadBytes >= Config.LogSizeLimit)
{
state.Error = LogParseState.ErrorCode.SizeLimit;

View File

@ -53,7 +53,15 @@ namespace CompatBot.EventHandlers.LogParsing
#if DEBUG
Console.WriteLine($"regex {group.Name} = {group.Value}");
#endif
state.WipCollection[group.Name] = group.Value.ToUtf8();
if (group.Name == "rap_file")
{
var currentValue = state.WipCollection[group.Name];
if (!string.IsNullOrEmpty(currentValue))
currentValue += Environment.NewLine;
state.WipCollection[group.Name] = currentValue + group.Value.ToUtf8();
}
else
state.WipCollection[group.Name] = group.Value.ToUtf8();
}
}

View File

@ -52,9 +52,7 @@ namespace CompatBot.EventHandlers
}
var previousReplies = previousRepliesBuilder?.ToString() ?? "";
var codesToLookup = ProductCode.Matches(args.Message.Content)
.Select(match => (match.Groups["letters"].Value + match.Groups["numbers"]).ToUpper())
.Distinct()
var codesToLookup = GetProductIds(args.Message.Content)
.Where(c => !previousReplies.Contains(c, StringComparison.InvariantCultureIgnoreCase))
.Take(args.Channel.IsPrivate ? 50 : 5)
.ToList();
@ -76,6 +74,14 @@ namespace CompatBot.EventHandlers
}
}
public static List<string> GetProductIds(string input)
{
return ProductCode.Matches(input)
.Select(match => (match.Groups["letters"].Value + match.Groups["numbers"]).ToUpper())
.Distinct()
.ToList();
}
private static bool NeedToSilence(DiscordMessage msg)
{
if (string.IsNullOrEmpty(msg.Content))
@ -108,16 +114,14 @@ namespace CompatBot.EventHandlers
if (result?.ReturnCode == -1)
return TitleInfo.CommunicationError.AsEmbed(null, footer);
var thumbnailUrl = await client.GetThumbnailUrlAsync(code).ConfigureAwait(false);
if (result?.Results == null)
return TitleInfo.Unknown.AsEmbed(code, footer);
return TitleInfo.Unknown.AsEmbed(code, footer, thumbnailUrl);
if (result.Results.TryGetValue(code, out var info))
{
var thumbnailUrl = await client.GetThumbnailUrlAsync(code).ConfigureAwait(false);
return info.AsEmbed(code, footer, thumbnailUrl);
}
return TitleInfo.Unknown.AsEmbed(code, footer);
return TitleInfo.Unknown.AsEmbed(code, footer, thumbnailUrl);
}
catch (Exception e)
{

View File

@ -104,7 +104,7 @@ namespace CompatBot.EventHandlers
if (reporters.Count < Config.Moderation.StarbucksThreshold)
return;
await message.CreateReactionAsync(emoji).ConfigureAwait(false);
await message.ReactWithAsync(client, emoji).ConfigureAwait(false);
await client.ReportAsync("User moderation report ⭐💵", message, reporters).ConfigureAwait(false);
}

View File

@ -0,0 +1,32 @@
using System.Linq;
using System.Threading.Tasks;
using CompatBot.Database;
using DSharpPlus.EventArgs;
namespace CompatBot.EventHandlers
{
internal static class ThumbnailCacheMonitor
{
public static async Task OnMessageDeleted(MessageDeleteEventArgs args)
{
if (args.Channel.Id != Config.ThumbnailSpamId)
return;
if (string.IsNullOrEmpty(args.Message.Content))
return;
if (!args.Message.Attachments.Any())
return;
using (var db = new ThumbnailDb())
{
var thumb = db.Thumbnail.FirstOrDefault(i => i.ContentId == args.Message.Content);
if (thumb?.EmbeddableUrl is string url && !string.IsNullOrEmpty(url) && args.Message.Attachments.Any(a => a.Url == url))
{
thumb.EmbeddableUrl = null;
await db.SaveChangesAsync(Config.Cts.Token).ConfigureAwait(false);
}
}
}
}
}

View File

@ -1,4 +1,5 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using CompatBot.Commands;
using CompatBot.Commands.Converters;
@ -13,93 +14,137 @@ namespace CompatBot
{
internal static class Program
{
private static readonly SemaphoreSlim InstanceCheck = new SemaphoreSlim(0, 1);
private static readonly SemaphoreSlim ShutdownCheck = new SemaphoreSlim(0, 1);
internal static async Task Main(string[] args)
{
if (string.IsNullOrEmpty(Config.Token))
var thread = new Thread(() =>
{
using (var instanceLock = new Mutex(false, @"Global\RPCS3 Compatibility Bot"))
{
if (instanceLock.WaitOne(1000))
try
{
InstanceCheck.Release();
ShutdownCheck.Wait();
}
finally
{
instanceLock.ReleaseMutex();
}
}
});
try
{
Console.WriteLine("No token was specified.");
return;
thread.Start();
if (!InstanceCheck.Wait(1000))
{
Console.WriteLine("Another instance is already running.");
return;
}
if (string.IsNullOrEmpty(Config.Token))
{
Console.WriteLine("No token was specified.");
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
{
Token = Config.Token,
TokenType = TokenType.Bot,
UseInternalLogHandler = true,
//LogLevel = LogLevel.Debug,
};
using (var client = new DiscordClient(config))
{
var commands = client.UseCommandsNext(new CommandsNextConfiguration
{
StringPrefixes = new[] {Config.CommandPrefix},
Services = new ServiceCollection().BuildServiceProvider(),
});
commands.RegisterConverter(new CustomDiscordChannelConverter());
commands.RegisterCommands<Misc>();
commands.RegisterCommands<CompatList>();
commands.RegisterCommands<Sudo>();
commands.RegisterCommands<Antipiracy>();
commands.RegisterCommands<Warnings>();
commands.RegisterCommands<Explain>();
commands.RegisterCommands<Psn>();
client.Ready += async r =>
{
Console.WriteLine("Bot is ready to serve!");
Console.WriteLine();
Console.WriteLine($"Bot user id : {r.Client.CurrentUser.Id} ({r.Client.CurrentUser.Username})");
Console.WriteLine($"Bot admin id : {Config.BotAdminId} ({(await r.Client.GetUserAsync(Config.BotAdminId)).Username})");
Console.WriteLine();
Console.WriteLine("Checking starbucks backlog...");
await r.Client.CheckBacklog().ConfigureAwait(false);
Console.WriteLine("Starbucks checked.");
};
client.MessageReactionAdded += Starbucks.Handler;
client.MessageCreated += AntipiracyMonitor.OnMessageCreated; // should be first
client.MessageCreated += ProductCodeLookup.OnMessageMention;
client.MessageCreated += LogInfoHandler.OnMessageCreated;
client.MessageCreated += LogsAsTextMonitor.OnMessageCreated;
client.MessageUpdated += AntipiracyMonitor.OnMessageEdit;
client.MessageDeleted += ThumbnailCacheMonitor.OnMessageDeleted;
try
{
await client.ConnectAsync();
}
catch (Exception e)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(e.Message);
Console.ResetColor();
Console.WriteLine("Terminating.");
return;
}
if (args.Length > 1 && ulong.TryParse(args[1], out var channelId))
{
Console.WriteLine("Found channelId: " + args[1]);
var channel = await client.GetChannelAsync(channelId).ConfigureAwait(false);
await channel.SendMessageAsync("Bot is up and running").ConfigureAwait(false);
}
while (!Config.Cts.IsCancellationRequested)
{
if (client.Ping > 1000)
await client.ReconnectAsync();
await Task.Delay(TimeSpan.FromMinutes(1), Config.Cts.Token).ContinueWith(dt => {/* in case it was cancelled */}).ConfigureAwait(false);
}
}
await psnScrappingTask.ConfigureAwait(false);
}
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
catch (TaskCanceledException)
{
Token = Config.Token,
TokenType = TokenType.Bot,
UseInternalLogHandler = true,
//LogLevel = LogLevel.Debug,
};
using (var client = new DiscordClient(config))
{
var commands = client.UseCommandsNext(new CommandsNextConfiguration {StringPrefixes = new[] {Config.CommandPrefix}, Services = new ServiceCollection().BuildServiceProvider()});
commands.RegisterConverter(new CustomDiscordChannelConverter());
commands.RegisterCommands<Misc>();
commands.RegisterCommands<CompatList>();
commands.RegisterCommands<Sudo>();
commands.RegisterCommands<Antipiracy>();
commands.RegisterCommands<Warnings>();
commands.RegisterCommands<Explain>();
client.Ready += async r =>
{
Console.WriteLine("Bot is ready to serve!");
Console.WriteLine();
Console.WriteLine($"Bot user id : {r.Client.CurrentUser.Id} ({r.Client.CurrentUser.Username})");
Console.WriteLine($"Bot admin id : {Config.BotAdminId} ({(await r.Client.GetUserAsync(Config.BotAdminId)).Username})");
Console.WriteLine();
Console.WriteLine("Checking starbucks backlog...");
await r.Client.CheckBacklog().ConfigureAwait(false);
Console.WriteLine("Starbucks checked.");
};
client.MessageReactionAdded += Starbucks.Handler;
client.MessageCreated += AntipiracyMonitor.OnMessageCreated; // should be first
client.MessageCreated += ProductCodeLookup.OnMessageMention;
client.MessageCreated += LogInfoHandler.OnMessageCreated;
client.MessageCreated += LogsAsTextMonitor.OnMessageCreated;
client.MessageUpdated += AntipiracyMonitor.OnMessageEdit;
try
{
await client.ConnectAsync();
}
catch (Exception e)
{
Console.ForegroundColor = ConsoleColor.Red;
Console.WriteLine(e.Message);
Console.ResetColor();
Console.WriteLine("Terminating.");
return;
}
if (args.Length > 1 && ulong.TryParse(args[1], out var channelId))
{
Console.WriteLine("Found channelId: " + args[1]);
var channel = await client.GetChannelAsync(channelId).ConfigureAwait(false);
await channel.SendMessageAsync("Bot is up and running").ConfigureAwait(false);
}
while (!Config.Cts.IsCancellationRequested)
{
if (client.Ping > 1000)
await client.ReconnectAsync();
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");
finally
{
ShutdownCheck.Release();
thread.Join(100);
Console.WriteLine("Exiting");
}
}
}
}

View File

@ -17,6 +17,9 @@ namespace CompatBot.ThumbScrapper
{
private static readonly PsnClient.Client Client = new PsnClient.Client();
private static readonly Regex ContentIdMatcher = new Regex(@"(?<service_id>(?<service_letters>\w\w)(?<service_number>\d{4}))-(?<product_id>(?<product_letters>\w{4})(?<product_number>\d{5}))_(?<part>\d\d)-(?<label>\w{16})", RegexOptions.Compiled | RegexOptions.IgnoreCase | RegexOptions.Singleline | RegexOptions.ExplicitCapture);
private static readonly SemaphoreSlim LockObj = new SemaphoreSlim(1, 1);
private static List<string> PsnStores = new List<string>();
private static DateTime StoreRefreshTimestamp = DateTime.MinValue;
public async Task Run(CancellationToken cancellationToken)
{
@ -26,7 +29,7 @@ namespace CompatBot.ThumbScrapper
break;
await ScrapeStateProvider.CleanAsync(cancellationToken).ConfigureAwait(false);
await RefreshStoresAsync(cancellationToken).ConfigureAwait(false);
try
{
await DoScrapePassAsync(cancellationToken).ConfigureAwait(false);
@ -39,18 +42,59 @@ namespace CompatBot.ThumbScrapper
} while (!cancellationToken.IsCancellationRequested);
}
private static async Task RefreshStoresAsync(CancellationToken cancellationToken)
{
try
{
if (ScrapeStateProvider.IsFresh(StoreRefreshTimestamp))
return;
var knownLocales = await Client.GetLocales(cancellationToken).ConfigureAwait(false);
var enabledLocales = knownLocales.EnabledLocales ?? new string[0];
var result = GetLocalesInPreferredOrder(enabledLocales);
await LockObj.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
if (ScrapeStateProvider.IsFresh(StoreRefreshTimestamp))
return;
PsnStores = result;
StoreRefreshTimestamp = DateTime.UtcNow;
}
finally
{
LockObj.Release();
}
}
catch (Exception e)
{
PrintError(e);
}
}
private static async Task DoScrapePassAsync(CancellationToken cancellationToken)
{
var knownLocales = await Client.GetLocales(cancellationToken).ConfigureAwait(false);
var enabledLocales = knownLocales.EnabledLocales ?? new string[0];
foreach (var locale in GetLocalesInPreferredOrder(enabledLocales))
List<string> storesToScrape;
await LockObj.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
storesToScrape = new List<string>(PsnStores);
}
finally
{
LockObj.Release();
}
var percentPerStore = 1.0 / storesToScrape.Count;
for (var storeIdx = 0; storeIdx < storesToScrape.Count; storeIdx++)
{
var locale = storesToScrape[storeIdx];
if (cancellationToken.IsCancellationRequested)
break;
if (ScrapeStateProvider.IsFresh(locale))
{
Console.WriteLine($"Cache for {locale} PSN is fresh, skipping");
//Console.WriteLine($"Cache for {locale} PSN is fresh, skipping");
continue;
}
@ -79,18 +123,22 @@ namespace CompatBot.ThumbScrapper
};
var take = 30;
var returned = 0;
foreach (var containerId in knownContainers)
var containersToScrape = knownContainers.ToList(); //.Where(c => c.Contains("FULL", StringComparison.InvariantCultureIgnoreCase)).ToList();
var percentPerContainer = 1.0 / containersToScrape.Count;
for (var containerIdx = 0; containerIdx < containersToScrape.Count; containerIdx++)
{
var containerId = containersToScrape[containerIdx];
if (cancellationToken.IsCancellationRequested)
return;
if (ScrapeStateProvider.IsFresh(locale, containerId))
{
Console.WriteLine($"\tCache for {locale} container {containerId} is fresh, skipping");
//Console.WriteLine($"\tCache for {locale} container {containerId} is fresh, skipping");
continue;
}
Console.WriteLine($"\tScraping {locale} container {containerId}...");
var currentPercent = storeIdx * percentPerStore + containerIdx * percentPerStore * percentPerContainer;
Console.WriteLine($"\tScraping {locale} container {containerId} ({currentPercent*100:##0.00}%)...");
var total = -1;
var start = 0;
do
@ -151,9 +199,9 @@ namespace CompatBot.ThumbScrapper
}
}
start += take;
} while ((returned > 0 || (total > -1 && start*take <= total)) && !cancellationToken.IsCancellationRequested);
} while ((returned > 0 || (total > -1 && start * take <= total)) && !cancellationToken.IsCancellationRequested);
await ScrapeStateProvider.SetLastRunTimestampAsync(locale, containerId).ConfigureAwait(false);
Console.WriteLine($"\tFinished scraping {locale} container {containerId}, processed {start-take+returned} items");
Console.WriteLine($"\tFinished scraping {locale} container {containerId}, processed {start - take + returned} items");
}
await ScrapeStateProvider.SetLastRunTimestampAsync(locale).ConfigureAwait(false);
}
@ -217,7 +265,7 @@ namespace CompatBot.ThumbScrapper
if (string.IsNullOrEmpty(item.Id))
continue;
await AddOrUpdateThumbnailAsync(item.Id, item.Attributes?.ThumbnailUrlBase, cancellationToken).ConfigureAwait(false);
await AddOrUpdateThumbnailAsync(item.Id, item.Attributes?.Name, item.Attributes?.ThumbnailUrlBase, cancellationToken).ConfigureAwait(false);
break;
case "legacy-sku":
@ -241,7 +289,7 @@ namespace CompatBot.ThumbScrapper
}
}
private static async Task AddOrUpdateThumbnailAsync(string contentId, string url, CancellationToken cancellationToken)
private static async Task AddOrUpdateThumbnailAsync(string contentId, string name, string url, CancellationToken cancellationToken)
{
var match = ContentIdMatcher.Match(contentId);
if (!match.Success)
@ -251,6 +299,7 @@ namespace CompatBot.ThumbScrapper
if (!ProductCodeLookup.ProductCode.IsMatch(productCode))
return;
name = string.IsNullOrEmpty(name) ? null : name;
using (var db = new ThumbnailDb())
{
var savedItem = db.Thumbnail.FirstOrDefault(t => t.ProductCode == productCode);
@ -260,6 +309,7 @@ namespace CompatBot.ThumbScrapper
{
ProductCode = productCode,
ContentId = contentId,
Name = name,
Url = url,
Timestamp = DateTime.UtcNow.Ticks,
};
@ -267,10 +317,15 @@ namespace CompatBot.ThumbScrapper
}
else if (!string.IsNullOrEmpty(url))
{
if (!ScrapeStateProvider.IsFresh(savedItem.Timestamp) && savedItem.Url != url)
if (!ScrapeStateProvider.IsFresh(savedItem.Timestamp))
{
savedItem.Url = url;
savedItem.EmbeddableUrl = null;
if (savedItem.Url != url)
{
savedItem.Url = url;
savedItem.EmbeddableUrl = null;
}
if (name != null && savedItem.Name != name)
savedItem.Name = name;
}
savedItem.ContentId = contentId;
savedItem.Timestamp = DateTime.UtcNow.Ticks;

View File

@ -22,6 +22,16 @@ namespace CompatBot.Utils
return ctx.Client.GetUserNameAsync(ctx.Channel, userId, forDmPurposes, defaultName);
}
public static DiscordMember GetMember(this DiscordClient client, DiscordGuild guild, DiscordUser user)
{
return (from g in client.Guilds
where g.Key == guild.Id
from u in g.Value.Members
where u.Id == user.Id
select u
).FirstOrDefault();
}
public static async Task<string> GetUserNameAsync(this DiscordClient client, DiscordChannel channel, ulong userId, bool? forDmPurposes = null, string defaultName = "Unknown user")
{
var isPrivate = forDmPurposes ?? channel.IsPrivate;
@ -38,6 +48,20 @@ namespace CompatBot.Utils
}
}
public static async Task ReactWithAsync(this DiscordMessage message, DiscordClient client, DiscordEmoji emoji, string fallbackMessage = null, bool showBoth = false)
{
var canReact = message.Channel.PermissionsFor(client.GetMember(message.Channel.Guild, client.CurrentUser)).HasPermission(Permissions.AddReactions);
if (canReact)
await message.CreateReactionAsync(emoji).ConfigureAwait(false);
if ((!canReact || showBoth) && !string.IsNullOrEmpty(fallbackMessage))
await message.Channel.SendMessageAsync(fallbackMessage).ConfigureAwait(false);
}
public static Task ReactWithAsync(this CommandContext ctx, DiscordEmoji emoji, string fallbackMessage = null, bool showBoth = false)
{
return ReactWithAsync(ctx.Message, ctx.Client, emoji, fallbackMessage, showBoth);
}
public static async Task<IReadOnlyCollection<DiscordMessage>> GetMessagesBeforeAsync(this DiscordChannel channel, ulong beforeMessageId, int limit = 100, DateTime? timeLimit = null)
{
if (timeLimit > DateTime.UtcNow)

View File

@ -196,21 +196,28 @@ namespace CompatBot.Utils.ResultFormatters
private static async Task BuildNotesSectionAsync(DiscordEmbedBuilder builder, LogParseState state, NameValueCollection items)
{
if (items["rap_file"] is string rap)
builder.AddField("Missing License", $"Missing `{Path.GetFileName(rap)}`");
{
var licenseNames = rap.Split(Environment.NewLine).Distinct().Select(p => $"`{Path.GetFileName(p)}`");
builder.AddField("Missing Licenses", string.Join(Environment.NewLine, licenseNames));
}
else if (items["fatal_error"] is string fatalError)
builder.AddField("Fatal Error", $"`{fatalError}`");
string notes = null;
var notes = new StringBuilder();
if (string.IsNullOrEmpty(items["ppu_decoder"]) || string.IsNullOrEmpty(items["renderer"]))
notes += "Log is empty. You need to run the game before uploading the log.";
notes.AppendLine("Log is empty. You need to run the game before uploading the log.");
if (!string.IsNullOrEmpty(items["hdd_game_path"]) && (items["serial"]?.StartsWith("BL", StringComparison.InvariantCultureIgnoreCase) ?? false))
notes.AppendLine($"Disc game inside `{items["hdd_game_path"]}`");
if (state.Error == LogParseState.ErrorCode.SizeLimit)
notes += "Log was too large, showing last processed run";
notes.AppendLine("Log was too large, showing last processed run");
// should be last check here
var updateInfo = await CheckForUpdateAsync(items).ConfigureAwait(false);
if (updateInfo != null)
notes += $"{Environment.NewLine}Outdated RPCS3 build detected, consider updating";
if (notes != null)
builder.AddField("Notes", notes);
notes.AppendLine("Outdated RPCS3 build detected, consider updating");
var notesContent = notes.ToString().Trim();
if (!string.IsNullOrEmpty(notesContent))
builder.AddField("Notes", notesContent);
if (updateInfo != null)
await updateInfo.AsEmbedAsync(builder).ConfigureAwait(false);

View File

@ -4,7 +4,9 @@ using System.Globalization;
using CompatApiClient;
using CompatApiClient.Utils;
using CompatApiClient.POCOs;
using CompatBot.Database.Providers;
using DSharpPlus.Entities;
using Microsoft.EntityFrameworkCore.Sqlite.Query.ExpressionTranslators.Internal;
namespace CompatBot.Utils.ResultFormatters
{
@ -75,7 +77,15 @@ namespace CompatBot.Utils.ResultFormatters
var desc = string.IsNullOrEmpty(titleId)
? "No product id was found; log might be corrupted, please reupload a new copy"
: $"Product code {titleId} was not found in compatibility database, possibly untested!";
return new DiscordEmbedBuilder{Description = desc, Color = Config.Colors.CompatStatusUnknown};
var result = new DiscordEmbedBuilder
{
Description = desc,
Color = Config.Colors.CompatStatusUnknown,
ThumbnailUrl = thumbnailUrl,
};
if (ThumbnailProvider.GetTitleName(titleId) is string titleName && !string.IsNullOrEmpty(titleName))
result.Title = $"[{titleId}] {titleName.Sanitize().Trim(200)}";
return result.Build();
}
}

View File

@ -0,0 +1,55 @@
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using CompatBot.Database.Providers;
using DSharpPlus;
using DSharpPlus.Entities;
using PsnClient.POCOs;
namespace CompatBot.Utils.ResultFormatters
{
internal static class TitlePatchFormatter
{
private const long UnderKB = 1000;
private const long UnderMB = 1000 * 1024;
private const long UnderGB = 1000 * 1024 * 1024;
public static async Task<DiscordEmbed> AsEmbedAsync(this TitlePatch patch, DiscordClient client)
{
var pkgs = patch.Tag?.Packages;
var title = pkgs?.Select(p => p.ParamSfo?.Title).LastOrDefault(t => !string.IsNullOrEmpty(t)) ?? patch.TitleId;
var thumbnailUrl = await client.GetThumbnailUrlAsync(patch.TitleId).ConfigureAwait(false);
var result = new DiscordEmbedBuilder
{
Title = title,
Color = Config.Colors.DownloadLinks,
ThumbnailUrl = thumbnailUrl,
};
if (pkgs.Length > 1)
{
result.Description = $"Total download size of all packages is {pkgs.Sum(p => p.Size).AsStorageUnit()}";
foreach (var pkg in pkgs)
result.AddField($"Update v{pkg.Version} ({pkg.Size.AsStorageUnit()})", $"⏬ [{Path.GetFileName(pkg.Url)}]({pkg.Url})");
}
else
{
result.Title = $"{title} update v{pkgs[0].Version} ({pkgs[0].Size.AsStorageUnit()})";
result.Description = $"⏬ [{Path.GetFileName(pkgs[0].Url)}]({pkgs[0].Url})";
}
return result.Build();
}
private static string AsStorageUnit(this long bytes)
{
if (bytes < UnderKB)
return $"{bytes} byte{(bytes % 10 == 1 && bytes % 100 != 11 ? "" : "s")}";
if (bytes < UnderMB)
return $"{bytes / 1024.0:0.##} KB";
if (bytes < UnderGB)
return $"{bytes / 1024.0 / 1024:0.##} MB";
return $"{bytes / 1024.0 / 1024 / 1024:0.##} GB";
}
}
}

View File

@ -0,0 +1,27 @@
using System;
using System.Net.Http;
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;
namespace PsnClient
{
public class CustomTlsCertificatesHandler: HttpClientHandler
{
private readonly Func<HttpRequestMessage, X509Certificate2, X509Chain, SslPolicyErrors, bool> defaultCertHandler;
public CustomTlsCertificatesHandler()
{
defaultCertHandler = ServerCertificateCustomValidationCallback;
ServerCertificateCustomValidationCallback = IgnoreSonyRootCertificates;
}
private bool IgnoreSonyRootCertificates(HttpRequestMessage requestMessage, X509Certificate2 certificate, X509Chain chain, SslPolicyErrors policyErrors)
{
//todo: do proper checks with root certs from ps3 fw
if (certificate.IssuerName.Name?.StartsWith("SCEI DNAS Root 0") ?? false)
return true;
return defaultCertHandler?.Invoke(requestMessage, certificate, chain, policyErrors) ?? true;
}
}
}

View File

@ -0,0 +1,46 @@
using System.Xml.Serialization;
namespace PsnClient.POCOs
{
[XmlRoot("titlepatch")]
public class TitlePatch
{
[XmlAttribute("titleid")]
public string TitleId { get; set; }
[XmlAttribute("status")]
public string Status { get; set; }
[XmlElement("tag")]
public TitlePatchTag Tag { get; set; }
}
public class TitlePatchTag
{
[XmlAttribute("name")]
public string Name { get; set; }
//no root element
[XmlElement("package")]
public TitlePatchPackage[] Packages { get; set; }
}
public class TitlePatchPackage
{
[XmlAttribute("version")]
public string Version { get; set; }
[XmlAttribute("size")]
public long Size { get; set; }
[XmlAttribute("sha1sum")]
public string Sha1Sum { get; set; }
[XmlAttribute("url")]
public string Url { get; set; }
[XmlAttribute("ps3_system_ver")]
public string Ps3SystemVer { get; set; }
[XmlElement("paramsfo")]
public TitlePatchParamSfo ParamSfo { get; set; }
}
public class TitlePatchParamSfo
{
[XmlElement("TITLE")]
public string Title { get; set; }
}
}

View File

@ -22,11 +22,13 @@ namespace PsnClient
private readonly HttpClient client;
private readonly MediaTypeFormatterCollection dashedFormatters;
private readonly MediaTypeFormatterCollection underscoreFormatters;
private readonly MediaTypeFormatterCollection xmlFormatters;
private static readonly Regex ContainerIdLink = new Regex(@"(?<id>STORE-(\w|\d)+-(\w|\d)+)");
public Client()
{
client = HttpClientFactory.Create(new CompressionMessageHandler());
client = HttpClientFactory.Create(new CustomTlsCertificatesHandler(), new CompressionMessageHandler());
var dashedSettings = new JsonSerializerSettings
{
ContractResolver = new JsonContractResolver(NamingStyles.Dashed),
@ -40,15 +42,15 @@ namespace PsnClient
NullValueHandling = NullValueHandling.Ignore
};
underscoreFormatters = new MediaTypeFormatterCollection(new[] { new JsonMediaTypeFormatter { SerializerSettings = underscoreSettings } });
xmlFormatters = new MediaTypeFormatterCollection(new[] {new XmlMediaTypeFormatter {UseXmlSerializer = true}});
}
public async Task<AppLocales> GetLocales(CancellationToken cancellationToken)
{
try
{
HttpResponseMessage response;
using (var message = new HttpRequestMessage(HttpMethod.Get, "https://transact.playstation.com/assets/app.json"))
using (response = await client.SendAsync(message, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false))
using (var response = await client.SendAsync(message, HttpCompletionOption.ResponseContentRead, cancellationToken).ConfigureAwait(false))
try
{
await response.Content.LoadIntoBufferAsync().ConfigureAwait(false);
@ -76,8 +78,7 @@ namespace PsnClient
using (var getMessage = new HttpRequestMessage(HttpMethod.Get, "https://store.playstation.com/kamaji/api/valkyrie_storefront/00_09_000/user/stores"))
{
getMessage.Headers.Add("Cookie", cookieHeaderValue);
HttpResponseMessage response;
using (response = await client.SendAsync(getMessage, cancellationToken).ConfigureAwait(false))
using (var response = await client.SendAsync(getMessage, cancellationToken).ConfigureAwait(false))
try
{
await response.Content.LoadIntoBufferAsync().ConfigureAwait(false);
@ -157,9 +158,8 @@ namespace PsnClient
{
var loc = locale.AsLocaleData();
var baseUrl = $"https://store.playstation.com/valkyrie-api/{loc.language}/{loc.country}/999/storefront/{containerId}";
HttpResponseMessage response;
using (var message = new HttpRequestMessage(HttpMethod.Get, baseUrl))
using (response = await client.SendAsync(message, cancellationToken).ConfigureAwait(false))
using (var response = await client.SendAsync(message, cancellationToken).ConfigureAwait(false))
try
{
if (response.StatusCode == HttpStatusCode.NotFound)
@ -192,9 +192,8 @@ namespace PsnClient
filters["size"] = take.ToString();
filters["bucket"] = "games";
url = url.SetQueryParameters(filters);
HttpResponseMessage response;
using (var message = new HttpRequestMessage(HttpMethod.Get, url))
using (response = await client.SendAsync(message, cancellationToken).ConfigureAwait(false))
using (var response = await client.SendAsync(message, cancellationToken).ConfigureAwait(false))
try
{
if (response.StatusCode == HttpStatusCode.NotFound)
@ -221,9 +220,8 @@ namespace PsnClient
try
{
var loc = locale.AsLocaleData();
HttpResponseMessage response;
using (var message = new HttpRequestMessage(HttpMethod.Get, $"https://store.playstation.com/valkyrie-api/{loc.language}/{loc.country}/999/resolve/{contentId}?depth={depth}"))
using (response = await client.SendAsync(message, cancellationToken).ConfigureAwait(false))
using (var response = await client.SendAsync(message, cancellationToken).ConfigureAwait(false))
try
{
if (response.StatusCode == HttpStatusCode.NotFound)
@ -245,6 +243,33 @@ namespace PsnClient
}
}
public async Task<TitlePatch> GetTitleUpdatesAsync(string productId, CancellationToken cancellationToken)
{
try
{
using (var message = new HttpRequestMessage(HttpMethod.Get, $"https://a0.ww.np.dl.playstation.net/tpl/np/{productId}/{productId}-ver.xml"))
using (var response = await client.SendAsync(message, cancellationToken).ConfigureAwait(false))
try
{
if (response.StatusCode == HttpStatusCode.NotFound)
return null;
await response.Content.LoadIntoBufferAsync().ConfigureAwait(false);
return await response.Content.ReadAsAsync<TitlePatch>(xmlFormatters, cancellationToken).ConfigureAwait(false);
}
catch (Exception e)
{
ConsoleLogger.PrintError(e, response);
return null;
}
}
catch (Exception e)
{
ConsoleLogger.PrintError(e, null);
return null;
}
}
private async Task<string> GetSessionCookies(string locale, CancellationToken cancellationToken)
{
var loc = locale.AsLocaleData();

View File

@ -1,5 +1,5 @@
RPCS3 Compatibility Bot reimplemented in C# for .NET Core
=========================================================
RPCS3 Compatibility Bot
=======================
Development Requirements
------------------------