timely now has a 3 letter password by default. Configurable via .conf gamb

This commit is contained in:
Toastie (DCS Team) 2024-11-02 01:31:06 +13:00
parent 82f7c3be27
commit e47e619ef9
Signed by: toastie_t0ast
GPG key ID: 27F3B6855AFD40A4
9 changed files with 179 additions and 98 deletions

View file

@ -44,9 +44,6 @@ public static class UserXpExtensions
.CountAsyncLinqToDB() .CountAsyncLinqToDB()
+ 1; + 1;
public static void ResetGuildUserXp(this DbSet<UserXpStats> xps, ulong userId, ulong guildId)
=> xps.Delete(x => x.UserId == userId && x.GuildId == guildId);
public static void ResetGuildXp(this DbSet<UserXpStats> xps, ulong guildId) public static void ResetGuildXp(this DbSet<UserXpStats> xps, ulong guildId)
=> xps.Delete(x => x.GuildId == guildId); => xps.Delete(x => x.GuildId == guildId);

View file

@ -14,6 +14,12 @@ using System.Text;
using EllieBot.Modules.Gambling.Rps; using EllieBot.Modules.Gambling.Rps;
using EllieBot.Common.TypeReaders; using EllieBot.Common.TypeReaders;
using EllieBot.Modules.Patronage; 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; namespace EllieBot.Modules.Gambling;
@ -26,6 +32,7 @@ public partial class Gambling : GamblingModule<GamblingService>
private readonly NumberFormatInfo _enUsCulture; private readonly NumberFormatInfo _enUsCulture;
private readonly DownloadTracker _tracker; private readonly DownloadTracker _tracker;
private readonly GamblingConfigService _configService; private readonly GamblingConfigService _configService;
private readonly FontProvider _fonts;
private readonly IBankService _bank; private readonly IBankService _bank;
private readonly IRemindService _remind; private readonly IRemindService _remind;
private readonly GamblingTxTracker _gamblingTxTracker; private readonly GamblingTxTracker _gamblingTxTracker;
@ -38,6 +45,7 @@ public partial class Gambling : GamblingModule<GamblingService>
DiscordSocketClient client, DiscordSocketClient client,
DownloadTracker tracker, DownloadTracker tracker,
GamblingConfigService configService, GamblingConfigService configService,
FontProvider fonts,
IBankService bank, IBankService bank,
IRemindService remind, IRemindService remind,
IPatronageService patronage, IPatronageService patronage,
@ -58,6 +66,7 @@ public partial class Gambling : GamblingModule<GamblingService>
_enUsCulture.NumberGroupSeparator = ""; _enUsCulture.NumberGroupSeparator = "";
_tracker = tracker; _tracker = tracker;
_configService = configService; _configService = configService;
_fonts = fonts;
} }
public async Task<string> GetBalanceStringAsync(ulong userId) public async Task<string> GetBalanceStringAsync(ulong userId)
@ -151,6 +160,49 @@ public partial class Gambling : GamblingModule<GamblingService>
return; return;
} }
if (Config.Timely.RequirePassword)
{
var password = _service.GeneratePassword();
var img = new Image<Rgba32>(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) if (await _service.ClaimTimelyAsync(ctx.User.Id, period) is { } remainder)
{ {
// Get correct time form remainder // Get correct time form remainder

View file

@ -11,7 +11,7 @@ namespace EllieBot.Modules.Gambling.Common;
public sealed partial class GamblingConfig : ICloneable<GamblingConfig> public sealed partial class GamblingConfig : ICloneable<GamblingConfig>
{ {
[Comment("""DO NOT CHANGE""")] [Comment("""DO NOT CHANGE""")]
public int Version { get; set; } = 8; public int Version { get; set; } = 9;
[Comment("""Currency settings""")] [Comment("""Currency settings""")]
public CurrencyConfig Currency { get; set; } public CurrencyConfig Currency { get; set; }
@ -111,6 +111,11 @@ public partial class TimelyConfig
setting to 0 or less will disable this feature setting to 0 or less will disable this feature
""")] """)]
public int Cooldown { get; set; } = 24; 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] [Cloneable]

View file

@ -144,6 +144,11 @@ public sealed class GamblingConfigService : ConfigServiceBase<GamblingConfig>
ConfigPrinters.ToString, ConfigPrinters.ToString,
val => val >= 0); val => val >= 0);
AddParsedProp("timely.pass",
gs => gs.Timely.RequirePassword,
bool.TryParse,
ConfigPrinters.ToString);
Migrate(); Migrate();
} }
@ -167,22 +172,6 @@ public sealed class GamblingConfigService : ConfigServiceBase<GamblingConfig>
}); });
} }
if (data.Version < 5)
{
ModifyConfig(c =>
{
c.Version = 5;
});
}
if (data.Version < 6)
{
ModifyConfig(c =>
{
c.Version = 6;
});
}
if (data.Version < 7) if (data.Version < 7)
{ {
ModifyConfig(c => ModifyConfig(c =>
@ -199,5 +188,13 @@ public sealed class GamblingConfigService : ConfigServiceBase<GamblingConfig>
c.Waifu.Decay.UnclaimedDecayPercent = 0; c.Waifu.Decay.UnclaimedDecayPercent = 0;
}); });
} }
if (data.Version < 9)
{
ModifyConfig(c =>
{
c.Version = 9;
});
}
} }
} }

