diff --git a/src/EllieBot/Db/Extensions/UserXpExtensions.cs b/src/EllieBot/Db/Extensions/UserXpExtensions.cs index 97e3e0a..b06d98b 100644 --- a/src/EllieBot/Db/Extensions/UserXpExtensions.cs +++ b/src/EllieBot/Db/Extensions/UserXpExtensions.cs @@ -44,9 +44,6 @@ public static class UserXpExtensions .CountAsyncLinqToDB() + 1; - public static void ResetGuildUserXp(this DbSet xps, ulong userId, ulong guildId) - => xps.Delete(x => x.UserId == userId && x.GuildId == guildId); - public static void ResetGuildXp(this DbSet xps, ulong guildId) => xps.Delete(x => x.GuildId == guildId); diff --git a/src/EllieBot/Modules/Gambling/Gambling.cs b/src/EllieBot/Modules/Gambling/Gambling.cs index 0348cf3..930c709 100644 --- a/src/EllieBot/Modules/Gambling/Gambling.cs +++ b/src/EllieBot/Modules/Gambling/Gambling.cs @@ -14,6 +14,12 @@ using System.Text; using EllieBot.Modules.Gambling.Rps; using EllieBot.Common.TypeReaders; using EllieBot.Modules.Patronage; +using SixLabors.Fonts; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using Color = SixLabors.ImageSharp.Color; namespace EllieBot.Modules.Gambling; @@ -26,6 +32,7 @@ public partial class Gambling : GamblingModule private readonly NumberFormatInfo _enUsCulture; private readonly DownloadTracker _tracker; private readonly GamblingConfigService _configService; + private readonly FontProvider _fonts; private readonly IBankService _bank; private readonly IRemindService _remind; private readonly GamblingTxTracker _gamblingTxTracker; @@ -38,6 +45,7 @@ public partial class Gambling : GamblingModule DiscordSocketClient client, DownloadTracker tracker, GamblingConfigService configService, + FontProvider fonts, IBankService bank, IRemindService remind, IPatronageService patronage, @@ -58,6 +66,7 @@ public partial class Gambling : GamblingModule _enUsCulture.NumberGroupSeparator = " "; _tracker = tracker; _configService = configService; + _fonts = fonts; } public async Task GetBalanceStringAsync(ulong userId) @@ -151,6 +160,49 @@ public partial class Gambling : GamblingModule return; } + if (Config.Timely.RequirePassword) + { + var password = _service.GeneratePassword(); + + var img = new Image(100, 40); + + var font = _fonts.NotoSans.CreateFont(30); + var outlinePen = new SolidPen(Color.Black, 1f); + // draw password on the image + img.Mutate(x => + { + x.DrawText(new RichTextOptions(font) + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center, + FallbackFontFamilies = _fonts.FallBackFonts, + Origin = new(50, 20) + }, + password, + Brushes.Solid(Color.White), + outlinePen); + }); + using var stream = await img.ToStreamAsync(); + var captcha = await Response() + .Embed(_sender.CreateEmbed() + .WithOkColor() + .WithImageUrl("attachment://timely.png")) + .File(stream, "timely.png") + .SendAsync(); + try + { + var userInput = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id); + if (userInput?.ToLowerInvariant() != password?.ToLowerInvariant()) + { + return; + } + } + finally + { + _ = captcha.DeleteAsync(); + } + } + if (await _service.ClaimTimelyAsync(ctx.User.Id, period) is { } remainder) { // Get correct time form remainder diff --git a/src/EllieBot/Modules/Gambling/GamblingConfig.cs b/src/EllieBot/Modules/Gambling/GamblingConfig.cs index 5f6693a..5cef563 100644 --- a/src/EllieBot/Modules/Gambling/GamblingConfig.cs +++ b/src/EllieBot/Modules/Gambling/GamblingConfig.cs @@ -11,7 +11,7 @@ namespace EllieBot.Modules.Gambling.Common; public sealed partial class GamblingConfig : ICloneable { [Comment("""DO NOT CHANGE""")] - public int Version { get; set; } = 8; + public int Version { get; set; } = 9; [Comment("""Currency settings""")] public CurrencyConfig Currency { get; set; } @@ -111,6 +111,11 @@ public partial class TimelyConfig setting to 0 or less will disable this feature """)] public int Cooldown { get; set; } = 24; + + [Comment(""" + Whether the users are required to type a password when they do timely. + """)] + public bool RequirePassword { get; set; } = true; } [Cloneable] diff --git a/src/EllieBot/Modules/Gambling/GamblingConfigService.cs b/src/EllieBot/Modules/Gambling/GamblingConfigService.cs index c1ad447..0789d67 100644 --- a/src/EllieBot/Modules/Gambling/GamblingConfigService.cs +++ b/src/EllieBot/Modules/Gambling/GamblingConfigService.cs @@ -144,6 +144,11 @@ public sealed class GamblingConfigService : ConfigServiceBase ConfigPrinters.ToString, val => val >= 0); + AddParsedProp("timely.pass", + gs => gs.Timely.RequirePassword, + bool.TryParse, + ConfigPrinters.ToString); + Migrate(); } @@ -167,22 +172,6 @@ public sealed class GamblingConfigService : ConfigServiceBase }); } - if (data.Version < 5) - { - ModifyConfig(c => - { - c.Version = 5; - }); - } - - if (data.Version < 6) - { - ModifyConfig(c => - { - c.Version = 6; - }); - } - if (data.Version < 7) { ModifyConfig(c => @@ -199,5 +188,13 @@ public sealed class GamblingConfigService : ConfigServiceBase c.Waifu.Decay.UnclaimedDecayPercent = 0; }); } + + if (data.Version < 9) + { + ModifyConfig(c => + { + c.Version = 9; + }); + } } } \ No newline at end of file diff --git a/src/EllieBot/Modules/Gambling/GamblingService.cs b/src/EllieBot/Modules/Gambling/GamblingService.cs index f9a55bc..da76900 100644 --- a/src/EllieBot/Modules/Gambling/GamblingService.cs +++ b/src/EllieBot/Modules/Gambling/GamblingService.cs @@ -16,6 +16,7 @@ public class GamblingService : IEService, IReadyExecutor private readonly DiscordSocketClient _client; private readonly IBotCache _cache; private readonly GamblingConfigService _gss; + private readonly EllieRandom _rng; private static readonly TypedKey _curDecayKey = new("currency:last_decay"); @@ -29,11 +30,19 @@ public class GamblingService : IEService, IReadyExecutor _client = client; _cache = cache; _gss = gss; + _rng = new EllieRandom(); } public Task OnReadyAsync() => Task.WhenAll(CurrencyDecayLoopAsync(), TransactionClearLoopAsync()); + + public string GeneratePassword() + { + var num = _rng.Next((int)Math.Pow(31, 2), (int)Math.Pow(32, 3)); + return new kwum(num).ToString(); + } + private async Task TransactionClearLoopAsync() { if (_client.ShardId != 0) @@ -52,7 +61,7 @@ public class GamblingService : IEService, IReadyExecutor var days = TimeSpan.FromDays(lifetime); await using var uow = _db.GetDbContext(); await uow.Set() - .DeleteAsync(ct => ct.DateAdded == null || now - ct.DateAdded < days); + .DeleteAsync(ct => ct.DateAdded == null || now - ct.DateAdded < days); } catch (Exception ex) { @@ -90,11 +99,11 @@ public class GamblingService : IEService, IReadyExecutor } Log.Information(""" - --- Decaying users' currency --- - | decay: {ConfigDecayPercent}% - | max: {MaxDecay} - | threshold: {DecayMinTreshold} - """, + --- Decaying users' currency --- + | decay: {ConfigDecayPercent}% + | max: {MaxDecay} + | threshold: {DecayMinTreshold} + """, config.Decay.Percent * 100, maxDecay, config.Decay.MinThreshold); @@ -104,14 +113,14 @@ public class GamblingService : IEService, IReadyExecutor var decay = (double)config.Decay.Percent; await uow.Set() - .Where(x => x.CurrencyAmount > config.Decay.MinThreshold && x.UserId != _client.CurrentUser.Id) - .UpdateAsync(old => new() - { - CurrencyAmount = - maxDecay > Sql.Round((old.CurrencyAmount * decay) - 0.5) - ? (long)(old.CurrencyAmount - Sql.Round((old.CurrencyAmount * decay) - 0.5)) - : old.CurrencyAmount - maxDecay - }); + .Where(x => x.CurrencyAmount > config.Decay.MinThreshold && x.UserId != _client.CurrentUser.Id) + .UpdateAsync(old => new() + { + CurrencyAmount = + maxDecay > Sql.Round((old.CurrencyAmount * decay) - 0.5) + ? (long)(old.CurrencyAmount - Sql.Round((old.CurrencyAmount * decay) - 0.5)) + : old.CurrencyAmount - maxDecay + }); await uow.SaveChangesAsync(); @@ -178,8 +187,9 @@ public class GamblingService : IEService, IReadyExecutor public bool UserHasTimelyReminder(ulong userId) { var db = _db.GetDbContext(); - return db.GetTable().Any(x => x.UserId == userId - && x.Type == ReminderType.Timely); + return db.GetTable() + .Any(x => x.UserId == userId + && x.Type == ReminderType.Timely); } public async Task RemoveAllTimelyClaimsAsync() diff --git a/src/EllieBot/Modules/Gambling/PlantPick/PlantPickService.cs b/src/EllieBot/Modules/Gambling/PlantPick/PlantPickService.cs index 80019ba..01dfae0 100644 --- a/src/EllieBot/Modules/Gambling/PlantPick/PlantPickService.cs +++ b/src/EllieBot/Modules/Gambling/PlantPick/PlantPickService.cs @@ -1,4 +1,6 @@ #nullable disable +using LinqToDB; +using LinqToDB.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using EllieBot.Common.ModuleBehaviors; using EllieBot.Db.Models; @@ -25,6 +27,7 @@ public class PlantPickService : IEService, IExecNoCommand private readonly EllieRandom _rng; private readonly DiscordSocketClient _client; private readonly GamblingConfigService _gss; + private readonly GamblingService _gs; private readonly ConcurrentHashSet _generationChannels; private readonly SemaphoreSlim _pickLock = new(1, 1); @@ -37,7 +40,8 @@ public class PlantPickService : IEService, IExecNoCommand ICurrencyService cs, CommandHandler cmdHandler, DiscordSocketClient client, - GamblingConfigService gss) + GamblingConfigService gss, + GamblingService gs) { _db = db; _strings = strings; @@ -48,6 +52,7 @@ public class PlantPickService : IEService, IExecNoCommand _rng = new(); _client = client; _gss = gss; + _gs = gs; using var uow = db.GetDbContext(); var guildIds = client.Guilds.Select(x => x.Id).ToList(); @@ -87,6 +92,7 @@ public class PlantPickService : IEService, IExecNoCommand var toDelete = guildConfig.GenerateCurrencyChannelIds.FirstOrDefault(x => x.Equals(toAdd)); if (toDelete is not null) uow.Remove(toDelete); + _generationChannels.TryRemove(cid); enabled = false; } @@ -208,7 +214,7 @@ public class PlantPickService : IEService, IExecNoCommand + " " + GetText(channel.GuildId, strs.pick_pl(prefix)); - var pw = config.Generation.HasPassword ? GenerateCurrencyPassword().ToUpperInvariant() : null; + var pw = config.Generation.HasPassword ? _gs.GeneratePassword().ToUpperInvariant() : null; IUserMessage sent; var (stream, ext) = await GetRandomCurrencyImageAsync(pw); @@ -232,67 +238,44 @@ public class PlantPickService : IEService, IExecNoCommand 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(); + long amount; + ulong[] ids; + await 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, true)?.ToUpperInvariant(); + // gets all plants in this channel with the same password + var entries = await uow.GetTable() + .Where(x => x.ChannelId == ch.Id && pass == x.Password) + .DeleteWithOutputAsync(); + + if (!entries.Any()) + return 0; + + amount = entries.Sum(x => x.Amount); + ids = entries.Select(x => x.MessageId).ToArray(); + } + + if (amount > 0) + await _cs.AddAsync(uid, amount, new("currency", "collect")); + + try { - long amount; - ulong[] ids; - await 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, true).ToUpperInvariant(); - // gets all plants in this channel with the same password - var entries = uow.Set() - .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, amount, new("currency", "collect")); - await uow.SaveChangesAsync(); - } - - try - { - // delete all of the plant messages which have just been picked - _ = ch.DeleteMessagesAsync(ids); - } - catch { } - - // return the amount of currency the user picked - return amount; - } - finally - { - _pickLock.Release(); + _ = ch.DeleteMessagesAsync(ids); } + catch { } + + // return the amount of currency the user picked + return amount; } public async Task SendPlantMessageAsync( diff --git a/src/EllieBot/Modules/Xp/Xp.cs b/src/EllieBot/Modules/Xp/Xp.cs index 3f4c3f3..65dd0eb 100644 --- a/src/EllieBot/Modules/Xp/Xp.cs +++ b/src/EllieBot/Modules/Xp/Xp.cs @@ -357,7 +357,7 @@ public partial class Xp : EllieModule if (!await PromptUserConfirmAsync(embed)) return; - _service.XpReset(ctx.Guild.Id, userId); + await _service.XpReset(ctx.Guild.Id, userId); await Response().Confirm(strs.reset_user(userId)).SendAsync(); } diff --git a/src/EllieBot/Modules/Xp/XpService.cs b/src/EllieBot/Modules/Xp/XpService.cs index 96c7b1b..808b79a 100644 --- a/src/EllieBot/Modules/Xp/XpService.cs +++ b/src/EllieBot/Modules/Xp/XpService.cs @@ -20,6 +20,31 @@ using Image = SixLabors.ImageSharp.Image; namespace EllieBot.Modules.Xp.Services; +public interface IUserService +{ + Task GetUserAsync(ulong userId); +} + +public sealed class UserService : IUserService, IEService +{ + private readonly DbService _db; + + public UserService(DbService db) + { + _db = db; + } + + public async Task GetUserAsync(ulong userId) + { + await using var uow = _db.GetDbContext(); + var user = await uow + .GetTable() + .FirstOrDefaultAsyncLinqToDB(u => u.UserId == userId); + + return user; + } +} + public class XpService : IEService, IReadyExecutor, IExecNoCommand { private readonly DbService _db; @@ -1437,11 +1462,11 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand } } - public void XpReset(ulong guildId, ulong userId) + public async Task XpReset(ulong guildId, ulong userId) { - using var uow = _db.GetDbContext(); - uow.Set().ResetGuildUserXp(userId, guildId); - uow.SaveChanges(); + await using var uow = _db.GetDbContext(); + await uow.GetTable() + .DeleteAsync(x => x.UserId == userId && x.GuildId == guildId); } public void XpReset(ulong guildId) @@ -1637,6 +1662,15 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand public bool IsShopEnabled() => _xpConfig.Data.Shop.IsEnabled; + + public async Task GetTotalGuildUsers(ulong requestGuildId, List? guildUsers = null) + { + await using var ctx = _db.GetDbContext(); + return await ctx.GetTable() + .Where(x => x.GuildId == requestGuildId + && (guildUsers == null || guildUsers.Contains(x.UserId))) + .CountAsyncLinqToDB(); + } } public enum BuyResult diff --git a/src/EllieBot/data/gambling.yml b/src/EllieBot/data/gambling.yml index 65edcc0..ddae85e 100644 --- a/src/EllieBot/data/gambling.yml +++ b/src/EllieBot/data/gambling.yml @@ -1,5 +1,5 @@ # DO NOT CHANGE -version: 8 +version: 9 # Currency settings currency: # What is the emoji/character which represents the currency @@ -56,6 +56,8 @@ timely: # How often (in hours) can users claim currency with .timely command # setting to 0 or less will disable this feature cooldown: 12 + # Whether the users are required to type a password when they do timely. + requirePassword: true # How much will each user's owned currency decay over time. decay: # Percentage of user's current currency which will be deducted every 24h. @@ -125,12 +127,13 @@ waifu: # Settings for periodic waifu price decay. # Waifu price decays only if the waifu has no claimer. decay: - # Percentage (0 - 100) of the waifu value to reduce. - # Set 0 to disable + # Unclaimed waifus will decay by this percentage (0 - 100). + # Default is 0 (disabled) # For example if a waifu has a price of 500$, setting this value to 10 would reduce the waifu value by 10% (50$) unclaimedDecayPercent: 0 # Claimed waifus will decay by this percentage (0 - 100). # Default is 0 (disabled) + # For example if a waifu has a price of 500$, setting this value to 10 would reduce the waifu value by 10% (50$) claimedDecayPercent: 0 # How often to decay waifu values, in hours hourInterval: 24