375 lines
No EOL
11 KiB
C#
375 lines
No EOL
11 KiB
C#
using LinqToDB;
|
|
using LinqToDB.EntityFrameworkCore;
|
|
using Microsoft.Extensions.Caching.Memory;
|
|
using EllieBot.Common.ModuleBehaviors;
|
|
using EllieBot.Db.Models;
|
|
|
|
namespace EllieBot.Modules.Utility;
|
|
|
|
public sealed class GiveawayService : IEService, IReadyExecutor
|
|
{
|
|
public static string GiveawayEmoji = "🎉";
|
|
|
|
private readonly DbService _db;
|
|
private readonly IBotCreds _creds;
|
|
private readonly DiscordSocketClient _client;
|
|
private readonly IMessageSenderService _sender;
|
|
private readonly IBotStrings _strings;
|
|
private readonly ILocalization _localization;
|
|
private readonly IMemoryCache _cache;
|
|
private SortedSet<GiveawayModel> _giveawayCache = new SortedSet<GiveawayModel>();
|
|
private readonly EllieRandom _rng;
|
|
|
|
public GiveawayService(
|
|
DbService db,
|
|
IBotCreds creds,
|
|
DiscordSocketClient client,
|
|
IMessageSenderService sender,
|
|
IBotStrings strings,
|
|
ILocalization localization,
|
|
IMemoryCache cache)
|
|
{
|
|
_db = db;
|
|
_creds = creds;
|
|
_client = client;
|
|
_sender = sender;
|
|
_strings = strings;
|
|
_localization = localization;
|
|
_cache = cache;
|
|
_rng = new EllieRandom();
|
|
|
|
|
|
_client.ReactionAdded += OnReactionAdded;
|
|
_client.ReactionRemoved += OnReactionRemoved;
|
|
}
|
|
|
|
private async Task OnReactionRemoved(
|
|
Cacheable<IUserMessage, ulong> msg,
|
|
Cacheable<IMessageChannel, ulong> arg2,
|
|
SocketReaction r)
|
|
{
|
|
if (!r.User.IsSpecified)
|
|
return;
|
|
|
|
var user = r.User.Value;
|
|
|
|
if (user.IsBot || user.IsWebhook)
|
|
return;
|
|
|
|
if (r.Emote is Emoji e && e.Name == GiveawayEmoji)
|
|
{
|
|
await LeaveGivawayAsync(msg.Id, user.Id);
|
|
}
|
|
}
|
|
|
|
private async Task OnReactionAdded(
|
|
Cacheable<IUserMessage, ulong> msg,
|
|
Cacheable<IMessageChannel, ulong> ch,
|
|
SocketReaction r)
|
|
{
|
|
if (!r.User.IsSpecified)
|
|
return;
|
|
|
|
var user = r.User.Value;
|
|
|
|
if (user.IsBot || user.IsWebhook)
|
|
return;
|
|
|
|
var textChannel = ch.Value as ITextChannel;
|
|
|
|
if (textChannel is null)
|
|
return;
|
|
|
|
|
|
if (r.Emote is Emoji e && e.Name == GiveawayEmoji)
|
|
{
|
|
await JoinGivawayAsync(msg.Id, user.Id, user.Username);
|
|
}
|
|
}
|
|
|
|
public async Task OnReadyAsync()
|
|
{
|
|
// load giveaways for this shard from the database
|
|
await using var ctx = _db.GetDbContext();
|
|
|
|
var gas = await ctx
|
|
.GetTable<GiveawayModel>()
|
|
.Where(x => Linq2DbExpressions.GuildOnShard(x.GuildId, _creds.TotalShards, _client.ShardId))
|
|
.ToArrayAsync();
|
|
|
|
lock (_giveawayCache)
|
|
{
|
|
_giveawayCache = new(gas, Comparer<GiveawayModel>.Create((x, y) => x.EndsAt.CompareTo(y.EndsAt)));
|
|
}
|
|
|
|
var timer = new PeriodicTimer(TimeSpan.FromMinutes(1));
|
|
|
|
while (await timer.WaitForNextTickAsync())
|
|
{
|
|
IEnumerable<GiveawayModel> toEnd;
|
|
lock (_giveawayCache)
|
|
{
|
|
toEnd = _giveawayCache.TakeWhile(
|
|
x => x.EndsAt <= DateTime.UtcNow.AddSeconds(15))
|
|
.ToArray();
|
|
}
|
|
|
|
foreach (var ga in toEnd)
|
|
{
|
|
try
|
|
{
|
|
await EndGiveawayAsync(ga.GuildId, ga.Id);
|
|
}
|
|
catch
|
|
{
|
|
Log.Warning("Failed to end the giveaway with id {Id}", ga.Id);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
public async Task<int?> StartGiveawayAsync(
|
|
ulong guildId,
|
|
ulong channelId,
|
|
ulong messageId,
|
|
TimeSpan duration,
|
|
string message)
|
|
{
|
|
await using var ctx = _db.GetDbContext();
|
|
|
|
// first check if there are more than 5 giveaways
|
|
var count = await ctx
|
|
.GetTable<GiveawayModel>()
|
|
.CountAsync(x => x.GuildId == guildId);
|
|
|
|
if (count >= 5)
|
|
return null;
|
|
|
|
var endsAt = DateTime.UtcNow + duration;
|
|
var ga = await ctx.GetTable<GiveawayModel>()
|
|
.InsertWithOutputAsync(() => new GiveawayModel
|
|
{
|
|
GuildId = guildId,
|
|
MessageId = messageId,
|
|
ChannelId = channelId,
|
|
Message = message,
|
|
EndsAt = endsAt,
|
|
});
|
|
|
|
lock (_giveawayCache)
|
|
{
|
|
_giveawayCache.Add(ga);
|
|
}
|
|
|
|
return ga.Id;
|
|
}
|
|
|
|
|
|
public async Task<bool> EndGiveawayAsync(ulong guildId, int id)
|
|
{
|
|
await using var ctx = _db.GetDbContext();
|
|
|
|
var giveaway = await ctx
|
|
.GetTable<GiveawayModel>()
|
|
.Where(x => x.GuildId == guildId && x.Id == id)
|
|
.LoadWith(x => x.Participants)
|
|
.FirstOrDefaultAsyncLinqToDB();
|
|
|
|
if (giveaway is null)
|
|
return false;
|
|
|
|
await ctx
|
|
.GetTable<GiveawayModel>()
|
|
.Where(x => x.Id == id)
|
|
.DeleteAsync();
|
|
|
|
lock (_giveawayCache)
|
|
{
|
|
_giveawayCache.Remove(giveaway);
|
|
}
|
|
|
|
var winner = PickWinner(giveaway);
|
|
|
|
await OnGiveawayEnded(giveaway, winner);
|
|
|
|
return true;
|
|
}
|
|
|
|
private GiveawayUser? PickWinner(GiveawayModel giveaway)
|
|
{
|
|
if (giveaway.Participants.Count == 0)
|
|
return default;
|
|
|
|
if (giveaway.Participants.Count == 1)
|
|
{
|
|
// as this is the last participant, rerolls no longer possible
|
|
_cache.Remove($"reroll:{giveaway.Id}");
|
|
return giveaway.Participants[0];
|
|
}
|
|
|
|
var winner = giveaway.Participants[_rng.Next(0, giveaway.Participants.Count - 1)];
|
|
|
|
HandleWinnerSelection(giveaway, winner);
|
|
return winner;
|
|
}
|
|
|
|
public async Task<bool> RerollGiveawayAsync(ulong guildId, int giveawayId)
|
|
{
|
|
var rerollModel = _cache.Get<GiveawayRerollData>("reroll:" + giveawayId);
|
|
|
|
if (rerollModel is null)
|
|
return false;
|
|
|
|
var winner = PickWinner(rerollModel.Giveaway);
|
|
|
|
if (winner is not null)
|
|
{
|
|
await OnGiveawayEnded(rerollModel.Giveaway, winner);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public async Task<bool> CancelGiveawayAsync(ulong guildId, int id)
|
|
{
|
|
await using var ctx = _db.GetDbContext();
|
|
|
|
var ga = await ctx
|
|
.GetTable<GiveawayModel>()
|
|
.Where(x => x.GuildId == guildId && x.Id == id)
|
|
.DeleteWithOutputAsync();
|
|
|
|
if (ga is not { Length: > 0 })
|
|
return false;
|
|
|
|
lock (_giveawayCache)
|
|
{
|
|
_giveawayCache.Remove(ga[0]);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
public async Task<IReadOnlyCollection<GiveawayModel>> GetGiveawaysAsync(ulong guildId)
|
|
{
|
|
await using var ctx = _db.GetDbContext();
|
|
|
|
return await ctx
|
|
.GetTable<GiveawayModel>()
|
|
.Where(x => x.GuildId == guildId)
|
|
.ToListAsync();
|
|
}
|
|
|
|
public async Task<bool> JoinGivawayAsync(ulong messageId, ulong userId, string userName)
|
|
{
|
|
await using var ctx = _db.GetDbContext();
|
|
|
|
var giveaway = await ctx
|
|
.GetTable<GiveawayModel>()
|
|
.Where(x => x.MessageId == messageId)
|
|
.FirstOrDefaultAsyncLinqToDB();
|
|
|
|
if (giveaway is null)
|
|
return false;
|
|
|
|
// add the user to the database
|
|
await ctx.GetTable<GiveawayUser>()
|
|
.InsertAsync(
|
|
() => new GiveawayUser()
|
|
{
|
|
UserId = userId,
|
|
GiveawayId = giveaway.Id,
|
|
Name = userName,
|
|
}
|
|
);
|
|
|
|
return true;
|
|
}
|
|
|
|
public async Task<bool> LeaveGivawayAsync(ulong messageId, ulong userId)
|
|
{
|
|
await using var ctx = _db.GetDbContext();
|
|
|
|
var giveaway = await ctx
|
|
.GetTable<GiveawayModel>()
|
|
.Where(x => x.MessageId == messageId)
|
|
.FirstOrDefaultAsyncLinqToDB();
|
|
|
|
if (giveaway is null)
|
|
return false;
|
|
|
|
await ctx
|
|
.GetTable<GiveawayUser>()
|
|
.Where(x => x.UserId == userId && x.GiveawayId == giveaway.Id)
|
|
.DeleteAsync();
|
|
|
|
return true;
|
|
}
|
|
|
|
public async Task OnGiveawayEnded(GiveawayModel ga, GiveawayUser? winner)
|
|
{
|
|
var culture = _localization.GetCultureInfo(ga.GuildId);
|
|
|
|
string GetText(LocStr str)
|
|
=> _strings.GetText(str, culture);
|
|
|
|
var ch = _client.GetChannel(ga.ChannelId) as ITextChannel;
|
|
if (ch is null)
|
|
return;
|
|
|
|
var msg = await ch.GetMessageAsync(ga.MessageId) as IUserMessage;
|
|
if (msg is null)
|
|
return;
|
|
|
|
var winnerStr = winner is null
|
|
? "-"
|
|
: $"""
|
|
{winner.Name}
|
|
<@{winner.UserId}>
|
|
{Format.Code(winner.UserId.ToString())}
|
|
""";
|
|
|
|
var eb = _sender.CreateEmbed(ch.GuildId)
|
|
.WithOkColor()
|
|
.WithTitle(GetText(strs.giveaway_ended))
|
|
.WithDescription(ga.Message)
|
|
.WithFooter($"id: {new kwum(ga.Id).ToString()}")
|
|
.AddField(GetText(strs.winner),
|
|
winnerStr,
|
|
true);
|
|
|
|
try
|
|
{
|
|
await msg.ModifyAsync(x => x.Embed = eb.Build());
|
|
|
|
if (winner is not null)
|
|
await _sender.Response(ch).Message(msg).Text($"🎉 <{winner.UserId}>").SendAsync();
|
|
}
|
|
catch
|
|
{
|
|
_ = msg.DeleteAsync();
|
|
await _sender.Response(ch).Embed(eb).SendAsync();
|
|
}
|
|
}
|
|
|
|
private void HandleWinnerSelection(GiveawayModel ga, GiveawayUser winner)
|
|
{
|
|
ga.Participants = ga.Participants.Where(x => x.UserId != winner.UserId).ToList();
|
|
|
|
var rerollData = new GiveawayRerollData(ga);
|
|
_cache.Set($"reroll:{ga.Id}", rerollData, TimeSpan.FromDays(1));
|
|
}
|
|
}
|
|
|
|
public sealed class GiveawayRerollData
|
|
{
|
|
public GiveawayModel Giveaway { get; init; }
|
|
public DateTime ExpiresAt { get; init; }
|
|
|
|
public GiveawayRerollData(GiveawayModel ga)
|
|
{
|
|
Giveaway = ga;
|
|
|
|
ExpiresAt = DateTime.UtcNow.AddDays(1);
|
|
}
|
|
} |