View file

@ -16,6 +16,7 @@ public class GamblingService : IEService, IReadyExecutor
private readonly DiscordSocketClient _client; private readonly DiscordSocketClient _client;
private readonly IBotCache _cache; private readonly IBotCache _cache;
private readonly GamblingConfigService _gss; private readonly GamblingConfigService _gss;
private readonly EllieRandom _rng;
private static readonly TypedKey<long> _curDecayKey = new("currency:last_decay"); private static readonly TypedKey<long> _curDecayKey = new("currency:last_decay");
@ -29,11 +30,19 @@ public class GamblingService : IEService, IReadyExecutor
_client = client; _client = client;
_cache = cache; _cache = cache;
_gss = gss; _gss = gss;
_rng = new EllieRandom();
} }
public Task OnReadyAsync() public Task OnReadyAsync()
=> Task.WhenAll(CurrencyDecayLoopAsync(), TransactionClearLoopAsync()); => 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() private async Task TransactionClearLoopAsync()
{ {
if (_client.ShardId != 0) if (_client.ShardId != 0)
@ -52,7 +61,7 @@ public class GamblingService : IEService, IReadyExecutor
var days = TimeSpan.FromDays(lifetime); var days = TimeSpan.FromDays(lifetime);
await using var uow = _db.GetDbContext(); await using var uow = _db.GetDbContext();
await uow.Set<CurrencyTransaction>() await uow.Set<CurrencyTransaction>()
.DeleteAsync(ct => ct.DateAdded == null || now - ct.DateAdded < days); .DeleteAsync(ct => ct.DateAdded == null || now - ct.DateAdded < days);
} }
catch (Exception ex) catch (Exception ex)
{ {
@ -90,11 +99,11 @@ public class GamblingService : IEService, IReadyExecutor
} }
Log.Information(""" Log.Information("""
--- Decaying users' currency --- --- Decaying users' currency ---
| decay: {ConfigDecayPercent}% | decay: {ConfigDecayPercent}%
| max: {MaxDecay} | max: {MaxDecay}
| threshold: {DecayMinTreshold} | threshold: {DecayMinTreshold}
""", """,
config.Decay.Percent * 100, config.Decay.Percent * 100,
maxDecay, maxDecay,
config.Decay.MinThreshold); config.Decay.MinThreshold);
@ -104,14 +113,14 @@ public class GamblingService : IEService, IReadyExecutor
var decay = (double)config.Decay.Percent; var decay = (double)config.Decay.Percent;
await uow.Set<DiscordUser>() await uow.Set<DiscordUser>()
.Where(x => x.CurrencyAmount > config.Decay.MinThreshold && x.UserId != _client.CurrentUser.Id) .Where(x => x.CurrencyAmount > config.Decay.MinThreshold && x.UserId != _client.CurrentUser.Id)
.UpdateAsync(old => new() .UpdateAsync(old => new()
{ {
CurrencyAmount = CurrencyAmount =
maxDecay > Sql.Round((old.CurrencyAmount * decay) - 0.5) maxDecay > Sql.Round((old.CurrencyAmount * decay) - 0.5)
? (long)(old.CurrencyAmount - Sql.Round((old.CurrencyAmount * decay) - 0.5)) ? (long)(old.CurrencyAmount - Sql.Round((old.CurrencyAmount * decay) - 0.5))
: old.CurrencyAmount - maxDecay : old.CurrencyAmount - maxDecay
}); });
await uow.SaveChangesAsync(); await uow.SaveChangesAsync();
@ -178,8 +187,9 @@ public class GamblingService : IEService, IReadyExecutor
public bool UserHasTimelyReminder(ulong userId) public bool UserHasTimelyReminder(ulong userId)
{ {
var db = _db.GetDbContext(); var db = _db.GetDbContext();
return db.GetTable<Reminder>().Any(x => x.UserId == userId return db.GetTable<Reminder>()
&& x.Type == ReminderType.Timely); .Any(x => x.UserId == userId
&& x.Type == ReminderType.Timely);
} }
public async Task RemoveAllTimelyClaimsAsync() public async Task RemoveAllTimelyClaimsAsync()

