2024-06-18 11:54:32 +00:00
|
|
|
|
using LinqToDB;
|
|
|
|
|
using LinqToDB.EntityFrameworkCore;
|
|
|
|
|
using EllieBot.Common.ModuleBehaviors;
|
|
|
|
|
using EllieBot.Db.Models;
|
|
|
|
|
|
|
|
|
|
namespace EllieBot.Modules.Patronage;
|
|
|
|
|
|
|
|
|
|
/// <inheritdoc cref="IPatronageService"/>
|
|
|
|
|
public sealed class PatronageService
|
|
|
|
|
: IPatronageService,
|
|
|
|
|
IReadyExecutor,
|
|
|
|
|
IEService
|
|
|
|
|
{
|
|
|
|
|
public event Func<Patron, Task> OnNewPatronPayment = static delegate { return Task.CompletedTask; };
|
|
|
|
|
public event Func<Patron, Patron, Task> OnPatronUpdated = static delegate { return Task.CompletedTask; };
|
|
|
|
|
public event Func<Patron, Task> OnPatronRefunded = static delegate { return Task.CompletedTask; };
|
|
|
|
|
|
|
|
|
|
// this has to run right before the command
|
|
|
|
|
public int Priority
|
|
|
|
|
=> int.MinValue;
|
|
|
|
|
|
|
|
|
|
private readonly PatronageConfig _pConf;
|
|
|
|
|
private readonly DbService _db;
|
|
|
|
|
private readonly DiscordSocketClient _client;
|
|
|
|
|
private readonly ISubscriptionHandler _subsHandler;
|
|
|
|
|
|
|
|
|
|
private readonly IBotCache _cache;
|
|
|
|
|
private readonly IBotCredsProvider _creds;
|
|
|
|
|
private readonly IMessageSenderService _sender;
|
|
|
|
|
|
|
|
|
|
public PatronageService(
|
|
|
|
|
PatronageConfig pConf,
|
|
|
|
|
DbService db,
|
|
|
|
|
DiscordSocketClient client,
|
|
|
|
|
ISubscriptionHandler subsHandler,
|
|
|
|
|
IBotCache cache,
|
|
|
|
|
IBotCredsProvider creds,
|
|
|
|
|
IMessageSenderService sender)
|
|
|
|
|
{
|
|
|
|
|
_pConf = pConf;
|
|
|
|
|
_db = db;
|
|
|
|
|
_client = client;
|
|
|
|
|
_subsHandler = subsHandler;
|
|
|
|
|
_sender = sender;
|
|
|
|
|
_cache = cache;
|
|
|
|
|
_creds = creds;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public Task OnReadyAsync()
|
|
|
|
|
{
|
|
|
|
|
if (_client.ShardId != 0)
|
|
|
|
|
return Task.CompletedTask;
|
|
|
|
|
|
2024-06-27 08:22:54 +00:00
|
|
|
|
return Task.WhenAll(LoadSubscribersLoopAsync());
|
2024-06-18 11:54:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task LoadSubscribersLoopAsync()
|
|
|
|
|
{
|
|
|
|
|
var timer = new PeriodicTimer(TimeSpan.FromSeconds(60));
|
|
|
|
|
while (await timer.WaitForNextTickAsync())
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
if (!_pConf.Data.IsEnabled)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
await foreach (var batch in _subsHandler.GetPatronsAsync())
|
|
|
|
|
{
|
|
|
|
|
await ProcesssPatronsAsync(batch);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Log.Error(ex, "Error processing patrons");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async Task ProcesssPatronsAsync(IReadOnlyCollection<ISubscriberData> subscribersEnum)
|
|
|
|
|
{
|
|
|
|
|
// process only users who have discord accounts connected
|
|
|
|
|
var subscribers = subscribersEnum.Where(x => x.UserId != 0).ToArray();
|
|
|
|
|
|
|
|
|
|
if (subscribers.Length == 0)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
var todayDate = DateTime.UtcNow.Date;
|
|
|
|
|
await using var ctx = _db.GetDbContext();
|
|
|
|
|
|
|
|
|
|
// handle paid users
|
|
|
|
|
foreach (var subscriber in subscribers.Where(x => x.ChargeStatus == SubscriptionChargeStatus.Paid))
|
|
|
|
|
{
|
|
|
|
|
if (subscriber.LastCharge is null)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
var lastChargeUtc = subscriber.LastCharge.Value.ToUniversalTime();
|
|
|
|
|
var dateInOneMonth = lastChargeUtc.Date.AddMonths(1);
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var dbPatron = await ctx.GetTable<PatronUser>()
|
|
|
|
|
.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,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// await tran.CommitAsync();
|
|
|
|
|
|
|
|
|
|
var newPatron = PatronUserToPatron(dbPatron);
|
|
|
|
|
_ = SendWelcomeMessage(newPatron);
|
|
|
|
|
await OnNewPatronPayment(newPatron);
|
|
|
|
|
}
|
|
|
|
|
else
|
|
|
|
|
{
|
|
|
|
|
if (dbPatron.LastCharge.Month < lastChargeUtc.Month
|
|
|
|
|
|| dbPatron.LastCharge.Year < lastChargeUtc.Year)
|
|
|
|
|
{
|
|
|
|
|
// user is charged again for this 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
|
2024-08-06 10:52:02 +00:00
|
|
|
|
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,
|
|
|
|
|
});
|
2024-06-18 11:54:32 +00:00
|
|
|
|
|
|
|
|
|
|
2024-06-27 08:22:54 +00:00
|
|
|
|
dbPatron.UserId = subscriber.UserId;
|
|
|
|
|
dbPatron.AmountCents = subscriber.Cents;
|
|
|
|
|
dbPatron.LastCharge = lastChargeUtc;
|
|
|
|
|
dbPatron.ValidThru = dbPatron.ValidThru >= todayDate
|
|
|
|
|
? dbPatron.ValidThru.AddMonths(1)
|
|
|
|
|
: dateInOneMonth;
|
2024-06-18 11:54:32 +00:00
|
|
|
|
|
|
|
|
|
await OnNewPatronPayment(PatronUserToPatron(dbPatron));
|
|
|
|
|
}
|
|
|
|
|
else if (dbPatron.AmountCents != subscriber.Cents // if user changed the amount
|
|
|
|
|
|| dbPatron.UserId != subscriber.UserId) // if user updated user id)
|
|
|
|
|
{
|
|
|
|
|
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,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
var newPatron = dbPatron.Clone();
|
|
|
|
|
newPatron.AmountCents = cents;
|
|
|
|
|
newPatron.UserId = subscriber.UserId;
|
|
|
|
|
|
|
|
|
|
// idk what's going on but UpdateWithOutputAsync doesn't work properly here
|
|
|
|
|
// nor does firstordefault after update. I'm not seeing something obvious
|
|
|
|
|
await OnPatronUpdated(
|
|
|
|
|
PatronUserToPatron(dbPatron),
|
|
|
|
|
PatronUserToPatron(newPatron));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
catch (Exception ex)
|
|
|
|
|
{
|
|
|
|
|
Log.Error(ex,
|
|
|
|
|
"Unexpected error occured while processing rewards for patron {UserId}",
|
|
|
|
|
subscriber.UserId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var expiredDate = DateTime.MinValue;
|
|
|
|
|
foreach (var patron in subscribers.Where(x => x.ChargeStatus == SubscriptionChargeStatus.Refunded))
|
|
|
|
|
{
|
|
|
|
|
// 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
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if (changedCount == 0)
|
|
|
|
|
continue;
|
|
|
|
|
|
|
|
|
|
var updated = await ctx.GetTable<PatronUser>()
|
|
|
|
|
.Where(x => x.UniquePlatformUserId == patron.UniquePlatformUserId)
|
|
|
|
|
.FirstAsync();
|
|
|
|
|
|
|
|
|
|
await OnPatronRefunded(PatronUserToPatron(updated));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-27 08:22:54 +00:00
|
|
|
|
public async Task<Patron?> GetPatronAsync(ulong userId)
|
2024-06-18 11:54:32 +00:00
|
|
|
|
{
|
2024-06-27 08:22:54 +00:00
|
|
|
|
await using var ctx = _db.GetDbContext();
|
2024-06-18 11:54:32 +00:00
|
|
|
|
|
2024-06-27 08:22:54 +00:00
|
|
|
|
// this can potentially return multiple users if the user
|
|
|
|
|
// 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();
|
2024-06-18 11:54:32 +00:00
|
|
|
|
|
2024-06-27 08:22:54 +00:00
|
|
|
|
// first find all active subscriptions
|
|
|
|
|
// and return the one with the highest amount
|
|
|
|
|
var maxActive = users.Where(x => !x.ValidThru.IsBeforeToday()).MaxBy(x => x.AmountCents);
|
|
|
|
|
if (maxActive is not null)
|
|
|
|
|
return PatronUserToPatron(maxActive);
|
2024-06-18 11:54:32 +00:00
|
|
|
|
|
2024-06-27 08:22:54 +00:00
|
|
|
|
// if there are no active subs, return the one with the highest amount
|
2024-06-18 11:54:32 +00:00
|
|
|
|
|
2024-06-27 08:22:54 +00:00
|
|
|
|
var max = users.MaxBy(x => x.AmountCents);
|
|
|
|
|
if (max is null)
|
|
|
|
|
return default; // no patron with that name
|
2024-06-18 11:54:32 +00:00
|
|
|
|
|
2024-06-27 08:22:54 +00:00
|
|
|
|
return PatronUserToPatron(max);
|
2024-06-18 11:54:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
2024-06-27 08:22:54 +00:00
|
|
|
|
public async Task<bool> LimitHitAsync(LimitedFeatureName key, ulong userId, int amount = 1)
|
2024-06-18 11:54:32 +00:00
|
|
|
|
{
|
2024-06-27 08:22:54 +00:00
|
|
|
|
if (_creds.GetCreds().IsOwner(userId))
|
|
|
|
|
return true;
|
2024-06-18 11:54:32 +00:00
|
|
|
|
|
2024-06-27 08:22:54 +00:00
|
|
|
|
if (!_pConf.Data.IsEnabled)
|
|
|
|
|
return true;
|
2024-06-18 11:54:32 +00:00
|
|
|
|
|
2024-06-27 08:22:54 +00:00
|
|
|
|
var userLimit = await GetUserLimit(key, userId);
|
2024-06-18 11:54:32 +00:00
|
|
|
|
|
2024-06-27 08:22:54 +00:00
|
|
|
|
if (userLimit.Quota == 0)
|
|
|
|
|
return false;
|
2024-06-18 11:54:32 +00:00
|
|
|
|
|
2024-06-27 08:22:54 +00:00
|
|
|
|
if (userLimit.Quota == -1)
|
|
|
|
|
return true;
|
2024-06-18 11:54:32 +00:00
|
|
|
|
|
2024-06-27 08:22:54 +00:00
|
|
|
|
return await TryAddLimit(key, userLimit, userId, amount);
|
2024-06-18 11:54:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
2024-06-27 08:22:54 +00:00
|
|
|
|
public async Task<bool> LimitForceHit(LimitedFeatureName key, ulong userId, int amount)
|
2024-06-18 11:54:32 +00:00
|
|
|
|
{
|
|
|
|
|
if (_creds.GetCreds().IsOwner(userId))
|
2024-06-27 08:22:54 +00:00
|
|
|
|
return true;
|
2024-06-18 11:54:32 +00:00
|
|
|
|
|
2024-06-27 08:22:54 +00:00
|
|
|
|
if (!_pConf.Data.IsEnabled)
|
|
|
|
|
return true;
|
2024-06-18 11:54:32 +00:00
|
|
|
|
|
2024-06-27 08:22:54 +00:00
|
|
|
|
var userLimit = await GetUserLimit(key, userId);
|
2024-06-18 11:54:32 +00:00
|
|
|
|
|
2024-06-27 08:22:54 +00:00
|
|
|
|
var cacheKey = CreateKey(key, userId);
|
|
|
|
|
await _cache.GetOrAddAsync(cacheKey, () => Task.FromResult(0), GetExpiry(userLimit));
|
2024-06-18 11:54:32 +00:00
|
|
|
|
|
2024-06-27 08:22:54 +00:00
|
|
|
|
return await TryAddLimit(key, userLimit, userId, amount);
|
2024-06-18 11:54:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
2024-06-27 08:22:54 +00:00
|
|
|
|
private async Task<bool> TryAddLimit(
|
|
|
|
|
LimitedFeatureName key,
|
|
|
|
|
QuotaLimit userLimit,
|
|
|
|
|
ulong userId,
|
|
|
|
|
int amount)
|
2024-06-18 11:54:32 +00:00
|
|
|
|
{
|
2024-06-27 08:22:54 +00:00
|
|
|
|
var cacheKey = CreateKey(key, userId);
|
|
|
|
|
var cur = await _cache.GetOrAddAsync(cacheKey, () => Task.FromResult(0), GetExpiry(userLimit));
|
2024-06-18 11:54:32 +00:00
|
|
|
|
|
2024-06-27 08:22:54 +00:00
|
|
|
|
if (cur + amount < userLimit.Quota)
|
2024-06-18 11:54:32 +00:00
|
|
|
|
{
|
2024-06-27 08:22:54 +00:00
|
|
|
|
await _cache.AddAsync(cacheKey, cur + amount);
|
|
|
|
|
return true;
|
2024-06-18 11:54:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-27 08:22:54 +00:00
|
|
|
|
private TimeSpan? GetExpiry(QuotaLimit userLimit)
|
2024-06-18 11:54:32 +00:00
|
|
|
|
{
|
2024-06-27 08:22:54 +00:00
|
|
|
|
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;
|
|
|
|
|
}
|
2024-06-18 11:54:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
2024-06-27 08:22:54 +00:00
|
|
|
|
private TypedKey<int> CreateKey(LimitedFeatureName key, ulong userId)
|
|
|
|
|
=> new($"limited_feature:{key}:{userId}");
|
2024-06-18 11:54:32 +00:00
|
|
|
|
|
2024-06-27 08:22:54 +00:00
|
|
|
|
private readonly QuotaLimit _emptyQuota = new QuotaLimit()
|
|
|
|
|
{
|
|
|
|
|
Quota = 0,
|
|
|
|
|
QuotaPeriod = QuotaPer.PerDay,
|
|
|
|
|
};
|
2024-06-18 11:54:32 +00:00
|
|
|
|
|
2024-06-27 08:22:54 +00:00
|
|
|
|
private readonly QuotaLimit _infiniteQuota = new QuotaLimit()
|
|
|
|
|
{
|
|
|
|
|
Quota = -1,
|
|
|
|
|
QuotaPeriod = QuotaPer.PerDay,
|
|
|
|
|
};
|
2024-06-18 11:54:32 +00:00
|
|
|
|
|
2024-06-27 08:22:54 +00:00
|
|
|
|
public async Task<QuotaLimit> GetUserLimit(LimitedFeatureName name, ulong userId)
|
|
|
|
|
{
|
|
|
|
|
if (!_pConf.Data.IsEnabled)
|
|
|
|
|
return _infiniteQuota;
|
2024-08-06 10:52:02 +00:00
|
|
|
|
|
2024-06-27 08:22:54 +00:00
|
|
|
|
var maybePatron = await GetPatronAsync(userId);
|
2024-06-18 11:54:32 +00:00
|
|
|
|
|
2024-06-27 08:22:54 +00:00
|
|
|
|
if (maybePatron is not { } patron)
|
|
|
|
|
return _emptyQuota;
|
2024-06-18 11:54:32 +00:00
|
|
|
|
|
2024-06-27 08:22:54 +00:00
|
|
|
|
if (patron.ValidThru < DateTime.UtcNow)
|
|
|
|
|
return _emptyQuota;
|
2024-06-18 11:54:32 +00:00
|
|
|
|
|
2024-06-27 08:22:54 +00:00
|
|
|
|
foreach (var (key, value) in _pConf.Data.Limits)
|
2024-06-18 11:54:32 +00:00
|
|
|
|
{
|
2024-06-27 08:22:54 +00:00
|
|
|
|
if (patron.Amount >= key)
|
2024-06-18 11:54:32 +00:00
|
|
|
|
{
|
2024-06-27 08:22:54 +00:00
|
|
|
|
if (value.TryGetValue(name, out var quotaLimit))
|
2024-06-18 11:54:32 +00:00
|
|
|
|
{
|
2024-06-27 08:22:54 +00:00
|
|
|
|
return quotaLimit;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
break;
|
2024-06-18 11:54:32 +00:00
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-27 08:22:54 +00:00
|
|
|
|
return _emptyQuota;
|
2024-06-18 11:54:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
2024-06-27 08:22:54 +00:00
|
|
|
|
public async Task<Dictionary<LimitedFeatureName, (int, QuotaLimit)>> LimitStats(ulong userId)
|
2024-06-18 11:54:32 +00:00
|
|
|
|
{
|
2024-06-27 08:22:54 +00:00
|
|
|
|
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));
|
2024-06-18 11:54:32 +00:00
|
|
|
|
|
2024-06-27 08:22:54 +00:00
|
|
|
|
dict[featureName] = (cur, userLimit);
|
|
|
|
|
}
|
2024-06-18 11:54:32 +00:00
|
|
|
|
|
2024-06-27 08:22:54 +00:00
|
|
|
|
return dict;
|
2024-06-18 11:54:32 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
private Patron PatronUserToPatron(PatronUser user)
|
|
|
|
|
=> new Patron()
|
|
|
|
|
{
|
|
|
|
|
UniquePlatformUserId = user.UniquePlatformUserId,
|
|
|
|
|
UserId = user.UserId,
|
|
|
|
|
Amount = user.AmountCents,
|
|
|
|
|
Tier = CalculateTier(user),
|
|
|
|
|
PaidAt = user.LastCharge,
|
|
|
|
|
ValidThru = user.ValidThru,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
private PatronTier CalculateTier(PatronUser user)
|
|
|
|
|
{
|
|
|
|
|
if (user.ValidThru.IsBeforeToday())
|
|
|
|
|
return PatronTier.None;
|
|
|
|
|
|
|
|
|
|
return user.AmountCents switch
|
|
|
|
|
{
|
|
|
|
|
>= 10_000 => PatronTier.C,
|
|
|
|
|
>= 5000 => PatronTier.L,
|
|
|
|
|
>= 2000 => PatronTier.XX,
|
|
|
|
|
>= 1000 => PatronTier.X,
|
|
|
|
|
>= 500 => PatronTier.V,
|
|
|
|
|
>= 100 => PatronTier.I,
|
|
|
|
|
_ => PatronTier.None
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2024-06-27 08:22:54 +00:00
|
|
|
|
public int PercentBonus(Patron? maybePatron)
|
|
|
|
|
=> maybePatron is { } user && user.ValidThru > DateTime.UtcNow
|
|
|
|
|
? PercentBonus(user.Amount)
|
|
|
|
|
: 0;
|
|
|
|
|
|
|
|
|
|
public int PercentBonus(long amount)
|
|
|
|
|
=> amount switch
|
|
|
|
|
{
|
|
|
|
|
>= 10_000 => 100,
|
|
|
|
|
>= 5000 => 50,
|
|
|
|
|
>= 2000 => 20,
|
|
|
|
|
>= 1000 => 10,
|
|
|
|
|
>= 500 => 5,
|
|
|
|
|
_ => 0
|
|
|
|
|
};
|
|
|
|
|
|
2024-06-18 11:54:32 +00:00
|
|
|
|
private async Task SendWelcomeMessage(Patron patron)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var user = (IUser)_client.GetUser(patron.UserId) ?? await _client.Rest.GetUserAsync(patron.UserId);
|
|
|
|
|
if (user is null)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
var eb = _sender.CreateEmbed()
|
2024-06-27 08:22:54 +00:00
|
|
|
|
.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}");
|
2024-06-18 11:54:32 +00:00
|
|
|
|
|
|
|
|
|
await _sender.Response(user).Embed(eb).SendAsync();
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
Log.Warning("Unable to send a \"Welcome\" message to the patron {UserId}", patron.UserId);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public async Task<(int Success, int Failed)> SendMessageToPatronsAsync(PatronTier tierAndHigher, string message)
|
|
|
|
|
{
|
|
|
|
|
await using var ctx = _db.GetDbContext();
|
|
|
|
|
|
|
|
|
|
var patrons = await ctx.GetTable<PatronUser>()
|
|
|
|
|
.Where(x => x.ValidThru > DateTime.UtcNow)
|
|
|
|
|
.ToArrayAsync();
|
|
|
|
|
|
|
|
|
|
var text = SmartText.CreateFrom(message);
|
|
|
|
|
|
|
|
|
|
var succ = 0;
|
|
|
|
|
var fail = 0;
|
|
|
|
|
foreach (var patron in patrons)
|
|
|
|
|
{
|
|
|
|
|
try
|
|
|
|
|
{
|
|
|
|
|
var user = await _client.GetUserAsync(patron.UserId);
|
|
|
|
|
await _sender.Response(user).Text(text).SendAsync();
|
|
|
|
|
++succ;
|
|
|
|
|
}
|
|
|
|
|
catch
|
|
|
|
|
{
|
|
|
|
|
++fail;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
await Task.Delay(1000);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return (succ, fail);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
public PatronConfigData GetConfig()
|
|
|
|
|
=> _pConf.Data;
|
|
|
|
|
}
|