From aba5c4fbfdb9c501fbb64f374b6e040de70f2054 Mon Sep 17 00:00:00 2001
From: Toastie <toastie@toastiet0ast.com>
Date: Sun, 30 Mar 2025 16:16:57 +1300
Subject: [PATCH] simplified quota system

---
 CHANGELOG.md                                  |   1 +
 .../Games/ChatterBot/ChatterBotService.cs     |  10 +-
 .../Modules/Patronage/PatronageCommands.cs    |  54 +---
 .../Modules/Patronage/PatronageService.cs     | 290 +++++++-----------
 .../LiveChannel/LiveChannelCommands.cs        |   4 +-
 .../Utility/LiveChannel/LiveChannelService.cs |  25 +-
 .../Modules/Utility/Quote/QuoteCommands.cs    |   3 +-
 .../_common/Patronage/FeatureLimitKey.cs      |   7 -
 .../_common/Patronage/IPatronageService.cs    |   8 +-
 .../_common/Patronage/PatronConfigData.cs     |   6 +-
 src/EllieBot/_common/Patronage/PatronTier.cs  |   2 +
 src/EllieBot/_common/Patronage/QuotaLimit.cs  |  23 --
 src/EllieBot/_common/Patronage/QuotaPer.cs    |   9 -
 .../_common/TypeReaders/Rgba32TypeReader.cs   |  21 --
 src/EllieBot/data/commandlist.json            |  16 -
 src/EllieBot/data/patron.yml                  |  53 ++--
 16 files changed, 166 insertions(+), 366 deletions(-)
 delete mode 100644 src/EllieBot/_common/Patronage/QuotaLimit.cs
 delete mode 100644 src/EllieBot/_common/Patronage/QuotaPer.cs

diff --git a/CHANGELOG.md b/CHANGELOG.md
index c7be193..465e1dc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -32,6 +32,7 @@
 - .notify will now let you know if you can't set a notify message due to a missing channel
 - `.say` will no longer reply
 - `.vote` and `.timely` will now show active bonuses
+- `.lcha` (live channel) limit increased to 5
 
 ### Fixed
 - Fixed `.antispamignore` restart persistence
diff --git a/src/EllieBot/Modules/Games/ChatterBot/ChatterBotService.cs b/src/EllieBot/Modules/Games/ChatterBot/ChatterBotService.cs
index e37046c..3562d56 100644
--- a/src/EllieBot/Modules/Games/ChatterBot/ChatterBotService.cs
+++ b/src/EllieBot/Modules/Games/ChatterBot/ChatterBotService.cs
@@ -145,7 +145,7 @@ public class ChatterBotService : IExecOnMessage, IReadyExecutor
             if (!res.IsAllowed)
                 return false;
 
-            if (!await _ps.LimitHitAsync(LimitedFeatureName.ChatBot, usrMsg.Author.Id, 2048 / 2))
+            if (!await _ps.LimitHitAsync("ai", guild.OwnerId, 1))
             {
                 // limit exceeded
                 return false;
@@ -156,14 +156,6 @@ public class ChatterBotService : IExecOnMessage, IReadyExecutor
 
             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)
                     .Confirm(result.Text)
                     .SendAsync();
diff --git a/src/EllieBot/Modules/Patronage/PatronageCommands.cs b/src/EllieBot/Modules/Patronage/PatronageCommands.cs
index 64850f3..892944d 100644
--- a/src/EllieBot/Modules/Patronage/PatronageCommands.cs
+++ b/src/EllieBot/Modules/Patronage/PatronageCommands.cs
@@ -36,11 +36,11 @@ public partial class Help
             var result = await _service.SendMessageToPatronsAsync(tierAndHigher, message);
 
             await Response()
-                  .Confirm(strs.patron_msg_sent(
-                      Format.Code(tierAndHigher.ToString()),
-                      Format.Bold(result.Success.ToString()),
-                      Format.Bold(result.Failed.ToString())))
-                  .SendAsync();
+                .Confirm(strs.patron_msg_sent(
+                    Format.Code(tierAndHigher.ToString()),
+                    Format.Bold(result.Success.ToString()),
+                    Format.Bold(result.Failed.ToString())))
+                .SendAsync();
         }
 
         // [OwnerOnly]
@@ -73,32 +73,24 @@ public partial class Help
 
             var maybePatron = await _service.GetPatronAsync(user.Id);
 