View file

@ -1,4 +1,6 @@
#nullable disable #nullable disable
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore; using Microsoft.EntityFrameworkCore;
using EllieBot.Common.ModuleBehaviors; using EllieBot.Common.ModuleBehaviors;
using EllieBot.Db.Models; using EllieBot.Db.Models;
@ -25,6 +27,7 @@ public class PlantPickService : IEService, IExecNoCommand
private readonly EllieRandom _rng; private readonly EllieRandom _rng;
private readonly DiscordSocketClient _client; private readonly DiscordSocketClient _client;
private readonly GamblingConfigService _gss; private readonly GamblingConfigService _gss;
private readonly GamblingService _gs;
private readonly ConcurrentHashSet<ulong> _generationChannels; private readonly ConcurrentHashSet<ulong> _generationChannels;
private readonly SemaphoreSlim _pickLock = new(1, 1); private readonly SemaphoreSlim _pickLock = new(1, 1);
@ -37,7 +40,8 @@ public class PlantPickService : IEService, IExecNoCommand
ICurrencyService cs, ICurrencyService cs,
CommandHandler cmdHandler, CommandHandler cmdHandler,
DiscordSocketClient client, DiscordSocketClient client,
GamblingConfigService gss) GamblingConfigService gss,
GamblingService gs)
{ {
_db = db; _db = db;
_strings = strings; _strings = strings;
@ -48,6 +52,7 @@ public class PlantPickService : IEService, IExecNoCommand
_rng = new(); _rng = new();
_client = client; _client = client;
_gss = gss; _gss = gss;
_gs = gs;
using var uow = db.GetDbContext(); using var uow = db.GetDbContext();
var guildIds = client.Guilds.Select(x => x.Id).ToList(); 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)); var toDelete = guildConfig.GenerateCurrencyChannelIds.FirstOrDefault(x => x.Equals(toAdd));
if (toDelete is not null) if (toDelete is not null)
uow.Remove(toDelete); uow.Remove(toDelete);
_generationChannels.TryRemove(cid); _generationChannels.TryRemove(cid);
enabled = false; enabled = false;
} }
@ -208,7 +214,7 @@ public class PlantPickService : IEService, IExecNoCommand
+ " " + " "
+ GetText(channel.GuildId, strs.pick_pl(prefix)); + 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; IUserMessage sent;
var (stream, ext) = await GetRandomCurrencyImageAsync(pw); var (stream, ext) = await GetRandomCurrencyImageAsync(pw);
@ -232,67 +238,44 @@ public class PlantPickService : IEService, IExecNoCommand
return Task.CompletedTask; return Task.CompletedTask;
} }
/// <summary>
/// Generate a hexadecimal string from 1000 to ffff.
/// </summary>
/// <returns>A hexadecimal string from 1000 to ffff</returns>
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<long> PickAsync( public async Task<long> PickAsync(
ulong gid, ulong gid,
ITextChannel ch, ITextChannel ch,
ulong uid, ulong uid,
string pass) 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<PlantedCurrency>()
.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 try
{ {
long amount; _ = ch.DeleteMessagesAsync(ids);
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<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, 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();
} }
catch { }
// return the amount of currency the user picked
return amount;
} }
public async Task<ulong?> SendPlantMessageAsync( public async Task<ulong?> SendPlantMessageAsync(

View file

@ -357,7 +357,7 @@ public partial class Xp : EllieModule<XpService>
if (!await PromptUserConfirmAsync(embed)) if (!await PromptUserConfirmAsync(embed))
return; return;
_service.XpReset(ctx.Guild.Id, userId); await _service.XpReset(ctx.Guild.Id, userId);
await Response().Confirm(strs.reset_user(userId)).SendAsync(); await Response().Confirm(strs.reset_user(userId)).SendAsync();
} }

