simplified quota system

This commit is contained in:
Toastie 2025-03-30 16:16:57 +13:00
parent 7d8f61ecea
commit aba5c4fbfd
Signed by: toastie_t0ast
GPG key ID: 0861BE54AD481DC7
16 changed files with 166 additions and 366 deletions

View file

@ -32,6 +32,7 @@
- .notify will now let you know if you can't set a notify message due to a missing channel - .notify will now let you know if you can't set a notify message due to a missing channel
- `.say` will no longer reply - `.say` will no longer reply
- `.vote` and `.timely` will now show active bonuses - `.vote` and `.timely` will now show active bonuses
- `.lcha` (live channel) limit increased to 5
### Fixed ### Fixed
- Fixed `.antispamignore` restart persistence - Fixed `.antispamignore` restart persistence

View file

@ -145,7 +145,7 @@ public class ChatterBotService : IExecOnMessage, IReadyExecutor
if (!res.IsAllowed) if (!res.IsAllowed)
return false; return false;
if (!await _ps.LimitHitAsync(LimitedFeatureName.ChatBot, usrMsg.Author.Id, 2048 / 2)) if (!await _ps.LimitHitAsync("ai", guild.OwnerId, 1))
{ {
// limit exceeded // limit exceeded
return false; return false;
@ -156,14 +156,6 @@ public class ChatterBotService : IExecOnMessage, IReadyExecutor
if (response.TryPickT0(out var result, out var error)) if (response.TryPickT0(out var result, out var error))
{ {
// calculate the diff in case we overestimated user's usage
var inTokens = (result.TokensIn - 2048) / 2;
// add the output tokens to the limit
await _ps.LimitForceHit(LimitedFeatureName.ChatBot,
usrMsg.Author.Id,
(inTokens) + (result.TokensOut / 2 * 3));
await _sender.Response(channel) await _sender.Response(channel)
.Confirm(result.Text) .Confirm(result.Text)
.SendAsync(); .SendAsync();

View file

@ -36,11 +36,11 @@ public partial class Help
var result = await _service.SendMessageToPatronsAsync(tierAndHigher, message); var result = await _service.SendMessageToPatronsAsync(tierAndHigher, message);
await Response() await Response()
.Confirm(strs.patron_msg_sent( .Confirm(strs.patron_msg_sent(
Format.Code(tierAndHigher.ToString()), Format.Code(tierAndHigher.ToString()),
Format.Bold(result.Success.ToString()), Format.Bold(result.Success.ToString()),
Format.Bold(result.Failed.ToString()))) Format.Bold(result.Failed.ToString())))
.SendAsync(); .SendAsync();
} }
// [OwnerOnly] // [OwnerOnly]
@ -73,32 +73,24 @@ public partial class Help
var maybePatron = await _service.GetPatronAsync(user.Id); var maybePatron = await _service.GetPatronAsync(user.Id);
var quotaStats = await _service.LimitStats(user.Id);
var eb = CreateEmbed() var eb = CreateEmbed()
.WithAuthor(user) .WithAuthor(user)
.WithTitle(GetText(strs.patron_info)) .WithTitle(GetText(strs.patron_info))
.WithOkColor(); .WithOkColor();
if (quotaStats.Count == 0 || maybePatron is not { } patron) if (maybePatron is not { } patron)
{ {
eb.WithDescription(GetText(strs.no_quota_found)); eb.WithDescription("You don't have an active subscription");
} }
else else
{ {
eb.AddField(GetText(strs.tier), Format.Bold(patron.Tier.ToFullName()), true) eb.AddField(GetText(strs.tier), Format.Bold(patron.Tier.ToFullName()), true)
.AddField(GetText(strs.pledge), $"**{patron.Amount / 100.0f:N1}$**", true); .AddField(GetText(strs.pledge), $"**{patron.Amount / 100.0f:N1}$**", true);
if (patron.Tier != PatronTier.None) if (patron.Tier != PatronTier.None)
eb.AddField(GetText(strs.expires), eb.AddField(GetText(strs.expires),
patron.ValidThru.AddDays(1).ToShortAndRelativeTimestampTag(), patron.ValidThru.AddDays(1).ToShortAndRelativeTimestampTag(),
true); true);
eb.AddField(GetText(strs.quotas), "", false);
var text = GetQuotaList(quotaStats);
if (!string.IsNullOrWhiteSpace(text))
eb.AddField(GetText(strs.modules), text, true);
} }
@ -112,29 +104,5 @@ public partial class Help
await Response().Error(strs.cant_dm).SendAsync(); await Response().Error(strs.cant_dm).SendAsync();
} }
} }
private string GetQuotaList(
IReadOnlyDictionary<LimitedFeatureName, (int Cur, QuotaLimit Quota)> featureQuotaStats)
{
var text = string.Empty;
foreach (var (key, (cur, quota)) in featureQuotaStats)
{
text += $"\n\t`{key}`\n";
if (quota.QuotaPeriod == QuotaPer.PerHour)
text += $" {cur}/{(quota.Quota == -1 ? "" : quota.Quota)} {QuotaPeriodToString(quota.QuotaPeriod)}\n";
}
return text;
}
public string QuotaPeriodToString(QuotaPer per)
=> per switch
{
QuotaPer.PerHour => "per hour",
QuotaPer.PerDay => "per day",
QuotaPer.PerMonth => "per month",
QuotaPer.Total => "total",
_ => throw new ArgumentOutOfRangeException(nameof(per), per, null)
};
} }
} }

