Files
nadekobot/src/NadekoBot/Modules/Gambling/Events/GameStatusEvent.cs
Kwoth 9b9fa2f357 - Fixed some potential causes for ratelimit due to default message retry settings
- Fixed a patron rewards bug caused by monthly donation checking not accounting for year increase
- Fixed a patron rewards bug for users who connected the same discord account with multiple patreon accounts
2023-01-09 02:05:33 +01:00

196 lines
6.0 KiB
C#

#nullable disable
using NadekoBot.Services.Database.Models;
using System.Collections.Concurrent;
using Nadeko.Common;
namespace NadekoBot.Modules.Gambling.Common.Events;
public class GameStatusEvent : ICurrencyEvent
{
public event Func<ulong, Task> OnEnded;
private long PotSize { get; set; }
public bool Stopped { get; private set; }
public bool PotEmptied { get; private set; }
private readonly DiscordSocketClient _client;
private readonly IGuild _guild;
private IUserMessage msg;
private readonly ICurrencyService _cs;
private readonly long _amount;
private readonly Func<CurrencyEvent.Type, EventOptions, long, IEmbedBuilder> _embedFunc;
private readonly bool _isPotLimited;
private readonly ITextChannel _channel;
private readonly ConcurrentHashSet<ulong> _awardedUsers = new();
private readonly ConcurrentQueue<ulong> _toAward = new();
private readonly Timer _t;
private readonly Timer _timeout;
private readonly EventOptions _opts;
private readonly string _code;
private readonly char[] _sneakyGameStatusChars = Enumerable.Range(48, 10)
.Concat(Enumerable.Range(65, 26))
.Concat(Enumerable.Range(97, 26))
.Select(x => (char)x)
.ToArray();
private readonly object _stopLock = new();
private readonly object _potLock = new();
public GameStatusEvent(
DiscordSocketClient client,
ICurrencyService cs,
SocketGuild g,
ITextChannel ch,
EventOptions opt,
Func<CurrencyEvent.Type, EventOptions, long, IEmbedBuilder> embedFunc)
{
_client = client;
_guild = g;
_cs = cs;
_amount = opt.Amount;
PotSize = opt.PotSize;
_embedFunc = embedFunc;
_isPotLimited = PotSize > 0;
_channel = ch;
_opts = opt;
// generate code
_code = new(_sneakyGameStatusChars.Shuffle().Take(5).ToArray());
_t = new(OnTimerTick, null, Timeout.InfiniteTimeSpan, TimeSpan.FromSeconds(2));
if (_opts.Hours > 0)
_timeout = new(EventTimeout, null, TimeSpan.FromHours(_opts.Hours), Timeout.InfiniteTimeSpan);
}
private void EventTimeout(object state)
=> _ = StopEvent();
private async void OnTimerTick(object state)
{
var potEmpty = PotEmptied;
var toAward = new List<ulong>();
while (_toAward.TryDequeue(out var x))
toAward.Add(x);
if (!toAward.Any())
return;
try
{
await _cs.AddBulkAsync(toAward,
_amount,
new("event", "gamestatus")
);
if (_isPotLimited)
{
await msg.ModifyAsync(m =>
{
m.Embed = GetEmbed(PotSize).Build();
});
}
Log.Information("Game status event awarded {Count} users {Amount} currency.{Remaining}",
toAward.Count,
_amount,
_isPotLimited ? $" {PotSize} left." : "");
if (potEmpty)
_ = StopEvent();
}
catch (Exception ex)
{
Log.Warning(ex, "Error in OnTimerTick in gamestatusevent");
}
}
public async Task StartEvent()
{
msg = await _channel.EmbedAsync(GetEmbed(_opts.PotSize));
await _client.SetGameAsync(_code);
_client.MessageDeleted += OnMessageDeleted;
_client.MessageReceived += HandleMessage;
_t.Change(TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(2));
}
private IEmbedBuilder GetEmbed(long pot)
=> _embedFunc(CurrencyEvent.Type.GameStatus, _opts, pot);
private async Task OnMessageDeleted(Cacheable<IMessage, ulong> message, Cacheable<IMessageChannel, ulong> cacheable)
{
if (message.Id == msg.Id)
await StopEvent();
}
public Task StopEvent()
{
lock (_stopLock)
{
if (Stopped)
return Task.CompletedTask;
Stopped = true;
_client.MessageDeleted -= OnMessageDeleted;
_client.MessageReceived -= HandleMessage;
_t.Change(Timeout.Infinite, Timeout.Infinite);
_timeout?.Change(Timeout.Infinite, Timeout.Infinite);
_ = _client.SetGameAsync(null);
try
{
_ = msg.DeleteAsync();
}
catch { }
_ = OnEnded?.Invoke(_guild.Id);
}
return Task.CompletedTask;
}
private Task HandleMessage(SocketMessage message)
{
_ = Task.Run(async () =>
{
if (message.Author is not IGuildUser gu // no unknown users, as they could be bots, or alts
|| gu.IsBot // no bots
|| message.Content != _code // code has to be the same
|| (DateTime.UtcNow - gu.CreatedAt).TotalDays <= 5) // no recently created accounts
return;
// there has to be money left in the pot
// and the user wasn't rewarded
if (_awardedUsers.Add(message.Author.Id) && TryTakeFromPot())
{
_toAward.Enqueue(message.Author.Id);
if (_isPotLimited && PotSize < _amount)
PotEmptied = true;
}
try
{
await message.DeleteAsync(new()
{
RetryMode = RetryMode.AlwaysFail
});
}
catch { }
});
return Task.CompletedTask;
}
private bool TryTakeFromPot()
{
if (_isPotLimited)
{
lock (_potLock)
{
if (PotSize < _amount)
return false;
PotSize -= _amount;
return true;
}
}
return true;
}
}