mirror of
https://github.com/RPCS3/discord-bot.git
synced 2026-01-31 01:25:22 +01:00
implemented some rudimentary psn api client
implemented psn crawling for ps3 game metadata (mostly for thumbnails) implemented game thumbnails for game embeds fixed usage of dbcontext some other bugfixes
This commit is contained in:
committed by
Roberto Anić Banić
parent
24040de62c
commit
fbad33ea13
1
.gitignore
vendored
1
.gitignore
vendored
@@ -262,3 +262,4 @@ __pycache__/
|
||||
launchSettings.json
|
||||
bot.db
|
||||
.vscode/
|
||||
thumbs.db
|
||||
|
||||
@@ -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<CompatResult> 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<CompatResult>(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<CompatResult>(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<UpdateInfo> 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<UpdateInfo>(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<UpdateInfo>(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<PrInfo> 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<PrInfo>(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<PrInfo>(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
using System;
|
||||
|
||||
namespace CompatApiClient.POCOs
|
||||
namespace CompatApiClient.POCOs
|
||||
{
|
||||
public class UpdateInfo
|
||||
{
|
||||
|
||||
28
CompatApiClient/Utils/ConsoleLogger.cs
Normal file
28
CompatApiClient/Utils/ConsoleLogger.cs
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
using System;
|
||||
|
||||
namespace CompatApiClient
|
||||
namespace CompatApiClient.Utils
|
||||
{
|
||||
public static class Utils
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
22
CompatBot/Commands/Attributes/LimitedToHelpChannel.cs
Normal file
22
CompatBot/Commands/Attributes/LimitedToHelpChannel.cs
Normal file
@@ -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<bool> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
22
CompatBot/Commands/Attributes/LimitedToSpamChannel.cs
Normal file
22
CompatBot/Commands/Attributes/LimitedToSpamChannel.cs
Normal file
@@ -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<bool> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<char, string[]> 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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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<int> rolls;
|
||||
lock (rng) rolls = Enumerable.Range(0, num).Select(_ => rng.Next(face) + 1).ToList();
|
||||
if (rolls.Count > 1)
|
||||
{
|
||||
List<int> 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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DSharpPlus" Version="4.0.0-beta-00470" />
|
||||
<PackageReference Include="DSharpPlus.CommandsNext" Version="4.0.0-beta-00470" />
|
||||
<PackageReference Include="MathParser.org-mXparser" Version="4.2.0" />
|
||||
<PackageReference Include="DSharpPlus" Version="4.0.0-beta-00491" />
|
||||
<PackageReference Include="DSharpPlus.CommandsNext" Version="4.0.0-beta-00491" />
|
||||
<PackageReference Include="MathParser.org-mXparser" Version="4.2.1" />
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.6" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="2.1.1" />
|
||||
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="2.1.1" />
|
||||
@@ -28,6 +28,7 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CompatApiClient\CompatApiClient.csproj" />
|
||||
<ProjectReference Include="..\PsnClient\PsnClient.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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<BotDb> instance = new Lazy<BotDb>(() => new BotDb());
|
||||
public static BotDb Instance => instance.Value;
|
||||
|
||||
public DbSet<Moderator> Moderator { get; set; }
|
||||
public DbSet<Piracystring> Piracystring { get; set; }
|
||||
public DbSet<Warning> Warning { get; set; }
|
||||
|
||||
@@ -10,11 +10,11 @@ namespace CompatBot.Database
|
||||
{
|
||||
internal static class DbImporter
|
||||
{
|
||||
public static async Task<bool> UpgradeAsync(BotDb dbContext, CancellationToken cancellationToken)
|
||||
public static async Task<bool> 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.");
|
||||
|
||||
82
CompatBot/Database/Migrations/ThumbnailDb/20180801095653_InitialCreate.Designer.cs
generated
Normal file
82
CompatBot/Database/Migrations/ThumbnailDb/20180801095653_InitialCreate.Designer.cs
generated
Normal file
@@ -0,0 +1,82 @@
|
||||
// <auto-generated />
|
||||
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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Locale")
|
||||
.HasColumnName("locale");
|
||||
|
||||
b.Property<long>("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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ContentId")
|
||||
.HasColumnName("content_id");
|
||||
|
||||
b.Property<string>("EmbeddableUrl")
|
||||
.HasColumnName("embeddable_url");
|
||||
|
||||
b.Property<string>("ProductCode")
|
||||
.IsRequired()
|
||||
.HasColumnName("product_code");
|
||||
|
||||
b.Property<long>("Timestamp")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.Property<string>("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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<int>(nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
locale = table.Column<string>(nullable: true),
|
||||
timestamp = table.Column<long>(nullable: false)
|
||||
},
|
||||
constraints: table =>
|
||||
{
|
||||
table.PrimaryKey("id", x => x.id);
|
||||
});
|
||||
|
||||
migrationBuilder.CreateTable(
|
||||
name: "thumbnail",
|
||||
columns: table => new
|
||||
{
|
||||
id = table.Column<int>(nullable: false)
|
||||
.Annotation("Sqlite:Autoincrement", true),
|
||||
product_code = table.Column<string>(nullable: false),
|
||||
content_id = table.Column<string>(nullable: true),
|
||||
url = table.Column<string>(nullable: true),
|
||||
embeddable_url = table.Column<string>(nullable: true),
|
||||
timestamp = table.Column<long>(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");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,80 @@
|
||||
// <auto-generated />
|
||||
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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("Locale")
|
||||
.HasColumnName("locale");
|
||||
|
||||
b.Property<long>("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<int>("Id")
|
||||
.ValueGeneratedOnAdd()
|
||||
.HasColumnName("id");
|
||||
|
||||
b.Property<string>("ContentId")
|
||||
.HasColumnName("content_id");
|
||||
|
||||
b.Property<string>("EmbeddableUrl")
|
||||
.HasColumnName("embeddable_url");
|
||||
|
||||
b.Property<string>("ProductCode")
|
||||
.IsRequired()
|
||||
.HasColumnName("product_code");
|
||||
|
||||
b.Property<long>("Timestamp")
|
||||
.HasColumnName("timestamp");
|
||||
|
||||
b.Property<string>("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
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,11 +8,12 @@ namespace CompatBot.Database.Providers
|
||||
internal static class ModProvider
|
||||
{
|
||||
private static readonly Dictionary<ulong, Moderator> mods;
|
||||
private static readonly BotDb db = new BotDb();
|
||||
public static ReadOnlyDictionary<ulong, Moderator> Mods => new ReadOnlyDictionary<ulong, Moderator>(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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string> PiracyStrings;
|
||||
private static AhoCorasickDoubleArrayTrie<string> 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<bool> RemoveAsync(int id)
|
||||
{
|
||||
var db = BotDb.Instance;
|
||||
var dbItem = await db.Piracystring.FirstOrDefaultAsync(ps => ps.Id == id).ConfigureAwait(false);
|
||||
if (dbItem == null)
|
||||
return false;
|
||||
|
||||
73
CompatBot/Database/Providers/ScrapeStateProvider.cs
Normal file
73
CompatBot/Database/Providers/ScrapeStateProvider.cs
Normal file
@@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
53
CompatBot/Database/Providers/ThumbnailProvider.cs
Normal file
53
CompatBot/Database/Providers/ThumbnailProvider.cs
Normal file
@@ -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<string> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
50
CompatBot/Database/ThumbnailDb.cs
Normal file
50
CompatBot/Database/ThumbnailDb.cs
Normal file
@@ -0,0 +1,50 @@
|
||||
using System.ComponentModel.DataAnnotations;
|
||||
using CompatApiClient;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace CompatBot.Database
|
||||
{
|
||||
internal class ThumbnailDb: DbContext
|
||||
{
|
||||
public DbSet<State> State { get; set; }
|
||||
public DbSet<Thumbnail> 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<State>().HasIndex(s => s.Locale).IsUnique().HasName("state_locale");
|
||||
modelBuilder.Entity<State>().HasIndex(s => s.Timestamp).HasName("state_timestamp");
|
||||
modelBuilder.Entity<Thumbnail>().HasIndex(m => m.ProductCode).IsUnique().HasName("thumbnail_product_code");
|
||||
modelBuilder.Entity<Thumbnail>().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; }
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<LogSection> LogSections = new List<LogSection>
|
||||
@@ -105,7 +105,7 @@ namespace CompatBot.EventHandlers.LogParsing
|
||||
["GL RENDERER:"] = new Regex(@"GL RENDERER: (?<driver_manuf_new>.*?)\r?\n", DefaultOptions),
|
||||
["GL VERSION:"] = new Regex(@"GL VERSION:(\d|\.|\s|\w|-)* (?<driver_version_new>(\d+\.)*\d+)\r?\n", DefaultOptions),
|
||||
["texel buffer size reported:"] = new Regex(@"RSX: Supported texel buffer size reported: (?<texel_buffer_size_new>\d*?) bytes", DefaultOptions),
|
||||
["·F "] = new Regex(@"F \d+:\d+:\d+\.\d+ {.+?} (?<fatal_error>.*?(\:\W*\r?\n\(.*?)*)\r?$", DefaultOptions),
|
||||
["F "] = new Regex(@"F \d+:\d+:\d+\.\d+ {.+?} (?<fatal_error>.*?(\:\W*\r?\n\(.*?)*)\r?$", DefaultOptions),
|
||||
["Failed to load RAP file:"] = new Regex(@"Failed to load RAP file: (?<rap_file>.*?)\r?$", DefaultOptions),
|
||||
["Rap file not found:"] = new Regex(@"Rap file not found: (?<rap_file>.*?)\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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<LogSectionParser> 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<Action<string, LogParseState>>(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();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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(@"(?<letters>(?:[BPSUVX][CL]|P[ETU]|NP)[AEHJKPUIX][ABSM])[ \-]?(?<numbers>\d{5})", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
public static readonly Regex ProductCode = new Regex(@"(?<letters>(?:[BPSUVX][CL]|P[ETU]|NP)[AEHJKPUIX][ABSM])[ \-]?(?<numbers>\d{5})", RegexOptions.Compiled | RegexOptions.IgnoreCase);
|
||||
private static readonly Client compatClient = new Client();
|
||||
private static readonly AhoCorasickDoubleArrayTrie<string> ChillCheck = new AhoCorasickDoubleArrayTrie<string>(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);
|
||||
}
|
||||
|
||||
@@ -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<Misc>();
|
||||
commands.RegisterCommands<CompatList>();
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
329
CompatBot/ThumbScrapper/PsnScraper.cs
Normal file
329
CompatBot/ThumbScrapper/PsnScraper.cs
Normal file
@@ -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(@"(?<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);
|
||||
|
||||
public async Task Run(CancellationToken cancellationToken)
|
||||
{
|
||||
do
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
await ScrapeStateProvider.CleanAsync(cancellationToken).ConfigureAwait(false);
|
||||
|
||||
try
|
||||
{
|
||||
await DoScrapePassAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
PrintError(e);
|
||||
}
|
||||
await Task.Delay(TimeSpan.FromHours(1), cancellationToken).ConfigureAwait(false);
|
||||
} while (!cancellationToken.IsCancellationRequested);
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
break;
|
||||
|
||||
if (ScrapeStateProvider.IsFresh(locale))
|
||||
{
|
||||
Console.WriteLine($"Cache for {locale} PSN is fresh, skipping");
|
||||
continue;
|
||||
}
|
||||
|
||||
Console.WriteLine($"Scraping {locale} PSN for PS3 games...");
|
||||
var knownContainers = new HashSet<string>();
|
||||
// get containers from the left side navigation panel on the main page
|
||||
var containerIds = await Client.GetMainPageNavigationContainerIdsAsync(locale, cancellationToken).ConfigureAwait(false);
|
||||
// get all containers from all the menus
|
||||
var stores = await Client.GetStoresAsync(locale, cancellationToken).ConfigureAwait(false);
|
||||
if (!string.IsNullOrEmpty(stores?.Data.BaseUrl))
|
||||
containerIds.Add(Path.GetFileName(stores.Data.BaseUrl));
|
||||
foreach (var id in containerIds)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
await ScrapeContainerIdsAsync(locale, id, knownContainers, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
Console.WriteLine($"\tFound {knownContainers.Count} containers");
|
||||
|
||||
// now let's scrape for actual games in every container
|
||||
var defaultFilters = new Dictionary<string, string>
|
||||
{
|
||||
["platform"] = "ps3",
|
||||
["game_content_type"] = "games",
|
||||
};
|
||||
var take = 30;
|
||||
var returned = 0;
|
||||
foreach (var containerId in knownContainers)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
if (ScrapeStateProvider.IsFresh(locale, containerId))
|
||||
{
|
||||
Console.WriteLine($"\tCache for {locale} container {containerId} is fresh, skipping");
|
||||
continue;
|
||||
}
|
||||
|
||||
Console.WriteLine($"\tScraping {locale} container {containerId}...");
|
||||
var total = -1;
|
||||
var start = 0;
|
||||
do
|
||||
{
|
||||
var tries = 0;
|
||||
Container container = null;
|
||||
bool error = false;
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
container = await Client.GetGameContainerAsync(locale, containerId, start, take, defaultFilters, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
PrintError(e);
|
||||
error = true;
|
||||
}
|
||||
tries++;
|
||||
} while (error && tries < 3 && !cancellationToken.IsCancellationRequested);
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
if (container != null)
|
||||
{
|
||||
// this might've changed between the pages for some stupid reason
|
||||
total = container.Data.Attributes.TotalResults;
|
||||
var pages = (int)Math.Ceiling((double)total / take);
|
||||
if (pages > 1)
|
||||
Console.WriteLine($"\t\tPage {start / take + 1} of {pages}");
|
||||
returned = container.Data?.Relationships?.Children?.Data?.Count(i => i.Type == "game" || i.Type == "legacy-sku") ?? 0;
|
||||
// included contains full data already, so it's wise to get it first
|
||||
await ProcessIncludedGamesAsync(locale, container, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
// returned items are just ids that need to be resolved
|
||||
if (returned > 0)
|
||||
{
|
||||
foreach (var item in container.Data.Relationships.Children.Data)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
if (item.Type == "game")
|
||||
{
|
||||
if (!NeedLookup(item.Id))
|
||||
continue;
|
||||
}
|
||||
else if (item.Type != "legacy-sku")
|
||||
continue;
|
||||
|
||||
//need depth=1 in case it's a crossplay title, so ps3 id will be in entitlements instead
|
||||
container = await Client.ResolveContentAsync(locale, item.Id, 1, cancellationToken).ConfigureAwait(false);
|
||||
if (container == null)
|
||||
PrintError(new InvalidOperationException("No container for " + item.Id));
|
||||
else
|
||||
await ProcessIncludedGamesAsync(locale, container, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
start += take;
|
||||
} 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");
|
||||
}
|
||||
await ScrapeStateProvider.SetLastRunTimestampAsync(locale).ConfigureAwait(false);
|
||||
}
|
||||
Console.WriteLine("Finished scraping all the PSN stores");
|
||||
}
|
||||
|
||||
private static List<string> GetLocalesInPreferredOrder(string[] locales)
|
||||
{
|
||||
/*
|
||||
* what we want here: only one language per country
|
||||
* prefer en, then ja language for the region if it has it
|
||||
* then order by language, so we get as much English titles as possible
|
||||
* then Japanese
|
||||
* then the rest
|
||||
* withing one language prefer US, then GB, then JP to cover the largest ones first
|
||||
*/
|
||||
var en = new List<string>();
|
||||
var ja = new List<string>();
|
||||
foreach (var l in locales)
|
||||
{
|
||||
if (l.StartsWith("en"))
|
||||
en.Add(l);
|
||||
else if (l.StartsWith("ja"))
|
||||
ja.Add(l);
|
||||
}
|
||||
var orderedLocales = new[] {"en-US", "en-GB"}
|
||||
.Concat(en)
|
||||
.Concat(new[] {"ja-JP"})
|
||||
.Concat(ja)
|
||||
.Concat(locales);
|
||||
var countries = new HashSet<string>();
|
||||
var result = new List<string>(locales.Length);
|
||||
foreach (var locale in orderedLocales)
|
||||
if (countries.Add(locale.AsLocaleData().country))
|
||||
result.Add(locale);
|
||||
Console.WriteLine($"Selected stores ({result.Count}): " + string.Join(' ', result));
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool NeedLookup(string contentId)
|
||||
{
|
||||
using (var db = new ThumbnailDb())
|
||||
if (db.Thumbnail.FirstOrDefault(t => t.ContentId == contentId) is Thumbnail thumbnail)
|
||||
if (!string.IsNullOrEmpty(thumbnail.Url))
|
||||
if (ScrapeStateProvider.IsFresh(new DateTime(thumbnail.Timestamp, DateTimeKind.Utc)))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static async Task ProcessIncludedGamesAsync(string locale, Container container, CancellationToken cancellationToken, bool resolveCrossplay = true)
|
||||
{
|
||||
if (container.Included?.Length > 0)
|
||||
foreach (var item in container.Included)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
switch (item.Type)
|
||||
{
|
||||
case "game":
|
||||
if (string.IsNullOrEmpty(item.Id))
|
||||
continue;
|
||||
|
||||
await AddOrUpdateThumbnailAsync(item.Id, item.Attributes?.ThumbnailUrlBase, cancellationToken).ConfigureAwait(false);
|
||||
break;
|
||||
|
||||
case "legacy-sku":
|
||||
if (!resolveCrossplay)
|
||||
continue;
|
||||
|
||||
var relatedSkus = (item.Attributes?.Eligibilities ?? Enumerable.Empty<GameSkuRelation>())
|
||||
.Concat(item.Attributes?.Entitlements ?? Enumerable.Empty<GameSkuRelation>())
|
||||
.Select(sku => sku.Id)
|
||||
.Distinct()
|
||||
.Where(id => ProductCodeLookup.ProductCode.IsMatch(id) && NeedLookup(id))
|
||||
.ToList();
|
||||
foreach (var relatedSku in relatedSkus)
|
||||
{
|
||||
var relatedContainer = await Client.ResolveContentAsync(locale, relatedSku, 1, cancellationToken).ConfigureAwait(false);
|
||||
if (relatedContainer != null)
|
||||
await ProcessIncludedGamesAsync(locale, relatedContainer, cancellationToken, false).ConfigureAwait(false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task AddOrUpdateThumbnailAsync(string contentId, string url, CancellationToken cancellationToken)
|
||||
{
|
||||
var match = ContentIdMatcher.Match(contentId);
|
||||
if (!match.Success)
|
||||
return;
|
||||
|
||||
var productCode = match.Groups["product_id"].Value;
|
||||
if (!ProductCodeLookup.ProductCode.IsMatch(productCode))
|
||||
return;
|
||||
|
||||
using (var db = new ThumbnailDb())
|
||||
{
|
||||
var savedItem = db.Thumbnail.FirstOrDefault(t => t.ProductCode == productCode);
|
||||
if (savedItem == null)
|
||||
{
|
||||
var newItem = new Thumbnail
|
||||
{
|
||||
ProductCode = productCode,
|
||||
ContentId = contentId,
|
||||
Url = url,
|
||||
Timestamp = DateTime.UtcNow.Ticks,
|
||||
};
|
||||
db.Thumbnail.Add(newItem);
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(url))
|
||||
{
|
||||
if (!ScrapeStateProvider.IsFresh(savedItem.Timestamp) && savedItem.Url != url)
|
||||
{
|
||||
savedItem.Url = url;
|
||||
savedItem.EmbeddableUrl = null;
|
||||
}
|
||||
savedItem.ContentId = contentId;
|
||||
savedItem.Timestamp = DateTime.UtcNow.Ticks;
|
||||
}
|
||||
await db.SaveChangesAsync(cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static async Task ScrapeContainerIdsAsync(string locale, string containerId, HashSet<string> knownContainerIds, CancellationToken cancellationToken)
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
if (string.IsNullOrEmpty(containerId))
|
||||
return;
|
||||
|
||||
if (!knownContainerIds.Add(containerId))
|
||||
return;
|
||||
|
||||
var navigation = await Client.GetStoreNavigationAsync(locale, containerId, cancellationToken).ConfigureAwait(false);
|
||||
if (navigation?.Data?.Attributes?.Navigation is StoreNavigationNavigation[] navs)
|
||||
{
|
||||
foreach (var nav in navs)
|
||||
{
|
||||
await ScrapeContainerIdsAsync(locale, nav.Id, knownContainerIds, cancellationToken).ConfigureAwait(false);
|
||||
if (nav.Submenu is StoreNavigationSubmenu[] submenus)
|
||||
foreach (var submenu in submenus)
|
||||
if (submenu.Items is StoreNavigationSubmenuItem[] items)
|
||||
foreach (var item in items)
|
||||
if (!item.IsSeparator && !string.IsNullOrEmpty(item.TargetContainerId))
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
await ScrapeContainerIdsAsync(locale, item.TargetContainerId, knownContainerIds, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (navigation?.Data?.Relationships?.Children?.Data is RelationshipsChildrenItem[] childItems)
|
||||
foreach (var item in childItems.Where(i => i.Type == "container"))
|
||||
{
|
||||
if (cancellationToken.IsCancellationRequested)
|
||||
return;
|
||||
|
||||
await ScrapeContainerIdsAsync(locale, item.Id, knownContainerIds, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
private static void PrintError(Exception e)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine("Error scraping thumbnails: " + e);
|
||||
Console.ResetColor();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,7 @@
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using CompatApiClient;
|
||||
using CompatApiClient.Utils;
|
||||
using DSharpPlus.CommandsNext;
|
||||
using DSharpPlus.Entities;
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using CompatApiClient;
|
||||
using CompatApiClient.Utils;
|
||||
using DSharpPlus;
|
||||
using DSharpPlus.CommandsNext;
|
||||
using DSharpPlus.Entities;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using CompatApiClient;
|
||||
using CompatApiClient.Utils;
|
||||
using DSharpPlus.Entities;
|
||||
|
||||
namespace CompatBot.Utils
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using CompatApiClient;
|
||||
using CompatApiClient.POCOs;
|
||||
using CompatApiClient.Utils;
|
||||
using CompatBot.EventHandlers;
|
||||
using CompatBot.EventHandlers.LogParsing.POCOs;
|
||||
using DSharpPlus;
|
||||
@@ -37,7 +38,7 @@ namespace CompatBot.Utils.ResultFormatters
|
||||
if (collection?.Count > 0)
|
||||
{
|
||||
var gameInfo = await client.LookupGameInfoAsync(collection["serial"], false).ConfigureAwait(false);
|
||||
builder = new DiscordEmbedBuilder(gameInfo);
|
||||
builder = new DiscordEmbedBuilder(gameInfo) {ThumbnailUrl = null}; // or this will fuck up all formatting
|
||||
if (state.Error == LogParseState.ErrorCode.PiracyDetected)
|
||||
{
|
||||
state.PiracyContext = state.PiracyContext.Sanitize();
|
||||
@@ -225,7 +226,7 @@ namespace CompatBot.Utils.ResultFormatters
|
||||
return null;
|
||||
|
||||
var updateInfo = await compatClient.GetUpdateAsync(Config.Cts.Token).ConfigureAwait(false);
|
||||
var link = updateInfo.LatestBuild?.Windows?.Download ?? updateInfo.LatestBuild?.Linux?.Download;
|
||||
var link = updateInfo?.LatestBuild?.Windows?.Download ?? updateInfo?.LatestBuild?.Linux?.Download;
|
||||
if (string.IsNullOrEmpty(link))
|
||||
return null;
|
||||
|
||||
@@ -236,16 +237,16 @@ namespace CompatBot.Utils.ResultFormatters
|
||||
return updateInfo;
|
||||
}
|
||||
|
||||
private static bool SameCommits(string commitA, string CommitB)
|
||||
private static bool SameCommits(string commitA, string commitB)
|
||||
{
|
||||
if (string.IsNullOrEmpty(commitA) && string.IsNullOrEmpty(CommitB))
|
||||
if (string.IsNullOrEmpty(commitA) && string.IsNullOrEmpty(commitB))
|
||||
return true;
|
||||
|
||||
if (string.IsNullOrEmpty(commitA) || string.IsNullOrEmpty(CommitB))
|
||||
if (string.IsNullOrEmpty(commitA) || string.IsNullOrEmpty(commitB))
|
||||
return false;
|
||||
|
||||
var len = Math.Min(commitA.Length, CommitB.Length);
|
||||
return commitA.Substring(0, len) == CommitB.Substring(0, len);
|
||||
var len = Math.Min(commitA.Length, commitB.Length);
|
||||
return commitA.Substring(0, len) == commitB.Substring(0, len);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using CompatApiClient;
|
||||
using CompatApiClient.Utils;
|
||||
using CompatApiClient.POCOs;
|
||||
using DSharpPlus.Entities;
|
||||
|
||||
@@ -45,7 +46,7 @@ namespace CompatBot.Utils.ResultFormatters
|
||||
return $"Product code {titleId} was not found in compatibility database, possibly untested!";
|
||||
}
|
||||
|
||||
public static DiscordEmbed AsEmbed(this TitleInfo info, string titleId, bool footer = true)
|
||||
public static DiscordEmbed AsEmbed(this TitleInfo info, string titleId, bool footer = true, string thumbnailUrl = null)
|
||||
{
|
||||
if (info.Status == TitleInfo.Maintenance.Status)
|
||||
return new DiscordEmbedBuilder{Description = "API is undergoing maintenance, please try again later.", Color = Config.Colors.Maintenance}.Build();
|
||||
@@ -55,6 +56,7 @@ namespace CompatBot.Utils.ResultFormatters
|
||||
|
||||
if (StatusColors.TryGetValue(info.Status, out var color))
|
||||
{
|
||||
footer = footer && string.IsNullOrEmpty(thumbnailUrl);
|
||||
// apparently there's no formatting in the footer, but you need to escape everything in description; ugh
|
||||
var pr = info.ToPrString(footer ? @"¯\_(ツ)_ /¯" : @"¯\\\_(ツ)\_ /¯");
|
||||
var desc = $"Status: {info.Status}, PR: {pr}, Updated: {info.ToUpdated()}";
|
||||
@@ -64,6 +66,7 @@ namespace CompatBot.Utils.ResultFormatters
|
||||
Url = $"https://forums.rpcs3.net/thread-{info.Thread}.html",
|
||||
Description = footer ? null : desc,
|
||||
Color = color,
|
||||
ThumbnailUrl = thumbnailUrl
|
||||
}.WithFooter(footer ? desc : null)
|
||||
.Build();
|
||||
}
|
||||
|
||||
@@ -13,8 +13,11 @@ namespace CompatBot.Utils.ResultFormatters
|
||||
|
||||
public static async Task<DiscordEmbedBuilder> AsEmbedAsync(this UpdateInfo info, DiscordEmbedBuilder builder = null)
|
||||
{
|
||||
if (info == null)
|
||||
return builder ?? new DiscordEmbedBuilder {Title = "Error", Description = "Error communicating with the update API. Try again later.", Color = Config.Colors.Maintenance};
|
||||
|
||||
var justAppend = builder != null;
|
||||
var build = info?.LatestBuild;
|
||||
var build = info.LatestBuild;
|
||||
var pr = build?.Pr ?? "0";
|
||||
string url = null;
|
||||
PrInfo prInfo = null;
|
||||
@@ -27,7 +30,7 @@ namespace CompatBot.Utils.ResultFormatters
|
||||
{
|
||||
url = "https://github.com/RPCS3/rpcs3/pull/" + pr;
|
||||
prInfo = await client.GetPrInfoAsync(pr, Config.Cts.Token).ConfigureAwait(false);
|
||||
pr = $"PR #{pr} by {prInfo?.User?.login ?? "???"}";
|
||||
pr = $"PR #{pr} by {prInfo?.User?.Login ?? "???"}";
|
||||
}
|
||||
}
|
||||
builder = builder ?? new DiscordEmbedBuilder {Title = pr, Url = url, Description = prInfo?.Title, Color = Config.Colors.DownloadLinks};
|
||||
@@ -41,7 +44,7 @@ namespace CompatBot.Utils.ResultFormatters
|
||||
if (string.IsNullOrEmpty(link))
|
||||
return "No link available";
|
||||
|
||||
var text = new Uri(link).Segments?.Last();
|
||||
var text = new Uri(link).Segments?.Last() ?? "";
|
||||
if (simpleName && text.Contains('_'))
|
||||
text = text.Split('_', 2)[0];
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using DSharpPlus.Entities;
|
||||
|
||||
|
||||
@@ -1,11 +1,18 @@
|
||||
using System;
|
||||
using System.Buffers;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace CompatBot.Utils
|
||||
{
|
||||
internal static class StringUtils
|
||||
{
|
||||
private static readonly Encoding Latin8BitEncoding = Encoding.GetEncodings()
|
||||
.FirstOrDefault(e => e.CodePage == 1250 || e.CodePage == 1252 || e.CodePage == 28591)?
|
||||
.GetEncoding()
|
||||
?? Encoding.ASCII;
|
||||
private static readonly Encoding Utf8 = new UTF8Encoding(false);
|
||||
|
||||
public static string StripQuotes(this string str)
|
||||
{
|
||||
if (str == null || str.Length < 2)
|
||||
@@ -18,7 +25,7 @@ namespace CompatBot.Utils
|
||||
|
||||
public static string AsString(this ReadOnlySequence<byte> buffer, Encoding encoding = null)
|
||||
{
|
||||
encoding = encoding ?? Encoding.ASCII;
|
||||
encoding = encoding ?? Latin8BitEncoding;
|
||||
if (buffer.IsSingleSegment)
|
||||
return encoding.GetString(buffer.First.Span);
|
||||
|
||||
@@ -33,6 +40,24 @@ namespace CompatBot.Utils
|
||||
return string.Create((int)buffer.Length, buffer, Splice);
|
||||
}
|
||||
|
||||
public static string ToUtf8(this string str)
|
||||
{
|
||||
return Utf8.GetString(Latin8BitEncoding.GetBytes(str));
|
||||
}
|
||||
|
||||
public static string ToLatin8BitEncoding(this string str)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Latin8BitEncoding.GetString(Utf8.GetBytes(str));
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
Console.WriteLine(e);
|
||||
return str;
|
||||
}
|
||||
}
|
||||
|
||||
public static string FixSpaces(this string text) => text?.Replace(" ", " \u200d \u200d");
|
||||
}
|
||||
}
|
||||
|
||||
25
PsnClient/POCOs/App.cs
Normal file
25
PsnClient/POCOs/App.cs
Normal file
@@ -0,0 +1,25 @@
|
||||
namespace PsnClient.POCOs
|
||||
{
|
||||
// https://transact.playstation.com/assets/app.json
|
||||
// returns an array of different objects
|
||||
// api endpoints, oauth, oauth authorize, telemetry, localization options, billing template, locales, country names, topup settings, paypal sandbox settings, gct, apm, sofort, ...
|
||||
|
||||
// this is item #6 in App array
|
||||
public class AppLocales
|
||||
{
|
||||
public string[] EnabledLocales; // "ar-AE",...
|
||||
public AppLocaleOverride[] Overrides;
|
||||
}
|
||||
|
||||
public class AppLocaleOverride
|
||||
{
|
||||
public AppLocaleOverrideCriteria Criteria;
|
||||
public string GensenLocale; // "ar-AE"
|
||||
}
|
||||
|
||||
public class AppLocaleOverrideCriteria
|
||||
{
|
||||
public string Language; // "ar"
|
||||
public string Country; // "AE|BH|KW|LB|OM|QA|SA"
|
||||
}
|
||||
}
|
||||
192
PsnClient/POCOs/Container.cs
Normal file
192
PsnClient/POCOs/Container.cs
Normal file
@@ -0,0 +1,192 @@
|
||||
using System;
|
||||
|
||||
namespace PsnClient.POCOs
|
||||
{
|
||||
public class Container
|
||||
{
|
||||
public ContainerData Data;
|
||||
public ContainerIncluded[] Included;
|
||||
}
|
||||
|
||||
public class ContainerData
|
||||
{
|
||||
public string Id;
|
||||
public string Type;
|
||||
public ContainerDataAttributes Attributes;
|
||||
public Relationships Relationships;
|
||||
}
|
||||
|
||||
public class ContainerDataAttributes
|
||||
{
|
||||
public string Name;
|
||||
public bool? NsxPsPlusUpsell;
|
||||
public int? TemplateId;
|
||||
public string ThumbnailUrlBase;
|
||||
public int TotalResults;
|
||||
public ContainerBanner[] Banners;
|
||||
public ContainerFacet[] Facets;
|
||||
public ContainerPromoBackground[] PromoBackgrounds;
|
||||
public ContainerDataAttributesSubScenes SubScenes;
|
||||
}
|
||||
|
||||
public class ContainerFacet
|
||||
{
|
||||
public string Name;
|
||||
public ContainerFacetItem[] Items;
|
||||
}
|
||||
|
||||
public class ContainerFacetItem
|
||||
{
|
||||
public string Key;
|
||||
public string Name;
|
||||
public int Count;
|
||||
}
|
||||
|
||||
public class ContainerBanner { }
|
||||
public class ContainerPromoBackground { }
|
||||
public class ContainerDataAttributesSubScenes { }
|
||||
|
||||
public class ContainerIncluded
|
||||
{
|
||||
public string Id;
|
||||
public string Type;
|
||||
public ContainerIncludedAttributes Attributes;
|
||||
public Relationships Relationships;
|
||||
}
|
||||
|
||||
public class ContainerIncludedAttributes
|
||||
{
|
||||
public string ContentType; // "1"
|
||||
public string DefaultSkuId;
|
||||
public bool DobRequired;
|
||||
public GameFileSize FileSize;
|
||||
public string GameContentType; // "Bundle"
|
||||
public string[] Genres;
|
||||
public bool? IsIgcUpsell;
|
||||
public bool? IsMultiplayerUpsell;
|
||||
public string KamajiRelationship; // "bundles"
|
||||
public string LegalText;
|
||||
public string LongDescription;
|
||||
public string MacrossBrainContext; // "game"
|
||||
public GameMediaList MediaList;
|
||||
public string Name;
|
||||
public GameParent Parent;
|
||||
public string[] Platforms; // "PS4"
|
||||
public string PrimaryClassification; // "PREMIUM_GAME"
|
||||
public string SecondaryClassification; // "GAME"
|
||||
public string TertiaryClassification; // "BUNDLE"
|
||||
public string ProviderName; // "EA Swiss Sarl"
|
||||
public string PsCameraCompatibility; // "incompatible"
|
||||
public string PsMoveCompatibility; // "incompatible"
|
||||
public string PsVrCompatibility; // "incompatible"
|
||||
public DateTime? ReleaseDate; // "2019-02-22T00:00:00Z"
|
||||
public GameSku[] Skus;
|
||||
public GameStarRating StarRating;
|
||||
public GameLanguageCode[] SubtitleLanguageCodes;
|
||||
public GameLanguageCode[] VoiceLanguageCodes;
|
||||
public string ThumbnailUrlBase;
|
||||
public string TopCategory; // "downloadable_game"
|
||||
public GameUpsellInfo UpsellInfo;
|
||||
// legacy-sku
|
||||
public GameSkuRelation[] Eligibilities;
|
||||
public GameSkuRelation[] Entitlements;
|
||||
}
|
||||
|
||||
public class GameFileSize
|
||||
{
|
||||
public string Unit;
|
||||
public decimal? Value;
|
||||
}
|
||||
|
||||
public class GameMediaList
|
||||
{
|
||||
public GameMediaPreview[] Preview;
|
||||
public GameMediaPromo Promo;
|
||||
public GameMediaLink[] Screenshots;
|
||||
}
|
||||
|
||||
public class GameMediaPreview { }
|
||||
|
||||
public class GameMediaPromo
|
||||
{
|
||||
public GameMediaLink[] Images;
|
||||
public GameMediaLink[] Videos;
|
||||
}
|
||||
|
||||
public class GameMediaLink
|
||||
{
|
||||
public string Url;
|
||||
}
|
||||
|
||||
public class GameParent
|
||||
{
|
||||
public string Id;
|
||||
public string Name;
|
||||
public string Thumbnail;
|
||||
public string Url;
|
||||
}
|
||||
|
||||
public class GameSku
|
||||
{
|
||||
public string Id;
|
||||
public string Name;
|
||||
public bool IsPreorder;
|
||||
public bool? Multibuy;
|
||||
public DateTime? PlayabilityDate;
|
||||
public GameSkuPrices Prices;
|
||||
}
|
||||
|
||||
public class GameSkuPrices
|
||||
{
|
||||
public GameSkuPricesInfo NonPlusUser;
|
||||
public GameSkuPricesInfo PlusUser;
|
||||
}
|
||||
|
||||
public class GameSkuPricesInfo
|
||||
{
|
||||
public GamePriceInfo ActualPrice;
|
||||
public GamePriceAvailability Availability;
|
||||
public decimal DiscountPercentage;
|
||||
public bool IsPlus;
|
||||
public GamePriceInfo StrikeThroughPrice;
|
||||
public GamePriceInfo UpsellPrice;
|
||||
}
|
||||
|
||||
public class GamePriceInfo
|
||||
{
|
||||
public string Display;
|
||||
public decimal Value;
|
||||
}
|
||||
|
||||
public class GamePriceAvailability
|
||||
{
|
||||
public DateTime? StartDate;
|
||||
public DateTime? EndDate;
|
||||
}
|
||||
|
||||
public class GameStarRating
|
||||
{
|
||||
public decimal Score;
|
||||
public int Total;
|
||||
}
|
||||
|
||||
public class GameLanguageCode
|
||||
{
|
||||
public string Name;
|
||||
public string[] Codes;
|
||||
}
|
||||
|
||||
public class GameUpsellInfo
|
||||
{
|
||||
public string Type;
|
||||
public string DisplayPrice;
|
||||
public bool IsFree;
|
||||
public decimal DiscountPercentageDifference;
|
||||
}
|
||||
|
||||
public class GameSkuRelation
|
||||
{
|
||||
public string Id;
|
||||
public string Name;
|
||||
}
|
||||
}
|
||||
24
PsnClient/POCOs/Relationships.cs
Normal file
24
PsnClient/POCOs/Relationships.cs
Normal file
@@ -0,0 +1,24 @@
|
||||
namespace PsnClient.POCOs
|
||||
{
|
||||
public class Relationships
|
||||
{
|
||||
public RelationshipsChildren Children;
|
||||
public RelationshipsLegacySkus LegacySkus;
|
||||
}
|
||||
|
||||
public class RelationshipsChildren
|
||||
{
|
||||
public RelationshipsChildrenItem[] Data;
|
||||
}
|
||||
|
||||
public class RelationshipsChildrenItem
|
||||
{
|
||||
public string Id;
|
||||
public string Type;
|
||||
}
|
||||
|
||||
public class RelationshipsLegacySkus
|
||||
{
|
||||
public RelationshipsChildrenItem[] Data;
|
||||
}
|
||||
}
|
||||
48
PsnClient/POCOs/StoreNavigation.cs
Normal file
48
PsnClient/POCOs/StoreNavigation.cs
Normal file
@@ -0,0 +1,48 @@
|
||||
namespace PsnClient.POCOs
|
||||
{
|
||||
public class StoreNavigation
|
||||
{
|
||||
public StoreNavigationData Data;
|
||||
//public StoreNavigationIncluded Included;
|
||||
}
|
||||
|
||||
public class StoreNavigationData
|
||||
{
|
||||
public string Id;
|
||||
public string Type;
|
||||
public StoreNavigationAttributes Attributes;
|
||||
public Relationships Relationships;
|
||||
}
|
||||
|
||||
public class StoreNavigationAttributes
|
||||
{
|
||||
public string Name;
|
||||
public StoreNavigationNavigation[] Navigation;
|
||||
}
|
||||
|
||||
public class StoreNavigationNavigation
|
||||
{
|
||||
public string Id;
|
||||
public string Name;
|
||||
public string TargetContainerId;
|
||||
public string RouteName;
|
||||
public StoreNavigationSubmenu[] Submenu;
|
||||
}
|
||||
|
||||
public class StoreNavigationSubmenu
|
||||
{
|
||||
public string Name;
|
||||
public string TargetContainerId;
|
||||
public int? TemplateDefId;
|
||||
public StoreNavigationSubmenuItem[] Items;
|
||||
}
|
||||
|
||||
public class StoreNavigationSubmenuItem
|
||||
{
|
||||
public string Name;
|
||||
public string TargetContainerId;
|
||||
public string TargetContainerType;
|
||||
public int? TemplateDefId;
|
||||
public bool IsSeparator;
|
||||
}
|
||||
}
|
||||
30
PsnClient/POCOs/Stores.cs
Normal file
30
PsnClient/POCOs/Stores.cs
Normal file
@@ -0,0 +1,30 @@
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace PsnClient.POCOs
|
||||
{
|
||||
// https://store.playstation.com/kamaji/api/valkyrie_storefront/00_09_000/user/stores
|
||||
// requires session
|
||||
public class Stores
|
||||
{
|
||||
public StoresHeader Header;
|
||||
public StoresData Data;
|
||||
}
|
||||
|
||||
public class StoresHeader
|
||||
{
|
||||
public string Details;
|
||||
[JsonProperty(PropertyName = "errorUUID")]
|
||||
public string ErrorUuid;
|
||||
|
||||
public string MessageKey; // "success"
|
||||
public string StatusCode; // "0x0000"
|
||||
}
|
||||
|
||||
public class StoresData
|
||||
{
|
||||
public string BaseUrl;
|
||||
public string RootUrl;
|
||||
public string SearchUrl;
|
||||
public string TumblerUrl;
|
||||
}
|
||||
}
|
||||
296
PsnClient/PsnClient.cs
Normal file
296
PsnClient/PsnClient.cs
Normal file
@@ -0,0 +1,296 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Net.Http.Formatting;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using CompatApiClient;
|
||||
using CompatApiClient.Compression;
|
||||
using CompatApiClient.Utils;
|
||||
using Newtonsoft.Json;
|
||||
using PsnClient.POCOs;
|
||||
using PsnClient.Utils;
|
||||
using JsonContractResolver = CompatApiClient.JsonContractResolver;
|
||||
|
||||
namespace PsnClient
|
||||
{
|
||||
public class Client
|
||||
{
|
||||
private readonly HttpClient client;
|
||||
private readonly MediaTypeFormatterCollection dashedFormatters;
|
||||
private readonly MediaTypeFormatterCollection underscoreFormatters;
|
||||
private static readonly Regex ContainerIdLink = new Regex(@"(?<id>STORE-(\w|\d)+-(\w|\d)+)");
|
||||
|
||||
public Client()
|
||||
{
|
||||
client = HttpClientFactory.Create(new CompressionMessageHandler());
|
||||
var dashedSettings = new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new JsonContractResolver(NamingStyles.Dashed),
|
||||
NullValueHandling = NullValueHandling.Ignore
|
||||
};
|
||||
dashedFormatters = new MediaTypeFormatterCollection(new[] { new JsonMediaTypeFormatter { SerializerSettings = dashedSettings } });
|
||||
|
||||
var underscoreSettings = new JsonSerializerSettings
|
||||
{
|
||||
ContractResolver = new JsonContractResolver(NamingStyles.Underscore),
|
||||
NullValueHandling = NullValueHandling.Ignore
|
||||
};
|
||||
underscoreFormatters = new MediaTypeFormatterCollection(new[] { new JsonMediaTypeFormatter { SerializerSettings = underscoreSettings } });
|
||||
}
|
||||
|
||||
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))
|
||||
try
|
||||
{
|
||||
await response.Content.LoadIntoBufferAsync().ConfigureAwait(false);
|
||||
var items = await response.Content.ReadAsAsync<AppLocales[]>(underscoreFormatters, cancellationToken).ConfigureAwait(false);
|
||||
return items.FirstOrDefault(i => i?.EnabledLocales != null);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ConsoleLogger.PrintError(e, response);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ConsoleLogger.PrintError(e, null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Stores> GetStoresAsync(string locale, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var cookieHeaderValue = await GetSessionCookies(locale, cancellationToken).ConfigureAwait(false);
|
||||
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))
|
||||
try
|
||||
{
|
||||
await response.Content.LoadIntoBufferAsync().ConfigureAwait(false);
|
||||
return await response.Content.ReadAsAsync<Stores>(underscoreFormatters, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ConsoleLogger.PrintError(e, response);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ConsoleLogger.PrintError(e, null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<string>> GetMainPageNavigationContainerIdsAsync(string locale, CancellationToken cancellationToken)
|
||||
{
|
||||
HttpResponseMessage response = null;
|
||||
try
|
||||
{
|
||||
var baseUrl = $"https://store.playstation.com/{locale}/";
|
||||
var sessionCookies = await GetSessionCookies(locale, cancellationToken).ConfigureAwait(false);
|
||||
using (var message = new HttpRequestMessage(HttpMethod.Get, baseUrl))
|
||||
{
|
||||
message.Headers.Add("Cookie", sessionCookies);
|
||||
response = await client.SendAsync(message, cancellationToken).ConfigureAwait(false);
|
||||
|
||||
await response.Content.LoadIntoBufferAsync().ConfigureAwait(false);
|
||||
var tries = 0;
|
||||
while (response.StatusCode == HttpStatusCode.Redirect && tries < 10 && !cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
using (var newLocationMessage = new HttpRequestMessage(HttpMethod.Get, response.Headers.Location))
|
||||
{
|
||||
newLocationMessage.Headers.Add("Cookie", sessionCookies);
|
||||
var redirectResponse = await client.SendAsync(newLocationMessage, cancellationToken).ConfigureAwait(false);
|
||||
response.Dispose();
|
||||
response = redirectResponse;
|
||||
}
|
||||
tries++;
|
||||
}
|
||||
if (response.StatusCode == HttpStatusCode.Redirect)
|
||||
return new List<string>(0);
|
||||
}
|
||||
|
||||
using (response)
|
||||
try
|
||||
{
|
||||
await response.Content.LoadIntoBufferAsync().ConfigureAwait(false);
|
||||
var html = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
|
||||
var matches = ContainerIdLink.Matches(html);
|
||||
var result = new List<string>();
|
||||
foreach (Match m in matches)
|
||||
if (m.Groups["id"].Value is string id && !string.IsNullOrEmpty(id))
|
||||
result.Add(id);
|
||||
return result;
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ConsoleLogger.PrintError(e, response);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ConsoleLogger.PrintError(e, response);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<StoreNavigation> GetStoreNavigationAsync(string locale, string containerId, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
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))
|
||||
try
|
||||
{
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
return null;
|
||||
|
||||
await response.Content.LoadIntoBufferAsync().ConfigureAwait(false);
|
||||
return await response.Content.ReadAsAsync<StoreNavigation>(dashedFormatters, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ConsoleLogger.PrintError(e, response);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ConsoleLogger.PrintError(e, null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Container> GetGameContainerAsync(string locale, string containerId, int start, int take, Dictionary<string, string> filters, CancellationToken cancellationToken)
|
||||
{
|
||||
try
|
||||
{
|
||||
var loc = locale.AsLocaleData();
|
||||
var url = new Uri($"https://store.playstation.com/valkyrie-api/{loc.language}/{loc.country}/999/container/{containerId}");
|
||||
filters = filters ?? new Dictionary<string, string>();
|
||||
filters["start"] = start.ToString();
|
||||
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))
|
||||
try
|
||||
{
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
return null;
|
||||
|
||||
await response.Content.LoadIntoBufferAsync().ConfigureAwait(false);
|
||||
return await response.Content.ReadAsAsync<Container>(dashedFormatters, cancellationToken).ConfigureAwait(false);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ConsoleLogger.PrintError(e, response);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ConsoleLogger.PrintError(e, null);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<Container> ResolveContentAsync(string locale, string contentId, int depth, CancellationToken cancellationToken)
|
||||
{
|
||||
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))
|
||||
try
|
||||
{
|
||||
if (response.StatusCode == HttpStatusCode.NotFound)
|
||||
return null;
|
||||
|
||||
await response.Content.LoadIntoBufferAsync().ConfigureAwait(false);
|
||||
return await response.Content.ReadAsAsync<Container>(dashedFormatters, 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();
|
||||
var uri = new Uri("https://store.playstation.com/kamaji/api/valkyrie_storefront/00_09_000/user/session");
|
||||
var tries = 0;
|
||||
do
|
||||
{
|
||||
try
|
||||
{
|
||||
HttpResponseMessage response;
|
||||
using (var deleteMessage = new HttpRequestMessage(HttpMethod.Delete, uri))
|
||||
using (response = await client.SendAsync(deleteMessage, cancellationToken))
|
||||
if (response.StatusCode != HttpStatusCode.OK)
|
||||
ConsoleLogger.PrintError(new InvalidOperationException("Couldn't delete current session"), response, ConsoleColor.Yellow);
|
||||
|
||||
var authMessage = new HttpRequestMessage(HttpMethod.Post, uri)
|
||||
{
|
||||
Content = new FormUrlEncodedContent(new Dictionary<string, string>
|
||||
{
|
||||
["country_code"] = loc.country,
|
||||
["language_code"] = loc.language,
|
||||
})
|
||||
};
|
||||
using (authMessage)
|
||||
using (response = await client.SendAsync(authMessage, cancellationToken).ConfigureAwait(false))
|
||||
try
|
||||
{
|
||||
var cookieContainer = new CookieContainer();
|
||||
foreach (var cookie in response.Headers.GetValues("set-cookie"))
|
||||
cookieContainer.SetCookies(uri, cookie);
|
||||
return cookieContainer.GetCookieHeader(uri);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ConsoleLogger.PrintError(e, response, tries < 3 ? ConsoleColor.Yellow : ConsoleColor.Red);
|
||||
tries++;
|
||||
}
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
ConsoleLogger.PrintError(e, null, tries < 3 ? ConsoleColor.Yellow : ConsoleColor.Red);
|
||||
tries++;
|
||||
}
|
||||
} while (tries < 3);
|
||||
throw new InvalidOperationException("Couldn't obtain web session");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
15
PsnClient/PsnClient.csproj
Normal file
15
PsnClient/PsnClient.csproj
Normal file
@@ -0,0 +1,15 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>netstandard2.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNet.WebApi.Client" Version="5.2.6" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\CompatApiClient\CompatApiClient.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
18
PsnClient/Utils/LocaleUtils.cs
Normal file
18
PsnClient/Utils/LocaleUtils.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace PsnClient.Utils
|
||||
{
|
||||
public static class LocaleUtils
|
||||
{
|
||||
public static (string language, string country) AsLocaleData(this string locale)
|
||||
{
|
||||
/*
|
||||
"zh-Hans-CN" -> zh-CN
|
||||
"zh-Hans-HK" -> zh-HK
|
||||
"zh-Hant-HK" -> ch-HK
|
||||
"zh-Hant-TW" -> ch-TW
|
||||
*/
|
||||
locale = locale.Replace("zh-Hans", "zh").Replace("zh-Hant", "ch");
|
||||
var localeParts = locale.Split('-');
|
||||
return (localeParts[0], localeParts[1]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,7 @@ How to Build
|
||||
* Note that token could be set in the settings _or_ supplied as a launch argument (higher priority)
|
||||
* If you've changed the database model, add a migration
|
||||
* `$ cd CompatBot`
|
||||
* `$ dotnet ef migrations add NAME`
|
||||
* `$ dotnet ef migrations add -c [BotDb|ThumbnailDb] MigrationName`
|
||||
* `$ cd ..`
|
||||
* `$ cd CompatBot`
|
||||
* `$ dotnet run [token]`
|
||||
|
||||
@@ -3,9 +3,11 @@ Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio 15
|
||||
VisualStudioVersion = 15.0.27703.2035
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompatBot", "CompatBot\CompatBot.csproj", "{6D9CA448-60C1-4D66-91D6-EC6C586508E6}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompatBot", "CompatBot\CompatBot.csproj", "{6D9CA448-60C1-4D66-91D6-EC6C586508E6}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "CompatApiClient", "CompatApiClient\CompatApiClient.csproj", "{8AF3C23B-D695-4391-A298-5BA4AAB8E13B}"
|
||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CompatApiClient", "CompatApiClient\CompatApiClient.csproj", "{8AF3C23B-D695-4391-A298-5BA4AAB8E13B}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PsnClient", "PsnClient\PsnClient.csproj", "{AA5FF441-BD1D-4444-9178-7DC7BFF3C139}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
@@ -21,6 +23,10 @@ Global
|
||||
{8AF3C23B-D695-4391-A298-5BA4AAB8E13B}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{8AF3C23B-D695-4391-A298-5BA4AAB8E13B}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{8AF3C23B-D695-4391-A298-5BA4AAB8E13B}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{AA5FF441-BD1D-4444-9178-7DC7BFF3C139}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{AA5FF441-BD1D-4444-9178-7DC7BFF3C139}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{AA5FF441-BD1D-4444-9178-7DC7BFF3C139}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{AA5FF441-BD1D-4444-9178-7DC7BFF3C139}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
Reference in New Issue
Block a user