-            var quotaStats = await _service.LimitStats(user.Id);
-
             var eb = CreateEmbed()
-                            .WithAuthor(user)
-                            .WithTitle(GetText(strs.patron_info))
-                            .WithOkColor();
+                .WithAuthor(user)
+                .WithTitle(GetText(strs.patron_info))
+                .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
             {
                 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)
                     eb.AddField(GetText(strs.expires),
                         patron.ValidThru.AddDays(1).ToShortAndRelativeTimestampTag(),
                         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();
             }
         }
-
-        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)
-            };
     }
 }
\ No newline at end of file
diff --git a/src/EllieBot/Modules/Patronage/PatronageService.cs b/src/EllieBot/Modules/Patronage/PatronageService.cs
index 3a30c99..b9b299a 100644
--- a/src/EllieBot/Modules/Patronage/PatronageService.cs
+++ b/src/EllieBot/Modules/Patronage/PatronageService.cs
@@ -98,21 +98,21 @@ public sealed class PatronageService
             try
             {
                 var dbPatron = await ctx.GetTable<PatronUser>()
-                                        .FirstOrDefaultAsync(x
-                                            => x.UniquePlatformUserId == subscriber.UniquePlatformUserId);
+                    .FirstOrDefaultAsync(x
+                        => x.UniquePlatformUserId == subscriber.UniquePlatformUserId);
 
                 if (dbPatron is null)
                 {
                     // if the user is not in the database alrady
                     dbPatron = await ctx.GetTable<PatronUser>()
-                                        .InsertWithOutputAsync(() => new()
-                                        {
-                                            UniquePlatformUserId = subscriber.UniquePlatformUserId,
-                                            UserId = subscriber.UserId,
-                                            AmountCents = subscriber.Cents,
-                                            LastCharge = lastChargeUtc,
-                                            ValidThru = dateInOneMonth,
-                                        });
+                        .InsertWithOutputAsync(() => new()
+                        {
+                            UniquePlatformUserId = subscriber.UniquePlatformUserId,
+                            UserId = subscriber.UserId,
+                            AmountCents = subscriber.Cents,
+                            LastCharge = lastChargeUtc,
+                            ValidThru = dateInOneMonth,
+                        });
 
                     // 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 it's not, just add 1 month to the last charge date
                         await ctx.GetTable<PatronUser>()
-                                 .Where(x => x.UniquePlatformUserId
-                                             == subscriber.UniquePlatformUserId)
-                                 .UpdateAsync(old => new()
-                                 {
-                                     UserId = subscriber.UserId,
-                                     AmountCents = subscriber.Cents,
-                                     LastCharge = lastChargeUtc,
-                                     ValidThru = old.ValidThru >= todayDate
-                                         // ? Sql.DateAdd(Sql.DateParts.Month, 1, old.ValidThru).Value
-                                         ? old.ValidThru.AddMonths(1)
-                                         : dateInOneMonth,
-                                 });
+                            .Where(x => x.UniquePlatformUserId
+                                        == subscriber.UniquePlatformUserId)
+                            .UpdateAsync(old => new()
+                            {
+                                UserId = subscriber.UserId,
+                                AmountCents = subscriber.Cents,
+                                LastCharge = lastChargeUtc,
+                                ValidThru = old.ValidThru >= todayDate
+                                    // ? Sql.DateAdd(Sql.DateParts.Month, 1, old.ValidThru).Value
+                                    ? old.ValidThru.AddMonths(1)
+                                    : dateInOneMonth,
+                            });
 
 
                         dbPatron.UserId = subscriber.UserId;
@@ -158,14 +158,14 @@ public sealed class PatronageService
                         var cents = subscriber.Cents;
                         // the user updated the pledge or changed the connected discord account
                         await ctx.GetTable<PatronUser>()
-                                 .Where(x => x.UniquePlatformUserId == subscriber.UniquePlatformUserId)
-                                 .UpdateAsync(old => new()
-                                 {
-                                     UserId = subscriber.UserId,
-                                     AmountCents = cents,
-                                     LastCharge = lastChargeUtc,
-                                     ValidThru = old.ValidThru,
-                                 });
+                            .Where(x => x.UniquePlatformUserId == subscriber.UniquePlatformUserId)
+                            .UpdateAsync(old => new()
+                            {
+                                UserId = subscriber.UserId,
+                                AmountCents = cents,
+                                LastCharge = lastChargeUtc,
+                                ValidThru = old.ValidThru,
+                            });
 
                         var newPatron = dbPatron.Clone();
                         newPatron.AmountCents = cents;
@@ -192,19 +192,19 @@ public sealed class PatronageService
         {
             // if the subscription is refunded, Disable user's valid thru 
             var changedCount = await ctx.GetTable<PatronUser>()
-                                        .Where(x => x.UniquePlatformUserId == patron.UniquePlatformUserId
-                                                    && x.ValidThru != expiredDate)
-                                        .UpdateAsync(old => new()
-                                        {
-                                            ValidThru = expiredDate
-                                        });
+                .Where(x => x.UniquePlatformUserId == patron.UniquePlatformUserId
+                            && x.ValidThru != expiredDate)
+                .UpdateAsync(old => new()
+                {
+                    ValidThru = expiredDate
+                });
 
             if (changedCount == 0)
                 continue;
 
             var updated = await ctx.GetTable<PatronUser>()
-                                   .Where(x => x.UniquePlatformUserId == patron.UniquePlatformUserId)
-                                   .FirstAsync();
+                .Where(x => x.UniquePlatformUserId == patron.UniquePlatformUserId)
+                .FirstAsync();
 
             await OnPatronRefunded(PatronUserToPatron(updated));
         }
@@ -218,8 +218,8 @@ public sealed class PatronageService
         // is subscribed on multiple platforms
         // or if there are multiple users on the same platform who connected the same discord account?!
         var users = await ctx.GetTable<PatronUser>()
-                             .Where(x => x.UserId == userId)
-                             .ToListAsync();
+            .Where(x => x.UserId == userId)
+            .ToListAsync();
 
         // first find all active subscriptions
         // and return the one with the highest amount
@@ -236,136 +236,56 @@ public sealed class PatronageService
         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;
 
-        if (!_pConf.Data.IsEnabled)
+        var limit = await GetUserLimit(name, userId, defaultMax);
+
+        if (limit == -1)
             return true;
+        
+        var timeUntilTomorrow = (DateTime.UtcNow.Date.AddDays(1) - DateTime.UtcNow);
+        var soFar = await _cache.GetOrAddAsync(Limitkey(name, userId),
+            () => Task.FromResult(0),
+            expiry: timeUntilTomorrow);
 
-        var userLimit = await GetUserLimit(key, userId);
-
-        if (userLimit.Quota == 0)
+        if (soFar >= limit)
             return false;
 
-        if (userLimit.Quota == -1)
-            return true;
-
-        return await TryAddLimit(key, userLimit, userId, amount);
+        await _cache.AddAsync(Limitkey(name, userId), soFar + 1, timeUntilTomorrow, overwrite: true);
+        return true;
     }
 
-    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))
-            return true;
+        var data = _pConf.Data;
+        if (!data.IsEnabled || _creds.GetCreds().OwnerIds.Contains(userId))
+            return defaultMax;
 