View file

@ -98,21 +98,21 @@ public sealed class PatronageService
try try
{ {
var dbPatron = await ctx.GetTable<PatronUser>() var dbPatron = await ctx.GetTable<PatronUser>()
.FirstOrDefaultAsync(x .FirstOrDefaultAsync(x
=> x.UniquePlatformUserId == subscriber.UniquePlatformUserId); => x.UniquePlatformUserId == subscriber.UniquePlatformUserId);
if (dbPatron is null) if (dbPatron is null)
{ {
// if the user is not in the database alrady // if the user is not in the database alrady
dbPatron = await ctx.GetTable<PatronUser>() dbPatron = await ctx.GetTable<PatronUser>()
.InsertWithOutputAsync(() => new() .InsertWithOutputAsync(() => new()
{ {
UniquePlatformUserId = subscriber.UniquePlatformUserId, UniquePlatformUserId = subscriber.UniquePlatformUserId,
UserId = subscriber.UserId, UserId = subscriber.UserId,
AmountCents = subscriber.Cents, AmountCents = subscriber.Cents,
LastCharge = lastChargeUtc, LastCharge = lastChargeUtc,
ValidThru = dateInOneMonth, ValidThru = dateInOneMonth,
}); });
// await tran.CommitAsync(); // await tran.CommitAsync();
@ -129,18 +129,18 @@ public sealed class PatronageService
// if his sub would end in teh future, extend it by one month. // if his sub would end in teh future, extend it by one month.
// if it's not, just add 1 month to the last charge date // if it's not, just add 1 month to the last charge date
await ctx.GetTable<PatronUser>() await ctx.GetTable<PatronUser>()
.Where(x => x.UniquePlatformUserId .Where(x => x.UniquePlatformUserId
== subscriber.UniquePlatformUserId) == subscriber.UniquePlatformUserId)
.UpdateAsync(old => new() .UpdateAsync(old => new()
{ {
UserId = subscriber.UserId, UserId = subscriber.UserId,
AmountCents = subscriber.Cents, AmountCents = subscriber.Cents,
LastCharge = lastChargeUtc, LastCharge = lastChargeUtc,
ValidThru = old.ValidThru >= todayDate ValidThru = old.ValidThru >= todayDate
// ? Sql.DateAdd(Sql.DateParts.Month, 1, old.ValidThru).Value // ? Sql.DateAdd(Sql.DateParts.Month, 1, old.ValidThru).Value
? old.ValidThru.AddMonths(1) ? old.ValidThru.AddMonths(1)
: dateInOneMonth, : dateInOneMonth,
}); });
dbPatron.UserId = subscriber.UserId; dbPatron.UserId = subscriber.UserId;
@ -158,14 +158,14 @@ public sealed class PatronageService
var cents = subscriber.Cents; var cents = subscriber.Cents;
// the user updated the pledge or changed the connected discord account // the user updated the pledge or changed the connected discord account
await ctx.GetTable<PatronUser>() await ctx.GetTable<PatronUser>()
.Where(x => x.UniquePlatformUserId == subscriber.UniquePlatformUserId) .Where(x => x.UniquePlatformUserId == subscriber.UniquePlatformUserId)
.UpdateAsync(old => new() .UpdateAsync(old => new()
{ {
UserId = subscriber.UserId, UserId = subscriber.UserId,
AmountCents = cents, AmountCents = cents,
LastCharge = lastChargeUtc, LastCharge = lastChargeUtc,
ValidThru = old.ValidThru, ValidThru = old.ValidThru,
}); });
var newPatron = dbPatron.Clone(); var newPatron = dbPatron.Clone();
newPatron.AmountCents = cents; newPatron.AmountCents = cents;
@ -192,19 +192,19 @@ public sealed class PatronageService
{ {
// if the subscription is refunded, Disable user's valid thru // if the subscription is refunded, Disable user's valid thru
var changedCount = await ctx.GetTable<PatronUser>() var changedCount = await ctx.GetTable<PatronUser>()
.Where(x => x.UniquePlatformUserId == patron.UniquePlatformUserId .Where(x => x.UniquePlatformUserId == patron.UniquePlatformUserId
&& x.ValidThru != expiredDate) && x.ValidThru != expiredDate)
.UpdateAsync(old => new() .UpdateAsync(old => new()
{ {
ValidThru = expiredDate ValidThru = expiredDate
}); });
if (changedCount == 0) if (changedCount == 0)
continue; continue;
var updated = await ctx.GetTable<PatronUser>() var updated = await ctx.GetTable<PatronUser>()
.Where(x => x.UniquePlatformUserId == patron.UniquePlatformUserId) .Where(x => x.UniquePlatformUserId == patron.UniquePlatformUserId)
.FirstAsync(); .FirstAsync();
await OnPatronRefunded(PatronUserToPatron(updated)); await OnPatronRefunded(PatronUserToPatron(updated));
} }
@ -218,8 +218,8 @@ public sealed class PatronageService
// is subscribed on multiple platforms // is subscribed on multiple platforms
// or if there are multiple users on the same platform who connected the same discord account?! // or if there are multiple users on the same platform who connected the same discord account?!
var users = await ctx.GetTable<PatronUser>() var users = await ctx.GetTable<PatronUser>()
.Where(x => x.UserId == userId) .Where(x => x.UserId == userId)
.ToListAsync(); .ToListAsync();
// first find all active subscriptions // first find all active subscriptions
// and return the one with the highest amount // and return the one with the highest amount
@ -236,136 +236,56 @@ public sealed class PatronageService
return PatronUserToPatron(max); return PatronUserToPatron(max);
} }
public async Task<bool> LimitHitAsync(LimitedFeatureName key, ulong userId, int amount = 1) private Func<string, ulong, TypedKey<int>> Limitkey
=> (name, userId) => new($"patron_limit:{userId}:{name}");
public async Task<bool> LimitHitAsync(string name, ulong userId, int defaultMax)
{ {
if (_creds.GetCreds().IsOwner(userId)) var data = _pConf.Data;
if (!data.IsEnabled)
return true; return true;
if (!_pConf.Data.IsEnabled) var limit = await GetUserLimit(name, userId, defaultMax);
if (limit == -1)
return true; return true;
var userLimit = await GetUserLimit(key, userId); var timeUntilTomorrow = (DateTime.UtcNow.Date.AddDays(1) - DateTime.UtcNow);
var soFar = await _cache.GetOrAddAsync(Limitkey(name, userId),
() => Task.FromResult(0),
expiry: timeUntilTomorrow);
if (userLimit.Quota == 0) if (soFar >= limit)
return false; return false;
if (userLimit.Quota == -1) await _cache.AddAsync(Limitkey(name, userId), soFar + 1, timeUntilTomorrow, overwrite: true);
return true; return true;
return await TryAddLimit(key, userLimit, userId, amount);
} }
public async Task<bool> LimitForceHit(LimitedFeatureName key, ulong userId, int amount) public async Task<int> GetUserLimit(string name, ulong userId, int defaultMax)
{ {
if (_creds.GetCreds().IsOwner(userId)) var data = _pConf.Data;
return true; if (!data.IsEnabled || _creds.GetCreds().OwnerIds.Contains(userId))
return defaultMax;
if (!_pConf.Data.IsEnabled) var mPatron = await GetPatronAsync(userId);
return true;
var userLimit = await GetUserLimit(key, userId); if (mPatron is not { } patron || !patron.IsActive)
var cacheKey = CreateKey(key, userId);
await _cache.GetOrAddAsync(cacheKey, () => Task.FromResult(0), GetExpiry(userLimit));
return await TryAddLimit(key, userLimit, userId, amount);
}
private async Task<bool> TryAddLimit(
LimitedFeatureName key,
QuotaLimit userLimit,
ulong userId,
int amount)
{
var cacheKey = CreateKey(key, userId);
var cur = await _cache.GetOrAddAsync(cacheKey, () => Task.FromResult(0), GetExpiry(userLimit));
if (cur + amount < userLimit.Quota)
{ {
await _cache.AddAsync(cacheKey, cur + amount); if (data.Quotas.TryGetValue(PatronTier.I, out var limits)
return true; && limits.TryGetValue(name, out var limit))
return limit;
return 0;
} }
return false; if (data.Quotas.TryGetValue(patron.Tier, out var plimits)
&& plimits.TryGetValue(name, out var plimit))
return plimit;
return 0;
} }
private TimeSpan? GetExpiry(QuotaLimit userLimit)
{
var now = DateTime.UtcNow;
switch (userLimit.QuotaPeriod)
{
case QuotaPer.PerHour:
return TimeSpan.FromMinutes(60 - now.Minute);
case QuotaPer.PerDay:
return TimeSpan.FromMinutes((24 * 60) - ((now.Hour * 60) + now.Minute));
case QuotaPer.PerMonth:
var firstOfNextMonth = now.FirstOfNextMonth();
return firstOfNextMonth - now;
default:
return null;
}
}
private TypedKey<int> CreateKey(LimitedFeatureName key, ulong userId)
=> new($"limited_feature:{key}:{userId}");
private readonly QuotaLimit _emptyQuota = new QuotaLimit()
{
Quota = 0,
QuotaPeriod = QuotaPer.PerDay,
};
private readonly QuotaLimit _infiniteQuota = new QuotaLimit()
{
Quota = -1,
QuotaPeriod = QuotaPer.PerDay,
};
public async Task<QuotaLimit> GetUserLimit(LimitedFeatureName name, ulong userId)
{
if (!_pConf.Data.IsEnabled)
return _infiniteQuota;
var maybePatron = await GetPatronAsync(userId);
if (maybePatron is not { } patron)
return _emptyQuota;
if (patron.ValidThru < DateTime.UtcNow)
return _emptyQuota;
foreach (var (key, value) in _pConf.Data.Limits)
{
if (patron.Amount >= key)
{
if (value.TryGetValue(name, out var quotaLimit))
{
return quotaLimit;
}
break;
}
}
return _emptyQuota;
}
public async Task<Dictionary<LimitedFeatureName, (int, QuotaLimit)>> LimitStats(ulong userId)
{
var dict = new Dictionary<LimitedFeatureName, (int, QuotaLimit)>();
foreach (var featureName in Enum.GetValues<LimitedFeatureName>())
{
var cacheKey = CreateKey(featureName, userId);
var userLimit = await GetUserLimit(featureName, userId);
var cur = await _cache.GetOrAddAsync(cacheKey, () => Task.FromResult(0), GetExpiry(userLimit));
dict[featureName] = (cur, userLimit);
}
return dict;
}
private Patron PatronUserToPatron(PatronUser user) private Patron PatronUserToPatron(PatronUser user)
=> new Patron() => new Patron()
{ {
@ -384,10 +304,13 @@ public sealed class PatronageService
return user.AmountCents switch return user.AmountCents switch
{ {
<= 200 => PatronTier.I, >= 10_000 => PatronTier.C,
<= 1_000 => PatronTier.C, >= 5_000 => PatronTier.L,
<= 5_000 => PatronTier.L, >= 2_000 => PatronTier.XX,
_ => PatronTier.None >= 1_000 => PatronTier.X,
>= 500 => PatronTier.V,
>= 100 => PatronTier.I,
_ => 0,
}; };
} }
@ -399,10 +322,12 @@ public sealed class PatronageService
public int PercentBonus(long amount) public int PercentBonus(long amount)
=> amount switch => amount switch
{ {
< 200 => 0, >= 10_000 => 100,
< 1_000 => 10, >= 5_000 => 50,
< 5_000 => 50, >= 2_000 => 25,
_ => 100 >= 1_000 => 10,
>= 500 => 5,
_ => 0,
}; };
private async Task SendWelcomeMessage(Patron patron) private async Task SendWelcomeMessage(Patron patron)
@ -414,28 +339,23 @@ public sealed class PatronageService
return; return;
var eb = _sender.CreateEmbed() var eb = _sender.CreateEmbed()
.WithOkColor() .WithOkColor()
.WithTitle("❤️ Thank you for supporting EllieBot! ❤️") .WithTitle("❤️ Thank you for supporting EllieBot! ❤️")
.WithDescription( .WithDescription(
"Your donation has been processed and you will receive the rewards shortly.\n" "Your donation has been processed and you will receive the rewards shortly.\n"
+ "You can visit <https://www.patreon.com/join/elliebot> to see rewards for your tier. 🎉") + "You can visit <https://www.patreon.com/join/elliebot> to see rewards for your tier. 🎉")
.AddField("Tier", Format.Bold(patron.Tier.ToString()), true) .AddField("Tier", Format.Bold(patron.Tier.ToString()), true)
.AddField("Pledge", $"**{patron.Amount / 100.0f:N1}$**", true) .AddField("Pledge", $"**{patron.Amount / 100.0f:N1}$**", true)
.AddField("Expires", .AddField("Expires",
patron.ValidThru.AddDays(1).ToShortAndRelativeTimestampTag(), patron.ValidThru.AddDays(1).ToShortAndRelativeTimestampTag(),
true) true)
.AddField("Instructions", .AddField("Instructions",
""" """
*- Within the next **1-2 minutes** you will have all of the benefits of the Tier you've subscribed to.* *- Within the next **1-2 minutes** you will have all of the benefits of the Tier you've subscribed to.*
*- You can check your benefits on <https://www.patreon.com/join/elliebot>* *- You can use the `.patron` command in this chat to check your current quota usage for the Patron-only commands*
*- You can use the `.patron` command in this chat to check your current quota usage for the Patron-only commands* """,
*- **ALL** of the servers that you **own** will enjoy your Patron benefits.* inline: false)
*- You can use any of the commands available in your tier on any server (assuming you have sufficient permissions to run those commands)* .WithFooter($"platform id: {patron.UniquePlatformUserId}");
*- Any user in any of your servers can use Patron-only commands, but they will spend **your quota**, which is why it's recommended to use Ellie's command cooldown system (.h .cmdcd) or permission system to limit the command usage for your server members.*
*- Permission guide can be found here if you're not familiar with it: <https://docs.elliebot.net/ellie/features/permissions-system/>*
""",
inline: false)
.WithFooter($"platform id: {patron.UniquePlatformUserId}");
await _sender.Response(user).Embed(eb).SendAsync(); await _sender.Response(user).Embed(eb).SendAsync();
} }
@ -450,8 +370,8 @@ public sealed class PatronageService
await using var ctx = _db.GetDbContext(); await using var ctx = _db.GetDbContext();
var patrons = await ctx.GetTable<PatronUser>() var patrons = await ctx.GetTable<PatronUser>()
.Where(x => x.ValidThru > DateTime.UtcNow) .Where(x => x.ValidThru > DateTime.UtcNow)
.ToArrayAsync(); .ToArrayAsync();
var text = SmartText.CreateFrom(message); var text = SmartText.CreateFrom(message);

View file

@ -13,10 +13,10 @@ public partial class Utility
[BotPerm(GuildPerm.ManageChannels)] [BotPerm(GuildPerm.ManageChannels)]
public async Task LiveChAdd(IChannel channel, [Leftover] string template) public async Task LiveChAdd(IChannel channel, [Leftover] string template)
{ {
if (!await svc.AddLiveChannelAsync(ctx.Guild.Id, channel.Id, template)) if (!await svc.AddLiveChannelAsync(ctx.Guild.Id, channel.Id, ctx.Guild.OwnerId, template))
{ {
await Response() await Response()
.Error(strs.livechannel_limit(LiveChannelService.MAX_LIVECHANNELS)) .Error(strs.livechannel_limit(await svc.GetMaxLiveChannels(ctx.Guild.OwnerId)))
.SendAsync(); .SendAsync();
return; return;
} }

View file

@ -4,6 +4,7 @@ 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;
using EllieBot.Modules.Patronage;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
namespace EllieBot.Modules.Utility.LiveChannel; namespace EllieBot.Modules.Utility.LiveChannel;
@ -15,9 +16,10 @@ public class LiveChannelService(
DbService db, DbService db,
DiscordSocketClient client, DiscordSocketClient client,
IReplacementService repSvc, IReplacementService repSvc,
IPatronageService patron,
ShardData shardData) : IReadyExecutor, IEService ShardData shardData) : IReadyExecutor, IEService
{ {
public const int MAX_LIVECHANNELS = 1; public const int DEFAULT_MAX_LIVECHANNELS = 5;
private readonly ConcurrentDictionary<ulong, ConcurrentDictionary<ulong, LiveChannelConfig>> _liveChannels = new(); private readonly ConcurrentDictionary<ulong, ConcurrentDictionary<ulong, LiveChannelConfig>> _liveChannels = new();
@ -122,23 +124,23 @@ public class LiveChannelService(
/// <param name="channelId">ID of the channel</param> /// <param name="channelId">ID of the channel</param>
/// <param name="template">Template text to use for the channel</param> /// <param name="template">Template text to use for the channel</param>
/// <returns>True if successfully added, false otherwise</returns> /// <returns>True if successfully added, false otherwise</returns>
public async Task<bool> AddLiveChannelAsync(ulong guildId, ulong channelId, string template) public async Task<bool> AddLiveChannelAsync(ulong guildId, ulong channelId, ulong guildOwnerId, string template)
{ {
var guildDict = _liveChannels.GetOrAdd( var guildDict = _liveChannels.GetOrAdd(
guildId, guildId,
_ => new()); _ => new());
if (!guildDict.ContainsKey(channelId) && guildDict.Count >= MAX_LIVECHANNELS) if (!guildDict.ContainsKey(channelId) && guildDict.Count >= await GetMaxLiveChannels(guildOwnerId))
return false; return false;
await using var uow = db.GetDbContext(); await using var uow = db.GetDbContext();
await uow.GetTable<LiveChannelConfig>() await uow.GetTable<LiveChannelConfig>()
.InsertOrUpdateAsync(() => new() .InsertOrUpdateAsync(() => new()
{ {
GuildId = guildId, GuildId = guildId,
ChannelId = channelId, ChannelId = channelId,
Template = template Template = template
}, },
(_) => new() (_) => new()
{ {
Template = template Template = template
@ -194,4 +196,11 @@ public class LiveChannelService(
.Where(x => x.GuildId == guildId) .Where(x => x.GuildId == guildId)
.ToListAsyncLinqToDB(); .ToListAsyncLinqToDB();
} }
public async Task<int> GetMaxLiveChannels(ulong guildOwnerId)
{
var limit = await patron.GetUserLimit("livechannels", guildOwnerId, DEFAULT_MAX_LIVECHANNELS);
return limit;
}
} }

View file

@ -236,7 +236,6 @@ public partial class Utility
} }
[Cmd]
[RequireContext(ContextType.Guild)] [RequireContext(ContextType.Guild)]
public async Task QuoteAdd(string keyword, [Leftover] string text) public async Task QuoteAdd(string keyword, [Leftover] string text)
{ {

View file

@ -1,12 +1,5 @@
namespace EllieBot.Modules.Patronage; namespace EllieBot.Modules.Patronage;
public enum LimitedFeatureName
{
ChatBot,
ReactionRole,
Prune,
}
public readonly struct FeatureLimitKey public readonly struct FeatureLimitKey
{ {
public string PrettyName { get; init; } public string PrettyName { get; init; }

View file

@ -30,12 +30,8 @@ public interface IPatronageService
/// <returns>A patron with the specifeid userId</returns> /// <returns>A patron with the specifeid userId</returns>
public Task<Patron?> GetPatronAsync(ulong userId); public Task<Patron?> GetPatronAsync(ulong userId);
Task<bool> LimitHitAsync(LimitedFeatureName key, ulong userId, int amount = 1); Task<bool> LimitHitAsync(string name, ulong userId, int def);
Task<bool> LimitForceHit(LimitedFeatureName key, ulong userId, int amount); Task<int> GetUserLimit(string name, ulong userId, int def);
Task<QuotaLimit> GetUserLimit(LimitedFeatureName name, ulong userId);
Task<Dictionary<LimitedFeatureName, (int, QuotaLimit)>> LimitStats(ulong userId);
PatronConfigData GetConfig(); PatronConfigData GetConfig();
int PercentBonus(Patron? user); int PercentBonus(Patron? user);
int PercentBonus(long amount); int PercentBonus(long amount);

View file

@ -12,6 +12,6 @@ public partial class PatronConfigData : ICloneable<PatronConfigData>
[Comment("Whether the patronage feature is enabled")] [Comment("Whether the patronage feature is enabled")]
public bool IsEnabled { get; set; } public bool IsEnabled { get; set; }
[Comment("Who can do how much of what")] [Comment("Quotas for patron system")]
public Dictionary<int, Dictionary<LimitedFeatureName, QuotaLimit>> Limits { get; set; } = new(); public Dictionary<PatronTier, Dictionary<string, int>> Quotas { get; set; } = new();
} }

View file

@ -5,7 +5,9 @@ public enum PatronTier
{ {
None, None,
I, I,
V,
X, X,
XX,
L, L,
C C
} }

View file

@ -1,23 +0,0 @@
namespace EllieBot.Modules.Patronage;
/// <summary>
/// Represents information about why the user has triggered a quota limit
/// </summary>
public readonly struct QuotaLimit
{
/// <summary>
/// Amount of usages reached, which is the limit
/// </summary>
public int Quota { get; init; }
/// <summary>
/// Which period is this quota limit for (hourly, daily, monthly, etc...)
/// </summary>
public QuotaPer QuotaPeriod { get; init; }
public QuotaLimit(int quota, QuotaPer quotaPeriod)
{
Quota = quota;
QuotaPeriod = quotaPeriod;
}
}

View file

@ -1,9 +0,0 @@
namespace EllieBot.Modules.Patronage;
public enum QuotaPer
{
PerHour,
PerDay,
PerMonth,
Total,
}

View file

@ -14,28 +14,7 @@ public sealed class Rgba32TypeReader : EllieTypeReader<Rgba32>
return ValueTask.FromResult( return ValueTask.FromResult(
TypeReaderResult.FromError<Rgba32>(CommandError.ParseFailed, "Parameter is not a valid color hex.")); TypeReaderResult.FromError<Rgba32>(CommandError.ParseFailed, "Parameter is not a valid color hex."));
} }
Log.Information(color.ToHex());
return ValueTask.FromResult(TypeReaderResult.FromSuccess((Rgba32)color)); return ValueTask.FromResult(TypeReaderResult.FromSuccess((Rgba32)color));
if (Rgba32.TryParseHex(input, out var clr))
{
return ValueTask.FromResult(TypeReaderResult.FromSuccess(clr));
}
if (!Enum.TryParse<Color>(input, true, out var clrName))
return ValueTask.FromResult(
TypeReaderResult.FromError<Rgba32>(CommandError.ParseFailed,
"Parameter is not a valid color hex."));
Log.Information(clrName.ToString());
if (Rgba32.TryParseHex(clrName.ToHex(), out clr))
{
return ValueTask.FromResult(TypeReaderResult.FromSuccess(clr));
}
return ValueTask.FromResult(
TypeReaderResult.FromError<Rgba32>(CommandError.ParseFailed, "Parameter is not a valid color hex."));
} }
} }

View file

@ -7717,22 +7717,6 @@
"Options": null, "Options": null,
"Requirements": [] "Requirements": []
}, },
{
"Aliases": [
".quoteadd",
".qa",
".qadd",
".quadd"
],
"Description": "Adds a new quote with the specified name and message.",
"Usage": [
".quoteadd sayhi Hi"
],
"Submodule": "QuoteCommands",
"Module": "Utility",
"Options": null,
"Requirements": []
},
{ {
"Aliases": [ "Aliases": [
".quoteedit", ".quoteedit",

View file

@ -2,35 +2,24 @@
version: 3 version: 3
# Whether the patronage feature is enabled # Whether the patronage feature is enabled
isEnabled: false isEnabled: false
# Who can do how much of what # Quotas for patron system
limits: quotas:
50: None:
ChatBot: I:
quota: 20000000 livechannels: 1
quotaPeriod: PerMonth ai: 0
ReactionRole: V:
quota: -1 livechannels: 2
quotaPeriod: Total ai: 50
Prune: X:
quota: -1 livechannels: 5
quotaPeriod: PerDay ai: 100
10: XX:
ChatBot: livechannels: 5
quota: 2500000 ai: 200
quotaPeriod: PerMonth L:
ReactionRole: livechannels: 5
quota: 50 ai: 500
quotaPeriod: Total C:
Prune: livechannels: 5
quota: 5 ai: -1
quotaPeriod: PerDay
2:
ChatBot:
quota: 1000000
quotaPeriod: PerMonth
ReactionRole:
quota: 25
quotaPeriod: Total
Prune:
quota: 2
quotaPeriod: PerDay