View file

@ -20,6 +20,31 @@ using Image = SixLabors.ImageSharp.Image;
namespace EllieBot.Modules.Xp.Services; namespace EllieBot.Modules.Xp.Services;
public interface IUserService
{
Task<DiscordUser?> GetUserAsync(ulong userId);
}
public sealed class UserService : IUserService, IEService
{
private readonly DbService _db;
public UserService(DbService db)
{
_db = db;
}
public async Task<DiscordUser> GetUserAsync(ulong userId)
{
await using var uow = _db.GetDbContext();
var user = await uow
.GetTable<DiscordUser>()
.FirstOrDefaultAsyncLinqToDB(u => u.UserId == userId);
return user;
}
}
public class XpService : IEService, IReadyExecutor, IExecNoCommand public class XpService : IEService, IReadyExecutor, IExecNoCommand
{ {
private readonly DbService _db; 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(); await using var uow = _db.GetDbContext();
uow.Set<UserXpStats>().ResetGuildUserXp(userId, guildId); await uow.GetTable<UserXpStats>()
uow.SaveChanges(); .DeleteAsync(x => x.UserId == userId && x.GuildId == guildId);
} }
public void XpReset(ulong guildId) public void XpReset(ulong guildId)
@ -1637,6 +1662,15 @@ public class XpService : IEService, IReadyExecutor, IExecNoCommand
public bool IsShopEnabled() public bool IsShopEnabled()
=> _xpConfig.Data.Shop.IsEnabled; => _xpConfig.Data.Shop.IsEnabled;
public async Task<int> GetTotalGuildUsers(ulong requestGuildId, List<ulong>? guildUsers = null)
{
await using var ctx = _db.GetDbContext();
return await ctx.GetTable<UserXpStats>()
.Where(x => x.GuildId == requestGuildId
&& (guildUsers == null || guildUsers.Contains(x.UserId)))
.CountAsyncLinqToDB();
}
} }
public enum BuyResult public enum BuyResult

View file

@ -1,5 +1,5 @@
# DO NOT CHANGE # DO NOT CHANGE
version: 8 version: 9
# Currency settings # Currency settings
currency: currency:
# What is the emoji/character which represents the 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 # How often (in hours) can users claim currency with .timely command
# setting to 0 or less will disable this feature # setting to 0 or less will disable this feature
cooldown: 12 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. # How much will each user's owned currency decay over time.
decay: decay:
# Percentage of user's current currency which will be deducted every 24h. # Percentage of user's current currency which will be deducted every 24h.
@ -125,12 +127,13 @@ waifu:
# Settings for periodic waifu price decay. # Settings for periodic waifu price decay.
# Waifu price decays only if the waifu has no claimer. # Waifu price decays only if the waifu has no claimer.
decay: decay:
# Percentage (0 - 100) of the waifu value to reduce. # Unclaimed waifus will decay by this percentage (0 - 100).
# Set 0 to disable # 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$) # 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 unclaimedDecayPercent: 0
# Claimed waifus will decay by this percentage (0 - 100). # Claimed waifus will decay by this percentage (0 - 100).
# Default is 0 (disabled) # 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 claimedDecayPercent: 0
# How often to decay waifu values, in hours # How often to decay waifu values, in hours
hourInterval: 24 hourInterval: 24