-        if (!_pConf.Data.IsEnabled)
-            return true;
+        var mPatron = await GetPatronAsync(userId);
 
-        var userLimit = await GetUserLimit(key, userId);
-
-        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)
+        if (mPatron is not { } patron || !patron.IsActive)
         {
-            await _cache.AddAsync(cacheKey, cur + amount);
-            return true;
+            if (data.Quotas.TryGetValue(PatronTier.I, out var limits)
+                && 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)
         => new Patron()
         {
@@ -384,10 +304,13 @@ public sealed class PatronageService
 
         return user.AmountCents switch
         {
-            <= 200 => PatronTier.I,
-            <= 1_000 => PatronTier.C,
-            <= 5_000 => PatronTier.L,
-            _ => PatronTier.None
+            >= 10_000 => PatronTier.C,
+            >= 5_000 => PatronTier.L,
+            >= 2_000 => PatronTier.XX,
+            >= 1_000 => PatronTier.X,
+            >= 500 => PatronTier.V,
+            >= 100 => PatronTier.I,
+            _ => 0,
         };
     }
 
@@ -399,10 +322,12 @@ public sealed class PatronageService
     public int PercentBonus(long amount)
         => amount switch
         {
-            < 200 => 0,
-            < 1_000 => 10,
-            < 5_000 => 50,
-            _ => 100
+            >= 10_000 => 100,
+            >= 5_000 => 50,
+            >= 2_000 => 25,
+            >= 1_000 => 10,
+            >= 500 => 5,
+            _ => 0,
         };
 
     private async Task SendWelcomeMessage(Patron patron)
@@ -414,28 +339,23 @@ public sealed class PatronageService
                 return;
 
             var eb = _sender.CreateEmbed()
-                            .WithOkColor()
-                            .WithTitle("❤️ Thank you for supporting EllieBot! ❤️")
-                            .WithDescription(
-                                "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. 🎉")
-                            .AddField("Tier", Format.Bold(patron.Tier.ToString()), true)
-                            .AddField("Pledge", $"**{patron.Amount / 100.0f:N1}$**", true)
-                            .AddField("Expires",
-                                patron.ValidThru.AddDays(1).ToShortAndRelativeTimestampTag(),
-                                true)
-                            .AddField("Instructions",
-                                """
-                                *- 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*
-                                *- **ALL** of the servers that you **own** will enjoy your Patron benefits.*
-                                *- You can use any of the commands available in your tier on any server (assuming you have sufficient permissions to run those commands)*
-                                *- 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}");
+                .WithOkColor()
+                .WithTitle("❤️ Thank you for supporting EllieBot! ❤️")
+                .WithDescription(
+                    "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. 🎉")
+                .AddField("Tier", Format.Bold(patron.Tier.ToString()), true)
+                .AddField("Pledge", $"**{patron.Amount / 100.0f:N1}$**", true)
+                .AddField("Expires",
+                    patron.ValidThru.AddDays(1).ToShortAndRelativeTimestampTag(),
+                    true)
+                .AddField("Instructions",
+                    """
+                    *- Within the next **1-2 minutes** you will have all of the benefits of the Tier you've subscribed to.*
+                    *- You can use the `.patron` command in this chat to check your current quota usage for the Patron-only commands*
+                    """,
+                    inline: false)
+                .WithFooter($"platform id: {patron.UniquePlatformUserId}");
 
             await _sender.Response(user).Embed(eb).SendAsync();
         }
@@ -450,8 +370,8 @@ public sealed class PatronageService
         await using var ctx = _db.GetDbContext();
 
         var patrons = await ctx.GetTable<PatronUser>()
-                               .Where(x => x.ValidThru > DateTime.UtcNow)
-                               .ToArrayAsync();
+            .Where(x => x.ValidThru > DateTime.UtcNow)
+            .ToArrayAsync();
 
         var text = SmartText.CreateFrom(message);
 
diff --git a/src/EllieBot/Modules/Utility/LiveChannel/LiveChannelCommands.cs b/src/EllieBot/Modules/Utility/LiveChannel/LiveChannelCommands.cs
index 7b8ca9a..879af02 100644
--- a/src/EllieBot/Modules/Utility/LiveChannel/LiveChannelCommands.cs
+++ b/src/EllieBot/Modules/Utility/LiveChannel/LiveChannelCommands.cs
@@ -13,10 +13,10 @@ public partial class Utility
         [BotPerm(GuildPerm.ManageChannels)]
         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()
-                    .Error(strs.livechannel_limit(LiveChannelService.MAX_LIVECHANNELS))
+                    .Error(strs.livechannel_limit(await svc.GetMaxLiveChannels(ctx.Guild.OwnerId)))
                     .SendAsync();
                 return;
             }
diff --git a/src/EllieBot/Modules/Utility/LiveChannel/LiveChannelService.cs b/src/EllieBot/Modules/Utility/LiveChannel/LiveChannelService.cs
index e8c764a..ab22fc7 100644
--- a/src/EllieBot/Modules/Utility/LiveChannel/LiveChannelService.cs
+++ b/src/EllieBot/Modules/Utility/LiveChannel/LiveChannelService.cs
@@ -4,6 +4,7 @@ using LinqToDB.EntityFrameworkCore;
 using Microsoft.EntityFrameworkCore;
 using EllieBot.Common.ModuleBehaviors;
 using EllieBot.Db.Models;
+using EllieBot.Modules.Patronage;
 using Newtonsoft.Json.Linq;
 
 namespace EllieBot.Modules.Utility.LiveChannel;
@@ -15,9 +16,10 @@ public class LiveChannelService(
     DbService db,
     DiscordSocketClient client,
     IReplacementService repSvc,
+    IPatronageService patron,
     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();
 
@@ -122,23 +124,23 @@ public class LiveChannelService(
     /// <param name="channelId">ID of the channel</param>
     /// <param name="template">Template text to use for the channel</param>
     /// <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(
             guildId,
             _ => new());
 
-        if (!guildDict.ContainsKey(channelId) && guildDict.Count >= MAX_LIVECHANNELS)
+        if (!guildDict.ContainsKey(channelId) && guildDict.Count >= await GetMaxLiveChannels(guildOwnerId))
             return false;
 
         await using var uow = db.GetDbContext();
         await uow.GetTable<LiveChannelConfig>()
             .InsertOrUpdateAsync(() => new()
-            {
-                GuildId = guildId,
-                ChannelId = channelId,
-                Template = template
-            },
+                {
+                    GuildId = guildId,
+                    ChannelId = channelId,
+                    Template = template
+                },
                 (_) => new()
                 {
                     Template = template
@@ -194,4 +196,11 @@ public class LiveChannelService(
             .Where(x => x.GuildId == guildId)
             .ToListAsyncLinqToDB();
     }
+
+
+    public async Task<int> GetMaxLiveChannels(ulong guildOwnerId)
+    {
+        var limit = await patron.GetUserLimit("livechannels", guildOwnerId, DEFAULT_MAX_LIVECHANNELS);
+        return limit;
+    }
 }
\ No newline at end of file
diff --git a/src/EllieBot/Modules/Utility/Quote/QuoteCommands.cs b/src/EllieBot/Modules/Utility/Quote/QuoteCommands.cs
index 38c392e..8b5b180 100644
--- a/src/EllieBot/Modules/Utility/Quote/QuoteCommands.cs
+++ b/src/EllieBot/Modules/Utility/Quote/QuoteCommands.cs
@@ -235,8 +235,7 @@ public partial class Utility
                   .SendAsync();
         }
 
-
-        [Cmd]
+        
         [RequireContext(ContextType.Guild)]
         public async Task QuoteAdd(string keyword, [Leftover] string text)
         {
diff --git a/src/EllieBot/_common/Patronage/FeatureLimitKey.cs b/src/EllieBot/_common/Patronage/FeatureLimitKey.cs
index 10278e1..9aa8a46 100644
--- a/src/EllieBot/_common/Patronage/FeatureLimitKey.cs
+++ b/src/EllieBot/_common/Patronage/FeatureLimitKey.cs
@@ -1,12 +1,5 @@
 namespace EllieBot.Modules.Patronage;
 
-public enum LimitedFeatureName
-{
-    ChatBot,
-    ReactionRole,
-    Prune,
-    
-}
 public readonly struct FeatureLimitKey
 {
     public string PrettyName { get; init; }
diff --git a/src/EllieBot/_common/Patronage/IPatronageService.cs b/src/EllieBot/_common/Patronage/IPatronageService.cs
index 379de6b..e093fa0 100644
--- a/src/EllieBot/_common/Patronage/IPatronageService.cs
+++ b/src/EllieBot/_common/Patronage/IPatronageService.cs
@@ -30,12 +30,8 @@ public interface IPatronageService
     /// <returns>A patron with the specifeid userId</returns>
     public Task<Patron?> GetPatronAsync(ulong userId);
     
-    Task<bool> LimitHitAsync(LimitedFeatureName key, ulong userId, int amount = 1);
-    Task<bool> LimitForceHit(LimitedFeatureName key, ulong userId, int amount);
-    Task<QuotaLimit> GetUserLimit(LimitedFeatureName name, ulong userId);
-    
-    Task<Dictionary<LimitedFeatureName, (int, QuotaLimit)>> LimitStats(ulong userId);
-
+    Task<bool> LimitHitAsync(string name, ulong userId, int def);
+    Task<int> GetUserLimit(string name, ulong userId, int def);
     PatronConfigData GetConfig();
     int PercentBonus(Patron? user);
     int PercentBonus(long amount);
diff --git a/src/EllieBot/_common/Patronage/PatronConfigData.cs b/src/EllieBot/_common/Patronage/PatronConfigData.cs
index 9becae0..d59a294 100644
--- a/src/EllieBot/_common/Patronage/PatronConfigData.cs
+++ b/src/EllieBot/_common/Patronage/PatronConfigData.cs
@@ -11,7 +11,7 @@ public partial class PatronConfigData : ICloneable<PatronConfigData>
 
     [Comment("Whether the patronage feature is enabled")]
     public bool IsEnabled { get; set; }
-
-    [Comment("Who can do how much of what")]
-    public Dictionary<int, Dictionary<LimitedFeatureName, QuotaLimit>> Limits { get; set; } = new();
+    
+    [Comment("Quotas for patron system")]
+    public Dictionary<PatronTier, Dictionary<string, int>> Quotas { get; set; } = new();
 }
\ No newline at end of file
diff --git a/src/EllieBot/_common/Patronage/PatronTier.cs b/src/EllieBot/_common/Patronage/PatronTier.cs
index a285fec..35fa670 100644
--- a/src/EllieBot/_common/Patronage/PatronTier.cs
+++ b/src/EllieBot/_common/Patronage/PatronTier.cs
@@ -5,7 +5,9 @@ public enum PatronTier
 {
     None,
     I,
+    V,
     X,
+    XX,
     L,
     C
 }
\ No newline at end of file
diff --git a/src/EllieBot/_common/Patronage/QuotaLimit.cs b/src/EllieBot/_common/Patronage/QuotaLimit.cs
deleted file mode 100644
index 5669c0c..0000000
--- a/src/EllieBot/_common/Patronage/QuotaLimit.cs
+++ /dev/null
@@ -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;
-    }
-}
\ No newline at end of file
diff --git a/src/EllieBot/_common/Patronage/QuotaPer.cs b/src/EllieBot/_common/Patronage/QuotaPer.cs
deleted file mode 100644
index 9f67a40..0000000
--- a/src/EllieBot/_common/Patronage/QuotaPer.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace EllieBot.Modules.Patronage;
-
-public enum QuotaPer
-{
-    PerHour,
-    PerDay,
-    PerMonth,
-    Total,
-}
\ No newline at end of file
diff --git a/src/EllieBot/_common/TypeReaders/Rgba32TypeReader.cs b/src/EllieBot/_common/TypeReaders/Rgba32TypeReader.cs
index ab1f9e8..cb5bd51 100644
--- a/src/EllieBot/_common/TypeReaders/Rgba32TypeReader.cs
+++ b/src/EllieBot/_common/TypeReaders/Rgba32TypeReader.cs
@@ -14,28 +14,7 @@ public sealed class Rgba32TypeReader : EllieTypeReader<Rgba32>
             return ValueTask.FromResult(
                 TypeReaderResult.FromError<Rgba32>(CommandError.ParseFailed, "Parameter is not a valid color hex."));
         }
-        Log.Information(color.ToHex());
 
         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."));
     }
 }
\ No newline at end of file
diff --git a/src/EllieBot/data/commandlist.json b/src/EllieBot/data/commandlist.json
index 8324dad..d72493b 100644
--- a/src/EllieBot/data/commandlist.json
+++ b/src/EllieBot/data/commandlist.json
@@ -7717,22 +7717,6 @@
       "Options": null,
       "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": [
         ".quoteedit",
diff --git a/src/EllieBot/data/patron.yml b/src/EllieBot/data/patron.yml
index 56da34a..f7a101a 100644
--- a/src/EllieBot/data/patron.yml
+++ b/src/EllieBot/data/patron.yml
@@ -2,35 +2,24 @@
 version: 3
 # Whether the patronage feature is enabled
 isEnabled: false
-# Who can do how much of what
-limits:
-  50:
-    ChatBot:
-      quota: 20000000
-      quotaPeriod: PerMonth
-    ReactionRole:
-      quota: -1
-      quotaPeriod: Total
-    Prune:
-      quota: -1
-      quotaPeriod: PerDay
-  10:
-    ChatBot:
-      quota: 2500000
-      quotaPeriod: PerMonth
-    ReactionRole:
-      quota: 50
-      quotaPeriod: Total
-    Prune:
-      quota: 5
-      quotaPeriod: PerDay
-  2:
-    ChatBot:
-      quota: 1000000
-      quotaPeriod: PerMonth
-    ReactionRole:
-      quota: 25
-      quotaPeriod: Total
-    Prune:
-      quota: 2
-      quotaPeriod: PerDay
+# Quotas for patron system
+quotas:
+  None:
+  I:
+    livechannels: 1
+    ai: 0
+  V:
+    livechannels: 2
+    ai: 50
+  X:
+    livechannels: 5
+    ai: 100
+  XX:
+    livechannels: 5
+    ai: 200
+  L:
+    livechannels: 5
+    ai: 500
+  C:
+    livechannels: 5
+    ai: -1
\ No newline at end of file