simplified quota system
This commit is contained in:
parent
7d8f61ecea
commit
aba5c4fbfd
16 changed files with 166 additions and 366 deletions
|
@ -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
|
||||||
|
|
|
@ -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();
|
||||||
|
|
|
@ -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)
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
|
@ -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)
|
||||||
{
|
{
|
||||||
|
|
|
@ -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; }
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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();
|
||||||
}
|
}
|
|
@ -5,7 +5,9 @@ public enum PatronTier
|
||||||
{
|
{
|
||||||
None,
|
None,
|
||||||
I,
|
I,
|
||||||
|
V,
|
||||||
X,
|
X,
|
||||||
|
XX,
|
||||||
L,
|
L,
|
||||||
C
|
C
|
||||||
}
|
}
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,9 +0,0 @@
|
||||||
namespace EllieBot.Modules.Patronage;
|
|
||||||
|
|
||||||
public enum QuotaPer
|
|
||||||
{
|
|
||||||
PerHour,
|
|
||||||
PerDay,
|
|
||||||
PerMonth,
|
|
||||||
Total,
|
|
||||||
}
|
|
|
@ -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."));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -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",
|
||||||
|
|
|
@ -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
|
|
Loading…
Add table
Add a link
Reference in a new issue