From 18a363f47b54201f757dfdd5240bd2b1f6f2437b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=84kwav?= Date: Fri, 17 Dec 2021 16:58:49 +0100 Subject: [PATCH] create service --- Client/generate.sh | 10 +- Commands/ChatPart.cs | 21 + Commands/FirstModVersionAdapter.cs | 42 + Commands/IModVersionAdapter.cs | 12 + Commands/Minecraft/BlacklistCommand.cs | 21 + Commands/Minecraft/BlockedCommand.cs | 28 + Commands/Minecraft/ClickedCommand.cs | 12 + Commands/Minecraft/ExactCommand.cs | 17 + Commands/Minecraft/ExperimentalCommand.cs | 19 + Commands/Minecraft/McCommand.cs | 11 + Commands/Minecraft/NormalCommand.cs | 17 + Commands/Minecraft/OnlineCommand.cs | 13 + Commands/Minecraft/PurchaseConfirmCommand.cs | 12 + Commands/Minecraft/PurchaseStartCommand.cs | 12 + Commands/Minecraft/RateCommand.cs | 62 ++ Commands/Minecraft/ReferenceCommand.cs | 49 ++ Commands/Minecraft/ReportCommand.cs | 31 + Commands/Minecraft/ResetCommand.cs | 12 + Commands/Minecraft/SniperCommand.cs | 17 + Commands/Minecraft/SoundCommand.cs | 16 + Commands/Minecraft/TestCommand.cs | 14 + Commands/MinecraftSocket.cs | 794 +++++++++++++++++++ Commands/NextUpdateRetriever.cs | 28 + Commands/Response.cs | 26 + Commands/SecondVersionAdapter.cs | 61 ++ Commands/ThirdVersionAdapter.cs | 50 ++ Constants/McColorCodes.cs | 23 + Controllers/StatsController.cs | 39 + Controllers/TrackerController.cs | 48 -- Dockerfile | 7 +- Models/BaseDbContext.cs | 9 +- Models/Flip.cs | 24 - Program.cs | 41 +- Services/BaseBackgroundService.cs | 28 +- Services/BaseService.cs | 44 +- SkyBase.csproj => SkyModCommands.csproj | 14 + Startup.cs | 12 +- appsettings.json | 9 +- 38 files changed, 1547 insertions(+), 158 deletions(-) create mode 100644 Commands/ChatPart.cs create mode 100644 Commands/FirstModVersionAdapter.cs create mode 100644 Commands/IModVersionAdapter.cs create mode 100644 Commands/Minecraft/BlacklistCommand.cs create mode 100644 Commands/Minecraft/BlockedCommand.cs create mode 100644 Commands/Minecraft/ClickedCommand.cs create mode 100644 Commands/Minecraft/ExactCommand.cs create mode 100644 Commands/Minecraft/ExperimentalCommand.cs create mode 100644 Commands/Minecraft/McCommand.cs create mode 100644 Commands/Minecraft/NormalCommand.cs create mode 100644 Commands/Minecraft/OnlineCommand.cs create mode 100644 Commands/Minecraft/PurchaseConfirmCommand.cs create mode 100644 Commands/Minecraft/PurchaseStartCommand.cs create mode 100644 Commands/Minecraft/RateCommand.cs create mode 100644 Commands/Minecraft/ReferenceCommand.cs create mode 100644 Commands/Minecraft/ReportCommand.cs create mode 100644 Commands/Minecraft/ResetCommand.cs create mode 100644 Commands/Minecraft/SniperCommand.cs create mode 100644 Commands/Minecraft/SoundCommand.cs create mode 100644 Commands/Minecraft/TestCommand.cs create mode 100644 Commands/MinecraftSocket.cs create mode 100644 Commands/NextUpdateRetriever.cs create mode 100644 Commands/Response.cs create mode 100644 Commands/SecondVersionAdapter.cs create mode 100644 Commands/ThirdVersionAdapter.cs create mode 100644 Constants/McColorCodes.cs create mode 100644 Controllers/StatsController.cs delete mode 100644 Controllers/TrackerController.cs delete mode 100644 Models/Flip.cs rename SkyBase.csproj => SkyModCommands.csproj (66%) diff --git a/Client/generate.sh b/Client/generate.sh index d90b9b1c..9cbfba4a 100644 --- a/Client/generate.sh +++ b/Client/generate.sh @@ -3,12 +3,12 @@ VERSION=0.0.1 docker run --rm -v "${PWD}:/local" --network host -u $(id -u ${USER}):$(id -g ${USER}) openapitools/openapi-generator-cli generate \ -i http://localhost:5000/swagger/v1/swagger.json \ -g csharp-netcore \ --o /local/out --additional-properties=packageName=Coflnet.Sky.Base.Client,packageVersion=$VERSION,licenseId=MIT +-o /local/out --additional-properties=packageName=Coflnet.Sky.ModCommands.Client,packageVersion=$VERSION,licenseId=MIT cd out -sed -i 's/GIT_USER_ID/Coflnet/g' src/Coflnet.Sky.Base.Client/Coflnet.Sky.Base.Client.csproj -sed -i 's/GIT_REPO_ID/SkyBase/g' src/Coflnet.Sky.Base.Client/Coflnet.Sky.Base.Client.csproj -sed -i 's/>OpenAPI/>Coflnet/g' src/Coflnet.Sky.Base.Client/Coflnet.Sky.Base.Client.csproj +sed -i 's/GIT_USER_ID/Coflnet/g' src/Coflnet.Sky.ModCommands.Client/Coflnet.Sky.ModCommands.Client.csproj +sed -i 's/GIT_REPO_ID/SkyModCommands/g' src/Coflnet.Sky.ModCommands.Client/Coflnet.Sky.ModCommands.Client.csproj +sed -i 's/>OpenAPI/>Coflnet/g' src/Coflnet.Sky.ModCommands.Client/Coflnet.Sky.ModCommands.Client.csproj dotnet pack -cp src/Coflnet.Sky.Base.Client/bin/Debug/Coflnet.Sky.Base.Client.*.nupkg .. +cp src/Coflnet.Sky.ModCommands.Client/bin/Debug/Coflnet.Sky.ModCommands.Client.*.nupkg .. diff --git a/Commands/ChatPart.cs b/Commands/ChatPart.cs new file mode 100644 index 00000000..75c2243f --- /dev/null +++ b/Commands/ChatPart.cs @@ -0,0 +1,21 @@ +namespace Coflnet.Sky.Commands.MC +{ + public class ChatPart + { + public string text; + public string onClick; + public string hover; + + public ChatPart() + { + } + + public ChatPart(string text, string onClick = null, string hover = null) + { + this.text = text; + this.onClick = onClick; + this.hover = hover; + } + + } +} \ No newline at end of file diff --git a/Commands/FirstModVersionAdapter.cs b/Commands/FirstModVersionAdapter.cs new file mode 100644 index 00000000..8f6f425f --- /dev/null +++ b/Commands/FirstModVersionAdapter.cs @@ -0,0 +1,42 @@ +using System.Linq; +using System.Threading.Tasks; +using hypixel; + +namespace Coflnet.Sky.Commands.MC +{ + public class FirstModVersionAdapter : IModVersionAdapter + { + MinecraftSocket socket; + + public FirstModVersionAdapter(MinecraftSocket socket) + { + this.socket = socket; + SendUpdateMessage(); + } + + private void SendUpdateMessage() + { + socket.SendMessage(MinecraftSocket.COFLNET + McColorCodes.RED + "There is a newer mod version. click this to open discord and download it", "https://discord.com/channels/267680588666896385/890682907889373257/898974585318416395"); + } + + public Task SendFlip(FlipInstance flip) + { + socket.SendMessage(socket.GetFlipMsg(flip), "/viewauction " + flip.Auction.Uuid, "UPDATE"); + SendUpdateMessage(); + return Task.FromResult(true); + } + + public void SendMessage(params ChatPart[] parts) + { + var part = parts.FirstOrDefault(); + socket.SendMessage(part.text, part.onClick, part.hover); + SendUpdateMessage(); + } + + public void SendSound(string name, float pitch = 1f) + { + // no support + SendUpdateMessage(); + } + } +} \ No newline at end of file diff --git a/Commands/IModVersionAdapter.cs b/Commands/IModVersionAdapter.cs new file mode 100644 index 00000000..225c753a --- /dev/null +++ b/Commands/IModVersionAdapter.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; +using hypixel; + +namespace Coflnet.Sky.Commands.MC +{ + public interface IModVersionAdapter + { + Task SendFlip(FlipInstance flip); + void SendSound(string name, float pitch = 1); + void SendMessage(params ChatPart[] parts); + } +} \ No newline at end of file diff --git a/Commands/Minecraft/BlacklistCommand.cs b/Commands/Minecraft/BlacklistCommand.cs new file mode 100644 index 00000000..92f1df36 --- /dev/null +++ b/Commands/Minecraft/BlacklistCommand.cs @@ -0,0 +1,21 @@ +using System.Threading.Tasks; +using Coflnet.Sky.Commands.Shared; + +namespace Coflnet.Sky.Commands.MC +{ + public class BlacklistCommand : McCommand + { + public override async Task Execute(MinecraftSocket socket, string arguments) + { + var tag = Newtonsoft.Json.JsonConvert.DeserializeObject(arguments); + await socket.UpdateSettings(settings => + { + if (settings.Settings.BlackList == null) + settings.Settings.BlackList = new System.Collections.Generic.List(); + settings.Settings.BlackList.Add(new ListEntry() { ItemTag = tag }); + return settings; + }); + socket.SendMessage(COFLNET + $"You blacklisted all {arguments} from appearing"); + } + } +} \ No newline at end of file diff --git a/Commands/Minecraft/BlockedCommand.cs b/Commands/Minecraft/BlockedCommand.cs new file mode 100644 index 00000000..b919631c --- /dev/null +++ b/Commands/Minecraft/BlockedCommand.cs @@ -0,0 +1,28 @@ +using System; +using System.Linq; +using System.Threading.Tasks; + +namespace Coflnet.Sky.Commands.MC +{ + public class BlockedCommand : McCommand + { + public override async Task Execute(MinecraftSocket socket, string arguments) + { + if (socket.TopBlocked.Count == 0) + { + socket.SendMessage(COFLNET + "No blocked flips found, make sure you don't click this shortly after the 'flips in 10 seconds' message. (the list gets reset when that message appears)"); + return; + } + var r = new Random(); + socket.SendMessage(socket.TopBlocked.OrderBy(e=>r.Next()).Take(5).Select(b => + { + socket.Settings.GetPrice(hypixel.FlipperService.LowPriceToFlip(b.Flip), out long targetPrice, out long profit); + return new ChatPart( + $"{b.Flip.Auction.ItemName} (+{socket.FormatPrice(profit)}) got blocked because {b.Reason.Replace("SNIPER", "experimental flip finder")}\n", + "https://sky.coflnet.com/auction/" + b.Flip.Auction.Uuid, + "Click to open"); + }) + .Append(new ChatPart() { text = COFLNET + "These are random examples of blocked flips.", onClick = "/cofl blocked", hover = "Execute again to get another sample" }).ToArray()); + } + } +} \ No newline at end of file diff --git a/Commands/Minecraft/ClickedCommand.cs b/Commands/Minecraft/ClickedCommand.cs new file mode 100644 index 00000000..a1d97aac --- /dev/null +++ b/Commands/Minecraft/ClickedCommand.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; + +namespace Coflnet.Sky.Commands.MC +{ + public class ClickedCommand : McCommand + { + public override Task Execute(MinecraftSocket socket, string arguments) + { + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/Commands/Minecraft/ExactCommand.cs b/Commands/Minecraft/ExactCommand.cs new file mode 100644 index 00000000..a7cdbe4e --- /dev/null +++ b/Commands/Minecraft/ExactCommand.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; + +namespace Coflnet.Sky.Commands.MC +{ + public class ExactCommand : McCommand + { + public override async Task Execute(MinecraftSocket socket, string arguments) + { + await socket.UpdateSettings(settings => + { + settings.Settings.AllowedFinders = LowPricedAuction.FinderType.SNIPER | LowPricedAuction.FinderType.SNIPER_MEDIAN; + return settings; + }); + socket.SendMessage(COFLNET + $"You enabled the exact flip mode, this is experimental"); + } + } +} \ No newline at end of file diff --git a/Commands/Minecraft/ExperimentalCommand.cs b/Commands/Minecraft/ExperimentalCommand.cs new file mode 100644 index 00000000..11e33f32 --- /dev/null +++ b/Commands/Minecraft/ExperimentalCommand.cs @@ -0,0 +1,19 @@ +using System.Threading.Tasks; + +namespace Coflnet.Sky.Commands.MC +{ + public class ExperimentalCommand : McCommand + { + public override async Task Execute(MinecraftSocket socket, string arguments) + { + await socket.UpdateSettings(settings => + { + settings.Settings.AllowedFinders = LowPricedAuction.FinderType.FLIPPER | LowPricedAuction.FinderType.SNIPER_MEDIAN | LowPricedAuction.FinderType.SNIPER; + return settings; + }); + socket.SendMessage(COFLNET + $"You opted in into experimental flips"); + } + } + + +} \ No newline at end of file diff --git a/Commands/Minecraft/McCommand.cs b/Commands/Minecraft/McCommand.cs new file mode 100644 index 00000000..91f91ca3 --- /dev/null +++ b/Commands/Minecraft/McCommand.cs @@ -0,0 +1,11 @@ +using System.Threading.Tasks; + +namespace Coflnet.Sky.Commands.MC +{ + public abstract class McCommand + { + public string COFLNET => MinecraftSocket.COFLNET; + public static string DEFAULT_COLOR => McColorCodes.GRAY; + public abstract Task Execute(MinecraftSocket socket, string arguments); + } +} \ No newline at end of file diff --git a/Commands/Minecraft/NormalCommand.cs b/Commands/Minecraft/NormalCommand.cs new file mode 100644 index 00000000..1ebbfa95 --- /dev/null +++ b/Commands/Minecraft/NormalCommand.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; + +namespace Coflnet.Sky.Commands.MC +{ + public class NormalCommand : McCommand + { + public override async Task Execute(MinecraftSocket socket, string arguments) + { + await socket.UpdateSettings(settings => + { + settings.Settings.AllowedFinders = LowPricedAuction.FinderType.FLIPPER; + return settings; + }); + socket.SendMessage(COFLNET + $"You went back to normal flipper mode again"); + } + } +} \ No newline at end of file diff --git a/Commands/Minecraft/OnlineCommand.cs b/Commands/Minecraft/OnlineCommand.cs new file mode 100644 index 00000000..2f81c425 --- /dev/null +++ b/Commands/Minecraft/OnlineCommand.cs @@ -0,0 +1,13 @@ +using System.Threading.Tasks; + +namespace Coflnet.Sky.Commands.MC +{ + public class OnlineCommand : McCommand + { + public override Task Execute(MinecraftSocket socket, string arguments) + { + socket.SendMessage(COFLNET + $"There are {hypixel.FlipperService.Instance.PremiumUserCount} users connected to this server"); + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/Commands/Minecraft/PurchaseConfirmCommand.cs b/Commands/Minecraft/PurchaseConfirmCommand.cs new file mode 100644 index 00000000..2d67ac2e --- /dev/null +++ b/Commands/Minecraft/PurchaseConfirmCommand.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; + +namespace Coflnet.Sky.Commands.MC +{ + public class PurchaseConfirmCommand : McCommand + { + public override Task Execute(MinecraftSocket socket, string arguments) + { + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/Commands/Minecraft/PurchaseStartCommand.cs b/Commands/Minecraft/PurchaseStartCommand.cs new file mode 100644 index 00000000..a54920b1 --- /dev/null +++ b/Commands/Minecraft/PurchaseStartCommand.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; + +namespace Coflnet.Sky.Commands.MC +{ + public class PurchaseStartCommand : McCommand + { + public override Task Execute(MinecraftSocket socket, string arguments) + { + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/Commands/Minecraft/RateCommand.cs b/Commands/Minecraft/RateCommand.cs new file mode 100644 index 00000000..6cdd74ba --- /dev/null +++ b/Commands/Minecraft/RateCommand.cs @@ -0,0 +1,62 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using hypixel; +using Coflnet.Sky.Commands.Shared; + +namespace Coflnet.Sky.Commands.MC +{ + public class RateCommand : McCommand + { + public override async Task Execute(MinecraftSocket socket, string arguments) + { + var args = Newtonsoft.Json.JsonConvert.DeserializeObject(arguments).Split(" "); + var uuid = args[0]; + var finder = args[1]; + var rating = args[2]; + using var span = socket.tracer.BuildSpan("vote").WithTag("type", rating).WithTag("finder", finder).WithTag("uuid", uuid).AsChildOf(socket.ConSpan).StartActive(); + var bad = socket.GetFlip(uuid); + span.Span.Log(JSON.Stringify(bad)); + span.Span.Log(JSON.Stringify(bad?.AdditionalProps)); + + + if (rating == "down") + { + if(bad != null) + Blacklist(socket, bad); + socket.SendMessage(new ChatPart(COFLNET + "Thanks for your feedback, Please help us better understand why this flip is bad\n", null, "you can also send free text with /cofl report"), + new ChatPart(" * Its overpriced\n", "/cofl report overpriced "), + new ChatPart(" * This item sells slowly\n", "/cofl report slow sell"), + new ChatPart(" * I blacklisted this before\n", "/cofl report blacklist broken"), + new ChatPart(" * This item is manipulated\n", "/cofl report manipulated item"), + new ChatPart(" * Reference auctions are wrong \n", "/cofl report reference auctions are wrong ", "please send /cofl report with further information")); + await FlipTrackingService.Instance.DownVote(uuid, socket.McUuid); + } + else if (rating == "up") + { + socket.SendMessage(new ChatPart(COFLNET + "Thanks for your feedback, Please help us better understand why this flip is good\n"), + new ChatPart(" * it isn't I mis-clicked \n", "/cofl report misclicked "), + new ChatPart(" * This item sells fast\n", "/cofl report fast sell"), + new ChatPart(" * High profit\n", "/cofl report high profit"), + new ChatPart(" * Something else \n", null, "please send /cofl report with further information")); + await FlipTrackingService.Instance.UpVote(uuid, socket.McUuid); + } + else + { + socket.SendMessage(COFLNET + $"Thanks for your feedback, you voted this flip " + rating, "/cofl undo", "We will try to improve the flips accordingly"); + } + await Task.Delay(3000); + var based = await hypixel.CoreServer.ExecuteCommandWithCache>("flipBased", uuid); + span.Span.Log(string.Join('\n', based.Select(b => $"{b.ItemName} {b.highestBid} {b.uuid}"))); + } + + private static void Blacklist(MinecraftSocket socket, LowPricedAuction bad) + { + if (socket.Settings.BlackList == null) + socket.Settings.BlackList = new System.Collections.Generic.List(); + socket.Settings.BlackList.Add(new ListEntry() { ItemTag = bad.Auction.Tag }); + } + } + + +} \ No newline at end of file diff --git a/Commands/Minecraft/ReferenceCommand.cs b/Commands/Minecraft/ReferenceCommand.cs new file mode 100644 index 00000000..66807cd8 --- /dev/null +++ b/Commands/Minecraft/ReferenceCommand.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Coflnet.Sky.Commands.Shared; +using hypixel; + +namespace Coflnet.Sky.Commands.MC +{ + public class ReferenceCommand : McCommand + { + public override async Task Execute(MinecraftSocket socket, string arguments) + { + Console.WriteLine(arguments); + var uuid = arguments.Trim('"'); + var flip = socket.GetFlip(uuid); + if (flip.Finder.HasFlag(LowPricedAuction.FinderType.SNIPER)) + { + await SniperReference(socket, uuid, flip, "sniping"); + return; + } + if (flip.Finder.HasFlag(LowPricedAuction.FinderType.SNIPER_MEDIAN)) + { + await SniperReference(socket, uuid, flip, "median sniper"); + return; + } + socket.ModAdapter.SendMessage(new ChatPart("Caclulating references", "https://sky.coflnet.com/auction/" + uuid, "please give it a second")); + var based = await CoreServer.ExecuteCommandWithCache>("flipBased", uuid); + socket.ModAdapter.SendMessage(based + .Select(b => new ChatPart( + $"\n-> {b.ItemName} for {McColorCodes.AQUA}{socket.FormatPrice(b.highestBid)}{McColorCodes.GRAY} {b.end}", + "https://sky.coflnet.com/auction/" + b.uuid, + "Click to open this auction")) + .ToArray()); + await Task.Delay(200); + socket.ModAdapter.SendMessage(new ChatPart(MinecraftSocket.COFLNET + "click this to open the auction on the website (in case you want to report an error or share it)", "https://sky.coflnet.com/auction/" + uuid, "please give it a second")); + } + + private async Task SniperReference(MinecraftSocket socket, string uuid, LowPricedAuction flip, string algo) + { + var reference = await AuctionService.Instance.GetAuctionAsync(flip.AdditionalProps["reference"]); + Console.WriteLine(JSON.Stringify(flip.AdditionalProps)); + Console.WriteLine(JSON.Stringify(reference)); + socket.ModAdapter.SendMessage(new ChatPart($"{COFLNET}This flip was found by the {algo} algorithm\n", "https://sky.coflnet.com/auction/" + uuid, "click this to open the flip on website"), + new ChatPart($"It was compared to {McColorCodes.AQUA} this auction {DEFAULT_COLOR}, open ah", $"/viewauction {reference.Uuid}", McColorCodes.GREEN + "open it on ah"), + new ChatPart($"{McColorCodes.WHITE} open on website", $"https://sky.coflnet.com/auction/{reference.Uuid}", "open it on website")); + } + } +} \ No newline at end of file diff --git a/Commands/Minecraft/ReportCommand.cs b/Commands/Minecraft/ReportCommand.cs new file mode 100644 index 00000000..f30a0212 --- /dev/null +++ b/Commands/Minecraft/ReportCommand.cs @@ -0,0 +1,31 @@ +using System.Threading.Tasks; +using hypixel; +using Newtonsoft.Json; +using OpenTracing.Util; + +namespace Coflnet.Sky.Commands.MC +{ + public class ReportCommand : McCommand + { + public override Task Execute(MinecraftSocket socket, string arguments) + { + if(string.IsNullOrEmpty(arguments) || arguments.Length < 3) + { + socket.SendMessage(COFLNET + "Please add some information to the report, ie. what happened, what do you think should have happened."); + } + using var reportSpan = socket.tracer.BuildSpan("report") + .WithTag("message", arguments.Truncate(150)) + .WithTag("error", "true") + .WithTag("mcId", JsonConvert.SerializeObject(socket.McId)) + .AsChildOf(socket.ConSpan).StartActive(); + + reportSpan.Span.Log(JsonConvert.SerializeObject(socket.Settings)); + reportSpan.Span.Log(JsonConvert.SerializeObject(socket.TopBlocked)); + var spanId = reportSpan.Span.Context.SpanId.Truncate(6); + reportSpan.Span.SetTag("id", spanId); + + socket.SendMessage(COFLNET + "Thanks for your report :)\n If you need further help, please refer to this report with " + spanId, spanId); + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/Commands/Minecraft/ResetCommand.cs b/Commands/Minecraft/ResetCommand.cs new file mode 100644 index 00000000..8363a584 --- /dev/null +++ b/Commands/Minecraft/ResetCommand.cs @@ -0,0 +1,12 @@ +using System.Threading.Tasks; + +namespace Coflnet.Sky.Commands.MC +{ + public class ResetCommand : McCommand + { + public override Task Execute(MinecraftSocket socket, string arguments) + { + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/Commands/Minecraft/SniperCommand.cs b/Commands/Minecraft/SniperCommand.cs new file mode 100644 index 00000000..a2b9f9ab --- /dev/null +++ b/Commands/Minecraft/SniperCommand.cs @@ -0,0 +1,17 @@ +using System.Threading.Tasks; + +namespace Coflnet.Sky.Commands.MC +{ + public class SniperCommand : McCommand + { + public override async Task Execute(MinecraftSocket socket, string arguments) + { + await socket.UpdateSettings(settings => + { + settings.Settings.AllowedFinders = LowPricedAuction.FinderType.SNIPER; + return settings; + }); + socket.SendMessage(COFLNET + $"You enabled the super secret sniper mode :O"); + } + } +} \ No newline at end of file diff --git a/Commands/Minecraft/SoundCommand.cs b/Commands/Minecraft/SoundCommand.cs new file mode 100644 index 00000000..ec7c9a0c --- /dev/null +++ b/Commands/Minecraft/SoundCommand.cs @@ -0,0 +1,16 @@ +using System.Threading.Tasks; +using Newtonsoft.Json; + +namespace Coflnet.Sky.Commands.MC +{ + public class SoundCommand : McCommand + { + public override Task Execute(MinecraftSocket socket, string arguments) + { + var name = JsonConvert.DeserializeObject(arguments); + socket.SendSound(name); + socket.SendMessage("playing " + name); + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/Commands/Minecraft/TestCommand.cs b/Commands/Minecraft/TestCommand.cs new file mode 100644 index 00000000..39c78ddc --- /dev/null +++ b/Commands/Minecraft/TestCommand.cs @@ -0,0 +1,14 @@ +using System.Threading.Tasks; + +namespace Coflnet.Sky.Commands.MC +{ + public class TestCommand : McCommand + { + public override Task Execute(MinecraftSocket socket, string arguments) + { + socket.SendSound("random.orb"); + socket.SendMessage("The test was successful :)"); + return Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/Commands/MinecraftSocket.cs b/Commands/MinecraftSocket.cs new file mode 100644 index 00000000..290a805e --- /dev/null +++ b/Commands/MinecraftSocket.cs @@ -0,0 +1,794 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using System.Web; +using Coflnet.Sky.Commands.Helper; +using Coflnet.Sky.Commands.Shared; +using Coflnet.Sky.Filter; +using hypixel; +using Jaeger.Samplers; +using Newtonsoft.Json; +using WebSocketSharp; +using WebSocketSharp.Server; + +namespace Coflnet.Sky.Commands.MC +{ + public partial class MinecraftSocket : WebSocketBehavior, IFlipConnection + { + public string McId; + public string McUuid = "00000000000000000"; + public static string COFLNET = "[§1C§6oflnet§f]§7: "; + + public long Id { get; private set; } + + protected string sessionId = ""; + + public FlipSettings Settings => LastSettingsChange.Settings; + public int UserId => LastSettingsChange.UserId; + private SettingsChange LastSettingsChange { get; set; } = new SettingsChange() { Settings = DEFAULT_SETTINGS }; + + public string Version { get; private set; } + public OpenTracing.ITracer tracer = new Jaeger.Tracer.Builder("sky-commands-mod").WithSampler(new ConstSampler(true)).Build(); + public OpenTracing.ISpan ConSpan { get; private set; } + private System.Threading.Timer PingTimer; + + public IModVersionAdapter ModAdapter; + + public static FlipSettings DEFAULT_SETTINGS = new FlipSettings() { MinProfit = 100000, MinVolume = 50, ModSettings = new ModSettings(), Visibility = new VisibilitySettings() }; + + public static ClassNameDictonary Commands = new ClassNameDictonary(); + + public static event Action NextUpdateStart; + private int blockedFlipFilterCount => TopBlocked.Count; + + private static System.Threading.Timer updateTimer; + + private ConcurrentDictionary SentFlips = new ConcurrentDictionary(); + public ConcurrentQueue TopBlocked = new ConcurrentQueue(); + public ConcurrentQueue LastSent = new ConcurrentQueue(); + + public class BlockedElement + { + public LowPricedAuction Flip; + public string Reason; + } + private static Prometheus.Counter sentFlipsCount = Prometheus.Metrics.CreateCounter("sky_commands_sent_flips", "How many flip messages were sent"); + + static MinecraftSocket() + { + Commands.Add(); + Commands.Add(); + Commands.Add(); + Commands.Add(); + Commands.Add(); + Commands.Add(); + Commands.Add(); + Commands.Add(); + Commands.Add(); + Commands.Add(); + Commands.Add(); + Commands.Add(); + Commands.Add(); + Commands.Add(); + Commands.Add(); + Commands.Add(); + + Task.Run(async () => + { + var next = await new NextUpdateRetriever().Get(); + + NextUpdateStart += () => + { + Console.WriteLine("next update"); + GC.Collect(); + }; + while (next < DateTime.Now) + next += TimeSpan.FromMinutes(1); + Console.WriteLine($"started timer to start at {next} now its {DateTime.Now}"); + updateTimer = new System.Threading.Timer((e) => + { + try + { + NextUpdateStart?.Invoke(); + } + catch (Exception ex) + { + dev.Logger.Instance.Error(ex, "sending next update"); + } + }, null, next - DateTime.Now, TimeSpan.FromMinutes(1)); + }).ConfigureAwait(false); + } + + protected override void OnOpen() + { + ConSpan = tracer.BuildSpan("connection").Start(); + base.OnOpen(); + Task.Run(() => + { + using var openSpan = tracer.BuildSpan("open").AsChildOf(ConSpan).StartActive(); + try + { + StartConnection(openSpan); + } + catch (Exception e) + { + Error(e, "starting connection"); + } + }).ConfigureAwait(false); + + } + + private void StartConnection(OpenTracing.IScope openSpan) + { + SendMessage(COFLNET + "§fNOTE §7This is a development preview, it is NOT stable/bugfree", $"https://discord.gg/wvKXfTgCfb", System.Net.Dns.GetHostName()); + var args = System.Web.HttpUtility.ParseQueryString(Context.RequestUri.Query); + Console.WriteLine(Context.RequestUri.Query); + if (args["uuid"] == null && args["player"] == null) + Send(Response.Create("error", "the connection query string needs to include 'player'")); + if (args["SId"] != null) + sessionId = args["SId"].Truncate(60); + if (args["version"] != null) + Version = args["version"].Truncate(10); + + ModAdapter = Version switch + { + "1.3-Alpha" => new ThirdVersionAdapter(this), + "1.2-Alpha" => new SecondVersionAdapter(this), + _ => new FirstModVersionAdapter(this) + }; + + McId = args["player"] ?? args["uuid"]; + ConSpan.SetTag("uuid", McId); + ConSpan.SetTag("version", Version); + + string stringId; + (this.Id, stringId) = ComputeConnectionId(); + ConSpan.SetTag("conId", stringId); + + + if (Settings == null) + LastSettingsChange.Settings = DEFAULT_SETTINGS; + FlipperService.Instance.AddNonConnection(this, false); + System.Threading.Tasks.Task.Run(async () => + { + await SetupConnectionSettings(stringId); + }).ConfigureAwait(false); + + PingTimer = new System.Threading.Timer((e) => + { + SendPing(); + }, null, TimeSpan.FromSeconds(50), TimeSpan.FromSeconds(50)); + } + + private async Task SetupConnectionSettings(string stringId) + { + var cachedSettings = await CacheService.Instance.GetFromRedis(this.Id.ToString()); + if (cachedSettings != null) + { + try + { + MigrateSettings(cachedSettings); + this.LastSettingsChange = cachedSettings; + UpdateConnectionTier(cachedSettings); + await SendAuthorizedHello(cachedSettings); + // set them again + this.LastSettingsChange = cachedSettings; + SendMessage(COFLNET + $"§fFound and loaded settings for your connection\n" + + $"{McColorCodes.GRAY} MinProfit: {McColorCodes.AQUA}{FormatPrice(Settings.MinProfit)} " + + $"{McColorCodes.GRAY} MaxCost: {McColorCodes.AQUA}{FormatPrice(Settings.MaxCost)}" + + $"{McColorCodes.GRAY} Blacklist-Size: {McColorCodes.AQUA}{Settings?.BlackList?.Count ?? 0}\n " + + (Settings.BasedOnLBin ? $"{McColorCodes.RED} Your profit is based on Lowest bin, please note that this is NOT the intended way to use this\n " : "") + + $"{McColorCodes.AQUA}: click this if you want to change a setting \n" + + "§8: nothing else to do have a nice day :)", + "https://sky.coflnet.com/flipper"); + Console.WriteLine($"loaded settings for {this.sessionId} " + JsonConvert.SerializeObject(cachedSettings)); + await Task.Delay(500); + SendMessage(COFLNET + $"{McColorCodes.DARK_GREEN} click this to relink your account", + GetAuthLink(stringId), "You don't need to relink your account. \nThis is only here to allow you to link your mod to the website again should you notice your settings aren't updated"); + return; + } + catch (Exception e) + { + Error(e, "loading modsocket"); + SendMessage(COFLNET + $"Your settings could not be loaded, please relink again :)"); + } + } + var index = 1; + while (true) + { + SendMessage(COFLNET + "§lPlease click this [LINK] to login and configure your flip filters §8(you won't receive real time flips until you do)", + GetAuthLink(stringId)); + await Task.Delay(TimeSpan.FromSeconds(60 * index)); + + if (Settings != DEFAULT_SETTINGS) + return; + SendMessage("do /cofl stop to stop receiving this (or click this message)", "/cofl stop"); + } + } + + private static void MigrateSettings(SettingsChange cachedSettings) + { + var currentVersion = 3; + if (cachedSettings.Version >= currentVersion) + return; + if (cachedSettings.Settings.AllowedFinders == LowPricedAuction.FinderType.UNKOWN) + cachedSettings.Settings.AllowedFinders = LowPricedAuction.FinderType.FLIPPER; + cachedSettings.Version = currentVersion; + } + + private string GetAuthLink(string stringId) + { + return $"https://sky.coflnet.com/authmod?mcid={McId}&conId={HttpUtility.UrlEncode(stringId)}"; + } + + private async Task SendAuthorizedHello(SettingsChange cachedSettings) + { + var player = await PlayerService.Instance.GetPlayer(this.McId); + var mcName = this.McId.Length == 32 ? player?.Name : this.McId; + McUuid = player.UuId; + var user = UserService.Instance.GetUserById(cachedSettings.UserId); + var length = user.Email.Length < 10 ? 3 : 6; + var builder = new StringBuilder(user.Email); + for (int i = 0; i < builder.Length - 5; i++) + { + if (builder[i] == '@' || i < 3) + continue; + builder[i] = '*'; + } + var anonymisedEmail = builder.ToString(); + var messageStart = $"Hello {mcName} ({anonymisedEmail}) \n"; + if (cachedSettings.Tier != AccountTier.NONE && cachedSettings.ExpiresAt > DateTime.Now) + SendMessage(COFLNET + messageStart + $"You have {cachedSettings.Tier.ToString()} until {cachedSettings.ExpiresAt}"); + else + SendMessage(COFLNET + messageStart + $"You use the free version of the flip finder"); + + await Task.Delay(300); + } + + private void SendPing() + { + using var span = tracer.BuildSpan("ping").AsChildOf(ConSpan.Context).WithTag("count", blockedFlipFilterCount).StartActive(); + try + { + if (blockedFlipFilterCount > 0) + { + SendMessage(COFLNET + $"there were {blockedFlipFilterCount} flips blocked by your filter the last minute");//, "/cofl blocked", "click to list the best 5 of the last min"); + } + else + { + Send(Response.Create("ping", 0)); + + UpdateConnectionTier(LastSettingsChange); + } + } + catch (Exception e) + { + span.Span.Log("could not send ping"); + CloseBecauseError(e); + } + } + + protected (long, string) ComputeConnectionId() + { + var bytes = Encoding.UTF8.GetBytes(McId.ToLower() + sessionId + DateTime.Now.Date.ToString()); + var hash = System.Security.Cryptography.SHA512.Create(); + var hashed = hash.ComputeHash(bytes); + return (BitConverter.ToInt64(hashed), Convert.ToBase64String(hashed, 0, 16).Replace('+', '-').Replace('/', '_')); + } + + int waiting = 0; + + protected override void OnMessage(MessageEventArgs e) + { + if (waiting > 2) + { + SendMessage(COFLNET + $"You are executing to many commands please wait a bit"); + return; + } + using var span = tracer.BuildSpan("Command").AsChildOf(ConSpan.Context).StartActive(); + + var a = JsonConvert.DeserializeObject(e.Data); + if (a == null || a.type == null) + { + Send(new Response("error", "the payload has to have the property 'type'")); + return; + } + span.Span.SetTag("type", a.type); + span.Span.SetTag("content", a.data); + if (sessionId.StartsWith("debug")) + SendMessage("executed " + a.data, ""); + + // block click commands for now + if (a.type == "tokenLogin" || a.type == "clicked") + return; + + if (!Commands.TryGetValue(a.type.ToLower(), out McCommand command)) + { + SendMessage($"The command '{a.type}' is not know. Please check your spelling ;)"); + return; + } + + Task.Run(async () => + { + waiting++; + try + { + await command.Execute(this, a.data); + } + catch (Exception ex) + { + var id = Error(ex, "mod command"); + SendMessage(COFLNET + $"An error occured why processing your command. The error was recorded and will be investigated soon. You can reffer to it by {id}", "http://" + id, "click to open the id as link (and be able to copy)"); + } + finally + { + waiting--; + } + }); + } + + protected override void OnClose(CloseEventArgs e) + { + base.OnClose(e); + FlipperService.Instance.RemoveConnection(this); + ConSpan.Log(e?.Reason); + + ConSpan.Finish(); + } + + public void SendMessage(string text, string clickAction = null, string hoverText = null) + { + if (ConnectionState != WebSocketState.Open) + { + OpenTracing.IScope span = RemoveMySelf(); + return; + } + try + { + this.Send(Response.Create("writeToChat", new { text, onClick = clickAction, hover = hoverText })); + } + catch (Exception e) + { + CloseBecauseError(e); + } + } + + private OpenTracing.IScope RemoveMySelf() + { + var span = tracer.BuildSpan("removing").AsChildOf(ConSpan).StartActive(); + FlipperService.Instance.RemoveConnection(this); + PingTimer.Dispose(); + return span; + } + + public void SendMessage(params ChatPart[] parts) + { + if (ConnectionState != WebSocketState.Open) + { + RemoveMySelf(); + return; + } + try + { + this.ModAdapter.SendMessage(parts); + } + catch (Exception e) + { + CloseBecauseError(e); + } + } + + public void SendSound(string soundId, float pitch = 1f) + { + ModAdapter.SendSound(soundId, pitch); + } + + private OpenTracing.IScope CloseBecauseError(Exception e) + { + dev.Logger.Instance.Log("removing connection because " + e.Message); + dev.Logger.Instance.Error(System.Environment.StackTrace); + var span = tracer.BuildSpan("Disconnect").WithTag("error", "true").AsChildOf(ConSpan.Context).StartActive(); + span.Span.Log(e.Message); + OnClose(null); + PingTimer.Dispose(); + return span; + } + + private string Error(Exception exception, string message = null, string additionalLog = null) + { + using var error = tracer.BuildSpan("error").WithTag("message", message).WithTag("error", "true").StartActive(); + AddExceptionLog(error, exception); + if (additionalLog != null) + error.Span.Log(additionalLog); + + return error.Span.Context.TraceId; + } + + private void AddExceptionLog(OpenTracing.IScope error, Exception e) + { + error.Span.Log(e.Message); + error.Span.Log(e.StackTrace); + if (e.InnerException != null) + AddExceptionLog(error, e.InnerException); + } + + public void Send(Response response) + { + var json = JsonConvert.SerializeObject(response); + this.Send(json); + } + + public async Task SendFlip(LowPricedAuction flip) + { + try + { + if (base.ConnectionState != WebSocketState.Open) + return false; + // pre check already sent flips + if (SentFlips.ContainsKey(flip.UId)) + return true; // don't double send + if (Settings.AllowedFinders != LowPricedAuction.FinderType.UNKOWN && flip.Finder != LowPricedAuction.FinderType.UNKOWN + && !Settings.AllowedFinders.HasFlag(flip.Finder) + && (int)flip.Finder != 3) + { + BlockedFlip(flip, "finder " + flip.Finder.ToString()); + return true; + } + if (!flip.Auction.Bin) // no nonbin + return true; + + if (flip.AdditionalProps.ContainsKey("sold")) + { + BlockedFlip(flip, "sold"); + return true; + } + var isMatch = (false, ""); + var flipInstance = FlipperService.LowPriceToFlip(flip); + try + { + isMatch = Settings.MatchesSettings(flipInstance); + if (flip.AdditionalProps == null) + flip.AdditionalProps = new Dictionary(); + flip.AdditionalProps["match"] = isMatch.Item2; + } + catch (Exception e) + { + var id = Error(e, "matching flip settings", JSON.Stringify(flip) + "\n" + JSON.Stringify(Settings)); + dev.Logger.Instance.Error(e, "minecraft socket flip settings matching " + id); + BlockedFlip(flip, "Error " + e.Message); + } + if (Settings != null && !isMatch.Item1) + { + BlockedFlip(flip, isMatch.Item2); + return true; + } + + // this check is down here to avoid filling up the list + if (!SentFlips.TryAdd(flip.UId, DateTime.Now)) + return true; // make sure flips are not sent twice + using var span = tracer.BuildSpan("Flip").WithTag("uuid", flipInstance.Uuid).AsChildOf(ConSpan.Context).StartActive(); + var settings = Settings; + await FlipperService.FillVisibilityProbs(flipInstance, settings); + + await ModAdapter.SendFlip(flipInstance); + + span.Span.Log("sent"); + LastSent.Enqueue(flip); + sentFlipsCount.Inc(); + + PingTimer.Change(TimeSpan.FromSeconds(20), TimeSpan.FromSeconds(55)); + await FlipTrackingService.Instance.ReceiveFlip(flip.Auction.Uuid, McUuid); + span.Span.Log("after inc"); + + // remove dupplicates + if (SentFlips.Count > 300) + { + foreach (var item in SentFlips.Where(i => i.Value < DateTime.Now - TimeSpan.FromMinutes(2)).ToList()) + { + SentFlips.TryRemove(item.Key, out DateTime value); + } + } + if (LastSent.Count > 30) + LastSent.TryDequeue(out _); + } + catch (Exception e) + { + Error(e, "sending flip"); + return false; + } + return true; + } + + private void BlockedFlip(LowPricedAuction flip, string reason) + { + + TopBlocked.Enqueue(new BlockedElement() + { + Flip = flip, + Reason = reason + }); + } + + public string GetFlipMsg(FlipInstance flip) + { + Settings.GetPrice(flip, out long targetPrice, out long profit); + var priceColor = GetProfitColor((int)profit); + var a = flip.Auction; + var textAfterProfit = (Settings?.Visibility?.ProfitPercentage ?? false) ? $" {McColorCodes.DARK_RED}{FormatPrice((profit * 100 / a.StartingBid))}%{priceColor}" : ""; + + var builder = new StringBuilder(80); + + builder.Append($"\n{(flip.Finder.HasFlag(LowPricedAuction.FinderType.SNIPER) ? "SNIPE" : "FLIP")}: {GetRarityColor(a.Tier)}{a.ItemName} {priceColor}{FormatPrice(a.StartingBid)} -> {FormatPrice(targetPrice)} " + + $"(+{FormatPrice(profit)}{textAfterProfit}) §g"); + /* tmp deactivated if (Settings.Visibility?.MedianPrice ?? false) + builder.Append(McColorCodes.GRAY + " Med: " + McColorCodes.AQUA + FormatPrice(flip.MedianPrice)); + if (Settings.Visibility?.LowestBin ?? false) + builder.Append(McColorCodes.GRAY + " LBin: " + McColorCodes.AQUA + FormatPrice(flip.LowestBin ?? 0)); */ + if (Settings.Visibility?.Volume ?? false) + builder.Append(McColorCodes.GRAY + " Vol: " + McColorCodes.AQUA + flip.Volume.ToString("0.#")); + return builder.ToString(); + } + + public string GetRarityColor(Tier rarity) + { + return rarity switch + { + Tier.COMMON => "§f", + Tier.EPIC => "§5", + Tier.UNCOMMON => "§a", + Tier.RARE => "§9", + Tier.SPECIAL => "§c", + Tier.SUPREME => "§4", + Tier.VERY_SPECIAL => "§4", + Tier.LEGENDARY => "§6", + Tier.MYTHIC => "§d", + _ => "" + }; + } + + public string GetProfitColor(int profit) + { + if (profit >= 50_000_000) + return McColorCodes.GOLD; + if (profit >= 10_000_000) + return McColorCodes.AQUA; + if (profit >= 1_000_000) + return McColorCodes.GREEN; + if (profit >= 100_000) + return McColorCodes.DARK_GREEN; + return McColorCodes.DARK_GRAY; + } + + public string FormatPrice(long price) + { + if (Settings.ModSettings?.ShortNumbers ?? false) + return FormatPriceShort(price); + return string.Format("{0:n0}", price); + } + + /// + /// By RenniePet on Stackoverflow + /// https://stackoverflow.com/a/30181106 + /// + /// + /// + private static string FormatPriceShort(long num) + { + if (num <= 0) // there was an issue with flips attempting to be devided by 0 + return "0"; + // Ensure number has max 3 significant digits (no rounding up can happen) + long i = (long)Math.Pow(10, (int)Math.Max(0, Math.Log10(num) - 2)); + num = num / i * i; + + if (num >= 1000000000) + return (num / 1000000000D).ToString("0.##") + "B"; + if (num >= 1000000) + return (num / 1000000D).ToString("0.##") + "M"; + if (num >= 1000) + return (num / 1000D).ToString("0.##") + "k"; + + return num.ToString("#,0"); + } + + public Task SendSold(string uuid) + { + if (base.ConnectionState != WebSocketState.Open) + return Task.FromResult(false); + // don't send extra messages + return Task.FromResult(true); + } + + public void UpdateSettings(SettingsChange settings) + { + var settingsSame = AreSettingsTheSame(settings); + using var span = tracer.BuildSpan("SettingsUpdate").AsChildOf(ConSpan.Context) + .WithTag("premium", settings.Tier.ToString()) + .WithTag("userId", settings.UserId.ToString()) + .StartActive(); + if (this.Settings == DEFAULT_SETTINGS) + { + Task.Run(async () => await ModGotAuthorised(settings)); + } + else if (!settingsSame) + { + var changed = FindWhatsNew(this.Settings, settings.Settings); + SendMessage($"setting changed " + changed); + span.Span.Log(changed); + } + LastSettingsChange = settings; + UpdateConnectionTier(settings); + + CacheService.Instance.SaveInRedis(this.Id.ToString(), settings, TimeSpan.FromDays(3)); + span.Span.Log(JSON.Stringify(settings)); + } + + public Task UpdateSettings(Func updatingFunc) + { + var newSettings = updatingFunc(this.LastSettingsChange); + return FlipperService.Instance.UpdateSettings(newSettings); + } + + private async Task ModGotAuthorised(SettingsChange settings) + { + var span = tracer.BuildSpan("Authorized").AsChildOf(ConSpan.Context).StartActive(); + try + { + await SendAuthorizedHello(settings); + SendMessage($"Authorized connection you can now control settings via the website"); + await Task.Delay(TimeSpan.FromSeconds(20)); + SendMessage($"Remember: the format of the flips is: §dITEM NAME §fCOST -> MEDIAN"); + } + catch (Exception e) + { + Error(e, "settings authorization"); + span.Span.Log(e.Message); + } + + //await Task.Delay(TimeSpan.FromMinutes(2)); + try + { + await CheckVerificationStatus(settings); + } + catch (Exception e) + { + Error(e, "verification failed"); + } + + return span; + } + + private async Task CheckVerificationStatus(SettingsChange settings) + { + var connect = await McAccountService.Instance.ConnectAccount(settings.UserId.ToString(), McUuid); + if (connect.IsConnected) + return; + using var verification = tracer.BuildSpan("Verification").AsChildOf(ConSpan.Context).StartActive(); + var activeAuction = await ItemPrices.Instance.GetActiveAuctions(new ActiveItemSearchQuery() + { + name = "STICK", + }); + var bid = connect.Code; + var r = new Random(); + + var targetAuction = activeAuction.Where(a => a.Price < bid).OrderBy(x => r.Next()).FirstOrDefault(); + verification.Span.SetTag("code", bid); + verification.Span.Log(JSON.Stringify(activeAuction)); + verification.Span.Log(JSON.Stringify(targetAuction)); + + SendMessage(new ChatPart( + $"{COFLNET}You connected from an unkown account. Please verify that you are indeed {McId} by bidding {McColorCodes.AQUA}{bid}{McCommand.DEFAULT_COLOR} on a random auction.", + $"/viewauction {targetAuction?.Uuid}", + $"{McColorCodes.GRAY}Click to open an auction to bid {McColorCodes.AQUA}{bid}{McCommand.DEFAULT_COLOR} on\nyou can also bid another number with the same digits at the end\neg. 1,234,{McColorCodes.AQUA}{bid}")); + + } + + /// + /// Tests if the given settings are different from the current active ones + /// + /// + /// + private bool AreSettingsTheSame(SettingsChange settings) + { + return MessagePack.MessagePackSerializer.Serialize(settings.Settings).SequenceEqual(MessagePack.MessagePackSerializer.Serialize(Settings)); + } + + private void UpdateConnectionTier(SettingsChange settings) + { + if ((settings.Tier.HasFlag(AccountTier.PREMIUM) || settings.Tier.HasFlag(AccountTier.STARTER_PREMIUM)) && settings.ExpiresAt > DateTime.Now) + { + FlipperService.Instance.AddConnection(this, false); + NextUpdateStart -= SendTimer; + NextUpdateStart += SendTimer; + } + else + FlipperService.Instance.AddNonConnection(this, false); + this.ConSpan.SetTag("tier", settings.Tier.ToString()); + } + + private void SendTimer() + { + if (base.ConnectionState != WebSocketState.Open) + { + NextUpdateStart -= SendTimer; + return; + } + SendMessage( + COFLNET + "Flips in 10 seconds", + null, + "The Hypixel API will update in 10 seconds. Get ready to receive the latest flips. " + + "(this is an automated message being sent 50 seconds after the last update)"); + TopBlocked.Clear(); + } + + private string FindWhatsNew(FlipSettings current, FlipSettings newSettings) + { + try + { + if (current.MinProfit != newSettings.MinProfit) + return "min Profit to " + FormatPrice(newSettings.MinProfit); + if (current.MinProfit != newSettings.MinProfit) + return "max Cost to " + FormatPrice(newSettings.MaxCost); + if (current.MinProfitPercent != newSettings.MinProfitPercent) + return "min profit percentage to " + FormatPrice(newSettings.MinProfitPercent); + if (current.BlackList?.Count < newSettings.BlackList?.Count) + return $"blacklisted item " + ItemDetails.TagToName(newSettings.BlackList?.Last()?.ItemTag); + if (current.WhiteList?.Count < newSettings.WhiteList?.Count) + return $"whitelisted item " + ItemDetails.TagToName(newSettings.BlackList?.Last()?.ItemTag); + if (current.Visibility != null) + foreach (var prop in current.Visibility?.GetType().GetFields()) + { + if (prop.GetValue(current.Visibility).ToString() != prop.GetValue(newSettings.Visibility).ToString()) + { + return GetEnableMessage(newSettings.Visibility, prop); + } + } + if (current.ModSettings != null) + foreach (var prop in current.ModSettings?.GetType().GetFields()) + { + if (prop.GetValue(current.ModSettings).ToString() != prop.GetValue(newSettings.ModSettings).ToString()) + { + return GetEnableMessage(newSettings.ModSettings, prop); + } + } + } + catch (Exception e) + { + Error(e, "updating settings"); + } + + return ""; + } + + private static string GetEnableMessage(object newSettings, System.Reflection.FieldInfo prop) + { + if (prop.GetValue(newSettings).Equals(true)) + return prop.Name + " got enabled"; + return prop.Name + " was disabled"; + } + + public LowPricedAuction GetFlip(string uuid) + { + return LastSent.Where(s => s.Auction.Uuid == uuid).FirstOrDefault(); + } + + public Task SendFlip(FlipInstance flip) + { + var props = flip.Context; + if (props == null) + props = new Dictionary(); + if (flip.Sold) + props["sold"] = "y"; + return this.SendFlip(new LowPricedAuction() + { + Auction = flip.Auction, + DailyVolume = flip.Volume, + Finder = flip.Finder, + TargetPrice = flip.MedianPrice, + AdditionalProps = props + }); + } + } +} diff --git a/Commands/NextUpdateRetriever.cs b/Commands/NextUpdateRetriever.cs new file mode 100644 index 00000000..df0efe71 --- /dev/null +++ b/Commands/NextUpdateRetriever.cs @@ -0,0 +1,28 @@ +using System; +using System.Threading.Tasks; +using RestSharp; + +namespace Coflnet.Sky.Commands.MC +{ + public class NextUpdateRetriever + { + static RestClient client = new RestClient("http://" + SimplerConfig.SConfig.Instance["UPDATER_HOST"]); + public async Task Get() + { + try + { + DateTime last = default; + while (last < new DateTime(2020, 1, 1)) + { + last = (await client.ExecuteAsync(new RestRequest("/api/time"))).Data; + } + return last + TimeSpan.FromSeconds(61); + } + catch (Exception e) + { + dev.Logger.Instance.Error(e, "getting next update time"); + throw e; + } + } + } +} \ No newline at end of file diff --git a/Commands/Response.cs b/Commands/Response.cs new file mode 100644 index 00000000..afd56f40 --- /dev/null +++ b/Commands/Response.cs @@ -0,0 +1,26 @@ +using Newtonsoft.Json; + +namespace Coflnet.Sky.Commands.MC +{ + public class Response + { + public string type; + public string data; + + public Response() + { + } + + public Response(string type, string data) + { + this.type = type; + this.data = data; + } + + public static Response Create(string type, T data) + { + return new Response(type, JsonConvert.SerializeObject(data)); + } + + } +} \ No newline at end of file diff --git a/Commands/SecondVersionAdapter.cs b/Commands/SecondVersionAdapter.cs new file mode 100644 index 00000000..72d5ca4a --- /dev/null +++ b/Commands/SecondVersionAdapter.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using hypixel; +using System.Threading.Tasks; + +namespace Coflnet.Sky.Commands.MC +{ + public class SecondVersionAdapter : IModVersionAdapter + { + MinecraftSocket socket; + + public SecondVersionAdapter(MinecraftSocket socket) + { + this.socket = socket; + } + + public async Task SendFlip(FlipInstance flip) + { + var message = socket.GetFlipMsg(flip); + var openCommand = "/viewauction " + flip.Auction.Uuid; + var interesting = flip.Interesting; + var extraText = "\n" + String.Join(McColorCodes.DARK_GRAY + ", " + McColorCodes.WHITE, interesting.Take(socket.Settings.Visibility?.ExtraInfoMax ?? 0)); + + var uuid = flip.Auction.Uuid; + var seller = ""; + if (socket.Settings.Visibility.Seller) + seller = await PlayerSearch.Instance.GetNameWithCacheAsync(flip.Auction.AuctioneerId); + + var parts = new List(){ + new ChatPart(message, openCommand, string.Join('\n', interesting.Select(s => "・" + s)) + "\n" + seller), + new ChatPart(" [?]", "/cofl reference " + uuid, "Get reference auctions"), + new ChatPart(" ❤", $"/cofl rate {uuid} {flip.Finder} up", "Vote this flip up"), + new ChatPart("✖ ", $"/cofl rate {uuid} {flip.Finder} down", "Vote this flip down"), + new ChatPart(extraText, openCommand, null) + }; + + + if (socket.Settings.Visibility?.Seller ?? false && flip.SellerName != null) + { + parts.Insert(1,new ChatPart(McColorCodes.GRAY + " From: " + McColorCodes.AQUA + flip.SellerName, $"/ah {flip.SellerName}", $"{McColorCodes.GRAY}Open the ah for {McColorCodes.AQUA} {flip.SellerName}")); + } + + SendMessage(parts.ToArray()); + + if (socket.Settings.ModSettings?.PlaySoundOnFlip ?? false && flip.Profit > 1_000_000) + SendSound("note.pling", (float)(1 / (Math.Sqrt((float)flip.Profit / 1_000_000) + 1))); + return true; + } + + public void SendMessage(params ChatPart[] parts) + { + socket.Send(Response.Create("chatMessage", parts)); + } + + public void SendSound(string name, float pitch = 1) + { + socket.Send(Response.Create("playSound", new { name, pitch })); + } + } +} \ No newline at end of file diff --git a/Commands/ThirdVersionAdapter.cs b/Commands/ThirdVersionAdapter.cs new file mode 100644 index 00000000..7dd98e46 --- /dev/null +++ b/Commands/ThirdVersionAdapter.cs @@ -0,0 +1,50 @@ +using System.Linq; +using System.Threading.Tasks; +using hypixel; + +namespace Coflnet.Sky.Commands.MC +{ + public class ThirdVersionAdapter : IModVersionAdapter + { + MinecraftSocket socket; + + public ThirdVersionAdapter(MinecraftSocket socket) + { + this.socket = socket; + } + + public async Task SendFlip(FlipInstance flip) + { + var message = socket.GetFlipMsg(flip); + var openCommand = "/viewauction " + flip.Auction.Uuid; + var interesting = flip.Interesting; + var uuid = flip.Auction.Uuid; + + string seller = null; + if (socket.Settings.Visibility.Seller) + seller = await PlayerSearch.Instance.GetNameWithCacheAsync(flip.Auction.AuctioneerId); + socket.Send(Response.Create("flip", new + { + messages = new ChatPart[]{ + new ChatPart(message, openCommand, string.Join('\n', interesting.Select(s => "・" + s)) + "\n" + seller), + new ChatPart("?", "/cofl reference " + uuid, "Get reference auctions"), + new ChatPart(" ", openCommand, null)}, + id = uuid, + worth = flip.Profit, + cost = flip.Auction.StartingBid, + sound = (string)null + })); + return true; + } + + public void SendMessage(params ChatPart[] parts) + { + socket.Send(Response.Create("chatMessage", parts)); + } + + public void SendSound(string name, float pitch = 1) + { + socket.Send(Response.Create("playSound", new { name, pitch })); + } + } +} \ No newline at end of file diff --git a/Constants/McColorCodes.cs b/Constants/McColorCodes.cs new file mode 100644 index 00000000..cc549d40 --- /dev/null +++ b/Constants/McColorCodes.cs @@ -0,0 +1,23 @@ +namespace Coflnet.Sky.Commands.MC +{ + public class McColorCodes + { + public static readonly string BLACK = "§0"; + public static readonly string DARK_BLUE = "§1"; + public static readonly string DARK_GREEN = "§2"; + public static readonly string DARK_AQUA = "§3"; + public static readonly string DARK_RED = "§4"; + public static readonly string DARK_PURPLE = "§5"; + public static readonly string GOLD = "§6"; + public static readonly string GRAY = "§7"; + public static readonly string DARK_GRAY = "§8"; + public static readonly string BLUE = "§9"; + public static readonly string GREEN = "§a"; + public static readonly string AQUA = "§b"; + public static readonly string RED = "§c"; + public static readonly string LIGHT_PURPLE = "§d"; + public static readonly string YELLOW = "§e"; + public static readonly string WHITE = "§f"; + public static readonly string MINECOIN_GOLD = "§g"; + } +} \ No newline at end of file diff --git a/Controllers/StatsController.cs b/Controllers/StatsController.cs new file mode 100644 index 00000000..0d627f06 --- /dev/null +++ b/Controllers/StatsController.cs @@ -0,0 +1,39 @@ +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using Coflnet.Sky.ModCommands.Models; +using System; +using System.Linq; +using Microsoft.EntityFrameworkCore; +using System.Collections; +using System.Collections.Generic; +using Coflnet.Sky.ModCommands.Services; + +namespace Coflnet.Sky.ModCommands.Controllers +{ + /// + /// Main Controller handling tracking + /// + [ApiController] + [Route("[controller]")] + public class StatsController : ControllerBase + { + /// + /// Creates a new instance of + /// + public StatsController() + { + } + + /// + /// Indicates status of service, should be 200 (OK) + /// + /// + [HttpPost] + [Route("/status")] + public async Task TrackFlip() + { + + } + } +} diff --git a/Controllers/TrackerController.cs b/Controllers/TrackerController.cs deleted file mode 100644 index 87d8ceea..00000000 --- a/Controllers/TrackerController.cs +++ /dev/null @@ -1,48 +0,0 @@ -using System.Threading.Tasks; -using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Logging; -using Coflnet.Sky.Base.Models; -using System; -using System.Linq; -using Microsoft.EntityFrameworkCore; -using System.Collections; -using System.Collections.Generic; -using Coflnet.Sky.Base.Services; - -namespace Coflnet.Sky.Base.Controllers -{ - /// - /// Main Controller handling tracking - /// - [ApiController] - [Route("[controller]")] - public class BaseController : ControllerBase - { - private readonly BaseDbContext db; - private readonly BaseService service; - - /// - /// Creates a new instance of - /// - /// - public BaseController(BaseDbContext context, BaseService service) - { - db = context; - this.service = service; - } - - /// - /// Tracks a flip - /// - /// - /// - /// - [HttpPost] - [Route("flip/{AuctionId}")] - public async Task TrackFlip([FromBody] Flip flip, string AuctionId) - { - await service.AddFlip(flip); - return flip; - } - } -} diff --git a/Dockerfile b/Dockerfile index 05b2dc83..f02e7056 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,11 @@ FROM mcr.microsoft.com/dotnet/sdk:6.0 as build WORKDIR /build +RUN git clone --depth=1 https://github.com/Ekwav/websocket-sharp RUN git clone --depth=1 -b net6 https://github.com/Coflnet/HypixelSkyblock.git dev +RUN git clone --depth=1 https://github.com/Coflnet/SkyFilter.git +RUN git clone --depth=1 https://github.com/Coflnet/SkyBackendForFrontend.git WORKDIR /build/sky -COPY SkyBase.csproj SkyBase.csproj +COPY SkyModCommands.csproj SkyModCommands.csproj RUN dotnet restore COPY . . RUN dotnet publish -c release @@ -17,4 +20,4 @@ ENV ASPNETCORE_URLS=http://+:8000 RUN useradd --uid $(shuf -i 2000-65000 -n 1) app USER app -ENTRYPOINT ["dotnet", "SkyBase.dll", "--hostBuilder:reloadConfigOnChange=false"] +ENTRYPOINT ["dotnet", "SkyModCommands.dll", "--hostBuilder:reloadConfigOnChange=false"] diff --git a/Models/BaseDbContext.cs b/Models/BaseDbContext.cs index a07efc72..03190dcb 100644 --- a/Models/BaseDbContext.cs +++ b/Models/BaseDbContext.cs @@ -1,14 +1,12 @@ using Microsoft.EntityFrameworkCore; -namespace Coflnet.Sky.Base.Models +namespace Coflnet.Sky.ModCommands.Models { /// /// For flip tracking /// public class BaseDbContext : DbContext { - public DbSet Flips { get; set; } - /// /// Creates a new instance of /// @@ -25,11 +23,6 @@ public BaseDbContext(DbContextOptions options) protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); - - modelBuilder.Entity(entity => - { - entity.HasIndex(e => new { e.AuctionId }); - }); } } } \ No newline at end of file diff --git a/Models/Flip.cs b/Models/Flip.cs deleted file mode 100644 index b16669c4..00000000 --- a/Models/Flip.cs +++ /dev/null @@ -1,24 +0,0 @@ - -using System; -using System.Runtime.Serialization; -using System.Text.Json.Serialization; - -namespace Coflnet.Sky.Base.Models -{ - [DataContract] - public class Flip - { - [IgnoreDataMember] - [JsonIgnore] - public int Id { get; set; } - [DataMember(Name = "auctionId")] - public long AuctionId { get; set; } - [DataMember(Name = "targetPrice")] - public int TargetPrice { get; set; } - [DataMember(Name = "finderType")] - public LowPricedAuction.FinderType FinderType { get; set; } - [System.ComponentModel.DataAnnotations.Timestamp] - [DataMember(Name = "timestamp")] - public DateTime Timestamp { get; set; } - } -} \ No newline at end of file diff --git a/Program.cs b/Program.cs index 4f50361f..8ff3869c 100644 --- a/Program.cs +++ b/Program.cs @@ -1,12 +1,29 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Hosting; +using WebSocketSharp.Server; +using Coflnet.Sky.Commands.MC; +using System.Threading.Tasks; +using System; +using hypixel; -namespace Coflnet.Sky.Base +namespace Coflnet.Sky.ModCommands.MC { public class Program { public static void Main(string[] args) { + var server = new HttpServer(8008); + server.KeepClean = false; + server.AddWebSocketService("/modsocket"); + server.Start(); + + RunIsolatedForever(FlipperService.Instance.ListentoUnavailableTopics, "flip wait"); + RunIsolatedForever(FlipperService.Instance.ListenToNewFlips, "flip wait"); + RunIsolatedForever(FlipperService.Instance.ListenToLowPriced, "low priced auctions"); + RunIsolatedForever(FlipperService.Instance.ListenForSettingsChange, "settings sync"); + + RunIsolatedForever(FlipperService.Instance.ProcessSlowQueue, "flip process slow", 10); + CreateHostBuilder(args).Build().Run(); } @@ -16,5 +33,27 @@ public static IHostBuilder CreateHostBuilder(string[] args) => { webBuilder.UseStartup(); }); + + private static TaskFactory factory = new TaskFactory(); + public static void RunIsolatedForever(Func todo, string message, int backoff = 2000) + { + factory.StartNew(async () => + { + while (true) + { + try + { + await todo(); + } + catch (Exception e) + { + Console.WriteLine(); + Console.WriteLine($"{message}: {e.Message} {e.StackTrace}\n {e.InnerException?.Message} {e.InnerException?.StackTrace} {e.InnerException?.InnerException?.Message} {e.InnerException?.InnerException?.StackTrace}"); + await Task.Delay(2000); + } + await Task.Delay(backoff); + } + }, TaskCreationOptions.LongRunning).ConfigureAwait(false); + } } } diff --git a/Services/BaseBackgroundService.cs b/Services/BaseBackgroundService.cs index ef87ae7e..cca9c253 100644 --- a/Services/BaseBackgroundService.cs +++ b/Services/BaseBackgroundService.cs @@ -1,6 +1,6 @@ using System.Threading; using System.Threading.Tasks; -using Coflnet.Sky.Base.Models; +using Coflnet.Sky.ModCommands.Models; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; @@ -8,9 +8,9 @@ using Microsoft.Extensions.Configuration; using System; using Microsoft.Extensions.Logging; -using Coflnet.Sky.Base.Controllers; +using Coflnet.Sky.ModCommands.Controllers; -namespace Coflnet.Sky.Base.Services +namespace Coflnet.Sky.ModCommands.Services { public class BaseBackgroundService : BackgroundService @@ -33,28 +33,12 @@ public BaseBackgroundService( /// protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - using var scope = scopeFactory.CreateScope(); - var context = scope.ServiceProvider.GetRequiredService(); - // make sure all migrations are applied - await context.Database.MigrateAsync(); - - var flipCons = Coflnet.Kafka.KafkaConsumer.Consume(config["KAFKA_HOST"], config["TOPICS:LOW_PRICED"], async lp => - { - var service = GetService(); - await service.AddFlip(new Flip() - { - AuctionId = lp.UId, - FinderType = lp.Finder, - TargetPrice = lp.TargetPrice, - }); - }, stoppingToken, "flipbase"); - - await Task.WhenAll(flipCons); + return; } - private BaseService GetService() + private ModService GetService() { - return scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); + return scopeFactory.CreateScope().ServiceProvider.GetRequiredService(); } } } \ No newline at end of file diff --git a/Services/BaseService.cs b/Services/BaseService.cs index b6f64bb8..9a10dcdd 100644 --- a/Services/BaseService.cs +++ b/Services/BaseService.cs @@ -1,51 +1,13 @@ using System.Threading.Tasks; -using Coflnet.Sky.Base.Models; +using Coflnet.Sky.ModCommands.Models; using System; using System.Linq; using Microsoft.EntityFrameworkCore; -namespace Coflnet.Sky.Base.Services +namespace Coflnet.Sky.ModCommands.Services { - public class BaseService + public class ModService { - private BaseDbContext db; - public BaseService(BaseDbContext db) - { - this.db = db; - } - - public async Task AddFlip(Flip flip) - { - if (flip.Timestamp == default) - { - flip.Timestamp = DateTime.Now; - } - var flipAlreadyExists = await db.Flips.Where(f => f.AuctionId == flip.AuctionId && f.FinderType == flip.FinderType).AnyAsync(); - if (flipAlreadyExists) - { - return flip; - } - db.Flips.Add(flip); - await db.SaveChangesAsync(); - return flip; - } - - public async Task AddEvent(FlipEvent flipEvent) - { - if (flipEvent.Timestamp == default) - { - flipEvent.Timestamp = DateTime.Now; - } - var flipEventAlreadyExists = await db.FlipEvents.Where(f => f.AuctionId == flipEvent.AuctionId && f.Type == flipEvent.Type && f.PlayerId == flipEvent.PlayerId) - .AnyAsync(); - if (flipEventAlreadyExists) - { - return flipEvent; - } - db.FlipEvents.Add(flipEvent); - await db.SaveChangesAsync(); - return flipEvent; - } } } diff --git a/SkyBase.csproj b/SkyModCommands.csproj similarity index 66% rename from SkyBase.csproj rename to SkyModCommands.csproj index caf4167b..43202591 100644 --- a/SkyBase.csproj +++ b/SkyModCommands.csproj @@ -2,16 +2,30 @@ net6.0 + 1591 true false + false + + + $(DefaultItemExcludes);Client\**\* + + + + + + + + + diff --git a/Startup.cs b/Startup.cs index 5a2c250d..e5327ade 100644 --- a/Startup.cs +++ b/Startup.cs @@ -1,8 +1,8 @@ using System; using System.IO; using System.Reflection; -using Coflnet.Sky.Base.Models; -using Coflnet.Sky.Base.Services; +using Coflnet.Sky.ModCommands.Models; +using Coflnet.Sky.ModCommands.Services; using hypixel; using Jaeger.Samplers; using Jaeger.Senders; @@ -19,7 +19,7 @@ using OpenTracing.Util; using Prometheus; -namespace Coflnet.Sky.Base +namespace Coflnet.Sky.ModCommands { public class Startup { @@ -36,7 +36,7 @@ public void ConfigureServices(IServiceCollection services) services.AddControllers(); services.AddSwaggerGen(c => { - c.SwaggerDoc("v1", new OpenApiInfo { Title = "SkyBase", Version = "v1" }); + c.SwaggerDoc("v1", new OpenApiInfo { Title = "SkyModCommands", Version = "v1" }); // Set the comments path for the Swagger JSON and UI. var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml"; var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile); @@ -58,7 +58,7 @@ public void ConfigureServices(IServiceCollection services) ); services.AddHostedService(); services.AddJaeger(); - services.AddTransient(); + services.AddTransient(); } // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. @@ -71,7 +71,7 @@ public void Configure(IApplicationBuilder app, IWebHostEnvironment env) app.UseSwagger(); app.UseSwaggerUI(c => { - c.SwaggerEndpoint("/swagger/v1/swagger.json", "SkyBase v1"); + c.SwaggerEndpoint("/swagger/v1/swagger.json", "SkyModCommands v1"); c.RoutePrefix = "api"; }); diff --git a/appsettings.json b/appsettings.json index 56e23295..768e4909 100644 --- a/appsettings.json +++ b/appsettings.json @@ -7,10 +7,17 @@ } }, "AllowedHosts": "*", - "DB_CONNECTION": "server=mariadb;user=root;password=takenfrombitnami;database=base", + "DB_CONNECTION": "server=mariadb;user=root;password=takenfrombitnami;database=test", "JAEGER_SAMPLER_TYPE": "ratelimiting", "KAFKA_HOST": "kafka", + "REDIS_HOST":"redis", + "SKYCOMMANDS_HOST": "commands", "TOPICS": { + "MISSING_AUCTION": "sky-canceledauction", + "SOLD_AUCTION": "sky-soldauction", + "AUCTION_ENDED": "sky-endedauction", + "FLIP": "sky-flip", + "SETTINGS_CHANGE": "sky-settings", "LOW_PRICED": "sky-lowpriced", "FLIP_EVENT": "sky-flipevent" },