using Discord; using Discord.WebSocket; using Microsoft.EntityFrameworkCore; using NadekoBot.Common; using NadekoBot.Common.Collections; using NadekoBot.Core.Services; using NadekoBot.Core.Services.Database.Models; using NadekoBot.Core.Services.Impl; using NadekoBot.Extensions; using SixLabors.Fonts; using SixLabors.ImageSharp; using SixLabors.ImageSharp.Drawing.Processing; using SixLabors.ImageSharp.PixelFormats; using SixLabors.ImageSharp.Processing; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading; using System.Threading.Tasks; using NadekoBot.Db; using Image = SixLabors.ImageSharp.Image; using Color = SixLabors.ImageSharp.Color; namespace NadekoBot.Modules.Gambling.Services { public class PlantPickService : INService { private readonly DbService _db; private readonly IBotStrings _strings; private readonly IImageCache _images; private readonly FontProvider _fonts; private readonly ICurrencyService _cs; private readonly CommandHandler _cmdHandler; private readonly NadekoRandom _rng; private readonly DiscordSocketClient _client; private readonly GamblingConfigService _gss; public readonly ConcurrentHashSet _generationChannels = new ConcurrentHashSet(); //channelId/last generation public ConcurrentDictionary LastGenerations { get; } = new ConcurrentDictionary(); private readonly SemaphoreSlim pickLock = new SemaphoreSlim(1, 1); public PlantPickService(DbService db, CommandHandler cmd, IBotStrings strings, IDataCache cache, FontProvider fonts, ICurrencyService cs, CommandHandler cmdHandler, DiscordSocketClient client, GamblingConfigService gss) { _db = db; _strings = strings; _images = cache.LocalImages; _fonts = fonts; _cs = cs; _cmdHandler = cmdHandler; _rng = new NadekoRandom(); _client = client; _gss = gss; cmd.OnMessageNoTrigger += PotentialFlowerGeneration; using (var uow = db.GetDbContext()) { var guildIds = client.Guilds.Select(x => x.Id).ToList(); var configs = uow.Set() .AsQueryable() .Include(x => x.GenerateCurrencyChannelIds) .Where(x => guildIds.Contains(x.GuildId)) .ToList(); _generationChannels = new ConcurrentHashSet(configs .SelectMany(c => c.GenerateCurrencyChannelIds.Select(obj => obj.ChannelId))); } } private string GetText(ulong gid, string key, params object[] rep) => _strings.GetText(key, gid, rep); public bool ToggleCurrencyGeneration(ulong gid, ulong cid) { bool enabled; using (var uow = _db.GetDbContext()) { var guildConfig = uow.GuildConfigsForId(gid, set => set.Include(gc => gc.GenerateCurrencyChannelIds)); var toAdd = new GCChannelId() { ChannelId = cid }; if (!guildConfig.GenerateCurrencyChannelIds.Contains(toAdd)) { guildConfig.GenerateCurrencyChannelIds.Add(toAdd); _generationChannels.Add(cid); enabled = true; } else { var toDelete = guildConfig.GenerateCurrencyChannelIds.FirstOrDefault(x => x.Equals(toAdd)); if (toDelete != null) { uow.Remove(toDelete); } _generationChannels.TryRemove(cid); enabled = false; } uow.SaveChanges(); } return enabled; } public IEnumerable GetAllGeneratingChannels() { using (var uow = _db.GetDbContext()) { var chs = uow.GuildConfigs.GetGeneratingChannels(); return chs; } } /// /// Get a random currency image stream, with an optional password sticked onto it. /// /// Optional password to add to top left corner. /// Stream of the currency image public Stream GetRandomCurrencyImage(string pass, out string extension) { // get a random currency image bytes var rng = new NadekoRandom(); var curImg = _images.Currency[rng.Next(0, _images.Currency.Count)]; if (string.IsNullOrWhiteSpace(pass)) { // determine the extension using (var img = Image.Load(curImg, out var format)) { extension = format.FileExtensions.FirstOrDefault() ?? "png"; } // return the image return curImg.ToStream(); } // get the image stream and extension var (s, ext) = AddPassword(curImg, pass); // set the out extension parameter to the extension we've got extension = ext; // return the image return s; } /// /// Add a password to the image. /// /// Image to add password to. /// Password to add to top left corner. /// Image with the password in the top left corner. private (Stream, string) AddPassword(byte[] curImg, string pass) { // draw lower, it looks better pass = pass.TrimTo(10, true).ToLowerInvariant(); using (var img = Image.Load(curImg, out var format)) { // choose font size based on the image height, so that it's visible var font = _fonts.NotoSans.CreateFont(img.Height / 12, FontStyle.Bold); img.Mutate(x => { // measure the size of the text to be drawing var size = TextMeasurer.Measure(pass, new RendererOptions(font, new PointF(0, 0))); // fill the background with black, add 5 pixels on each side to make it look better x.FillPolygon(Color.ParseHex("00000080"), new PointF(0, 0), new PointF(size.Width + 5, 0), new PointF(size.Width + 5, size.Height + 10), new PointF(0, size.Height + 10)); // draw the password over the background x.DrawText(pass, font, SixLabors.ImageSharp.Color.White, new PointF(0, 0)); }); // return image as a stream for easy sending return (img.ToStream(format), format.FileExtensions.FirstOrDefault() ?? "png"); } } private Task PotentialFlowerGeneration(IUserMessage imsg) { var msg = imsg as SocketUserMessage; if (msg == null || msg.Author.IsBot) return Task.CompletedTask; if (!(imsg.Channel is ITextChannel channel)) return Task.CompletedTask; if (!_generationChannels.Contains(channel.Id)) return Task.CompletedTask; var _ = Task.Run(async () => { try { var config = _gss.Data; var lastGeneration = LastGenerations.GetOrAdd(channel.Id, DateTime.MinValue); var rng = new NadekoRandom(); if (DateTime.UtcNow - TimeSpan.FromSeconds(config.Generation.GenCooldown) < lastGeneration) //recently generated in this channel, don't generate again return; var num = rng.Next(1, 101) + config.Generation.Chance * 100; if (num > 100 && LastGenerations.TryUpdate(channel.Id, DateTime.UtcNow, lastGeneration)) { var dropAmount = config.Generation.MinAmount; var dropAmountMax = config.Generation.MaxAmount; if (dropAmountMax > dropAmount) dropAmount = new NadekoRandom().Next(dropAmount, dropAmountMax + 1); if (dropAmount > 0) { var prefix = _cmdHandler.GetPrefix(channel.Guild.Id); var toSend = dropAmount == 1 ? GetText(channel.GuildId, "curgen_sn", config.Currency.Sign) + " " + GetText(channel.GuildId, "pick_sn", prefix) : GetText(channel.GuildId, "curgen_pl", dropAmount, config.Currency.Sign) + " " + GetText(channel.GuildId, "pick_pl", prefix); var pw = config.Generation.HasPassword ? GenerateCurrencyPassword().ToUpperInvariant() : null; IUserMessage sent; using (var stream = GetRandomCurrencyImage(pw, out var ext)) { sent = await channel.SendFileAsync(stream, $"currency_image.{ext}", toSend).ConfigureAwait(false); } await AddPlantToDatabase(channel.GuildId, channel.Id, _client.CurrentUser.Id, sent.Id, dropAmount, pw).ConfigureAwait(false); } } } catch { } }); return Task.CompletedTask; } /// /// Generate a hexadecimal string from 1000 to ffff. /// /// A hexadecimal string from 1000 to ffff private string GenerateCurrencyPassword() { // generate a number from 1000 to ffff var num = _rng.Next(4096, 65536); // convert it to hexadecimal return num.ToString("x4"); } public async Task PickAsync(ulong gid, ITextChannel ch, ulong uid, string pass) { await pickLock.WaitAsync(); try { long amount; ulong[] ids; using (var uow = _db.GetDbContext()) { // this method will sum all plants with that password, // remove them, and get messageids of the removed plants pass = pass?.Trim().TrimTo(10, hideDots: true).ToUpperInvariant(); // gets all plants in this channel with the same password var entries = uow.PlantedCurrency .AsQueryable() .Where(x => x.ChannelId == ch.Id && pass == x.Password) .ToList(); // sum how much currency that is, and get all of the message ids (so that i can delete them) amount = entries.Sum(x => x.Amount); ids = entries.Select(x => x.MessageId).ToArray(); // remove them from the database uow.RemoveRange(entries); if (amount > 0) { // give the picked currency to the user await _cs.AddAsync(uid, "Picked currency", amount, gamble: false); } uow.SaveChanges(); } try { // delete all of the plant messages which have just been picked var _ = ch.DeleteMessagesAsync(ids); } catch { } // return the amount of currency the user picked return amount; } finally { pickLock.Release(); } } public async Task SendPlantMessageAsync(ulong gid, IMessageChannel ch, string user, long amount, string pass) { try { // get the text var prefix = _cmdHandler.GetPrefix(gid); var msgToSend = GetText(gid, "planted", Format.Bold(user), amount + _gss.Data.Currency.Sign, prefix); if (amount > 1) msgToSend += " " + GetText(gid, "pick_pl", prefix); else msgToSend += " " + GetText(gid, "pick_sn", prefix); //get the image using (var stream = GetRandomCurrencyImage(pass, out var ext)) { // send it var msg = await ch.SendFileAsync(stream, $"img.{ext}", msgToSend).ConfigureAwait(false); // return sent message's id (in order to be able to delete it when it's picked) return msg.Id; } } catch { // if sending fails, return null as message id return null; } } public async Task PlantAsync(ulong gid, IMessageChannel ch, ulong uid, string user, long amount, string pass) { // normalize it - no more than 10 chars, uppercase pass = pass?.Trim().TrimTo(10, hideDots: true).ToUpperInvariant(); // has to be either null or alphanumeric if (!string.IsNullOrWhiteSpace(pass) && !pass.IsAlphaNumeric()) return false; // remove currency from the user who's planting if (await _cs.RemoveAsync(uid, "Planted currency", amount, gamble: false)) { // try to send the message with the currency image var msgId = await SendPlantMessageAsync(gid, ch, user, amount, pass).ConfigureAwait(false); if (msgId == null) { // if it fails it will return null, if it returns null, refund await _cs.AddAsync(uid, "Planted currency refund", amount, gamble: false); return false; } // if it doesn't fail, put the plant in the database for other people to pick await AddPlantToDatabase(gid, ch.Id, uid, msgId.Value, amount, pass).ConfigureAwait(false); return true; } // if user doesn't have enough currency, fail return false; } private async Task AddPlantToDatabase(ulong gid, ulong cid, ulong uid, ulong mid, long amount, string pass) { using (var uow = _db.GetDbContext()) { uow.PlantedCurrency.Add(new PlantedCurrency { Amount = amount, GuildId = gid, ChannelId = cid, Password = pass, UserId = uid, MessageId = mid, }); await uow.SaveChangesAsync(); } } } }