mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-09-11 17:58:26 -04:00
Restructured the project structure back to the way it was, there's no reasonable way to split the modules
This commit is contained in:
@@ -0,0 +1,60 @@
|
||||
#nullable disable
|
||||
using NadekoBot.Modules.Gambling.Common;
|
||||
using NadekoBot.Modules.Gambling.Common.Events;
|
||||
using NadekoBot.Modules.Gambling.Services;
|
||||
using Nadeko.Bot.Db.Models;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling;
|
||||
|
||||
public partial class Gambling
|
||||
{
|
||||
[Group]
|
||||
public partial class CurrencyEventsCommands : GamblingSubmodule<CurrencyEventsService>
|
||||
{
|
||||
public CurrencyEventsCommands(GamblingConfigService gamblingConf)
|
||||
: base(gamblingConf)
|
||||
{
|
||||
}
|
||||
|
||||
[Cmd]
|
||||
[RequireContext(ContextType.Guild)]
|
||||
[NadekoOptions<EventOptions>]
|
||||
[OwnerOnly]
|
||||
public async Task EventStart(CurrencyEvent.Type ev, params string[] options)
|
||||
{
|
||||
var (opts, _) = OptionsParser.ParseFrom(new EventOptions(), options);
|
||||
if (!await _service.TryCreateEventAsync(ctx.Guild.Id, ctx.Channel.Id, ev, opts, GetEmbed))
|
||||
await ReplyErrorLocalizedAsync(strs.start_event_fail);
|
||||
}
|
||||
|
||||
private IEmbedBuilder GetEmbed(CurrencyEvent.Type type, EventOptions opts, long currentPot)
|
||||
=> type switch
|
||||
{
|
||||
CurrencyEvent.Type.Reaction => _eb.Create()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.event_title(type.ToString())))
|
||||
.WithDescription(GetReactionDescription(opts.Amount, currentPot))
|
||||
.WithFooter(GetText(strs.event_duration_footer(opts.Hours))),
|
||||
CurrencyEvent.Type.GameStatus => _eb.Create()
|
||||
.WithOkColor()
|
||||
.WithTitle(GetText(strs.event_title(type.ToString())))
|
||||
.WithDescription(GetGameStatusDescription(opts.Amount, currentPot))
|
||||
.WithFooter(GetText(strs.event_duration_footer(opts.Hours))),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(type))
|
||||
};
|
||||
|
||||
private string GetReactionDescription(long amount, long potSize)
|
||||
{
|
||||
var potSizeStr = Format.Bold(potSize == 0 ? "∞" + CurrencySign : N(potSize));
|
||||
|
||||
return GetText(strs.new_reaction_event(CurrencySign, Format.Bold(N(amount)), potSizeStr));
|
||||
}
|
||||
|
||||
private string GetGameStatusDescription(long amount, long potSize)
|
||||
{
|
||||
var potSizeStr = Format.Bold(potSize == 0 ? "∞" + CurrencySign : potSize + CurrencySign);
|
||||
|
||||
return GetText(strs.new_gamestatus_event(CurrencySign, Format.Bold(N(amount)), potSizeStr));
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,67 @@
|
||||
#nullable disable
|
||||
using NadekoBot.Modules.Gambling.Common;
|
||||
using NadekoBot.Modules.Gambling.Common.Events;
|
||||
using Nadeko.Bot.Db.Models;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling.Services;
|
||||
|
||||
public class CurrencyEventsService : INService
|
||||
{
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly ICurrencyService _cs;
|
||||
private readonly GamblingConfigService _configService;
|
||||
|
||||
private readonly ConcurrentDictionary<ulong, ICurrencyEvent> _events = new();
|
||||
|
||||
public CurrencyEventsService(DiscordSocketClient client, ICurrencyService cs, GamblingConfigService configService)
|
||||
{
|
||||
_client = client;
|
||||
_cs = cs;
|
||||
_configService = configService;
|
||||
}
|
||||
|
||||
public async Task<bool> TryCreateEventAsync(
|
||||
ulong guildId,
|
||||
ulong channelId,
|
||||
CurrencyEvent.Type type,
|
||||
EventOptions opts,
|
||||
Func<CurrencyEvent.Type, EventOptions, long, IEmbedBuilder> embed)
|
||||
{
|
||||
var g = _client.GetGuild(guildId);
|
||||
if (g?.GetChannel(channelId) is not ITextChannel ch)
|
||||
return false;
|
||||
|
||||
ICurrencyEvent ce;
|
||||
|
||||
if (type == CurrencyEvent.Type.Reaction)
|
||||
ce = new ReactionEvent(_client, _cs, g, ch, opts, _configService.Data, embed);
|
||||
else if (type == CurrencyEvent.Type.GameStatus)
|
||||
ce = new GameStatusEvent(_client, _cs, g, ch, opts, embed);
|
||||
else
|
||||
return false;
|
||||
|
||||
var added = _events.TryAdd(guildId, ce);
|
||||
if (added)
|
||||
{
|
||||
try
|
||||
{
|
||||
ce.OnEnded += OnEventEnded;
|
||||
await ce.StartEvent();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error starting event");
|
||||
_events.TryRemove(guildId, out ce);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return added;
|
||||
}
|
||||
|
||||
private Task OnEventEnded(ulong gid)
|
||||
{
|
||||
_events.TryRemove(gid, out _);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
39
src/NadekoBot/Modules/Gambling/Events/EventOptions.cs
Normal file
39
src/NadekoBot/Modules/Gambling/Events/EventOptions.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
#nullable disable
|
||||
using CommandLine;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling.Common.Events;
|
||||
|
||||
public class EventOptions : INadekoCommandOptions
|
||||
{
|
||||
[Option('a', "amount", Required = false, Default = 100, HelpText = "Amount of currency each user receives.")]
|
||||
public long Amount { get; set; } = 100;
|
||||
|
||||
[Option('p',
|
||||
"pot-size",
|
||||
Required = false,
|
||||
Default = 0,
|
||||
HelpText = "The maximum amount of currency that can be rewarded. 0 means no limit.")]
|
||||
public long PotSize { get; set; }
|
||||
|
||||
//[Option('t', "type", Required = false, Default = "reaction", HelpText = "Type of the event. reaction, gamestatus or joinserver.")]
|
||||
//public string TypeString { get; set; } = "reaction";
|
||||
[Option('d',
|
||||
"duration",
|
||||
Required = false,
|
||||
Default = 24,
|
||||
HelpText = "Number of hours the event should run for. Default 24.")]
|
||||
public int Hours { get; set; } = 24;
|
||||
|
||||
|
||||
public void NormalizeOptions()
|
||||
{
|
||||
if (Amount < 0)
|
||||
Amount = 100;
|
||||
if (PotSize < 0)
|
||||
PotSize = 0;
|
||||
if (Hours <= 0)
|
||||
Hours = 24;
|
||||
if (PotSize != 0 && PotSize < Amount)
|
||||
PotSize = 0;
|
||||
}
|
||||
}
|
195
src/NadekoBot/Modules/Gambling/Events/GameStatusEvent.cs
Normal file
195
src/NadekoBot/Modules/Gambling/Events/GameStatusEvent.cs
Normal file
@@ -0,0 +1,195 @@
|
||||
#nullable disable
|
||||
using Nadeko.Bot.Db.Models;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
9
src/NadekoBot/Modules/Gambling/Events/ICurrencyEvent.cs
Normal file
9
src/NadekoBot/Modules/Gambling/Events/ICurrencyEvent.cs
Normal file
@@ -0,0 +1,9 @@
|
||||
#nullable disable
|
||||
namespace NadekoBot.Modules.Gambling.Common;
|
||||
|
||||
public interface ICurrencyEvent
|
||||
{
|
||||
event Func<ulong, Task> OnEnded;
|
||||
Task StopEvent();
|
||||
Task StartEvent();
|
||||
}
|
194
src/NadekoBot/Modules/Gambling/Events/ReactionEvent.cs
Normal file
194
src/NadekoBot/Modules/Gambling/Events/ReactionEvent.cs
Normal file
@@ -0,0 +1,194 @@
|
||||
#nullable disable
|
||||
using Nadeko.Bot.Db.Models;
|
||||
|
||||
namespace NadekoBot.Modules.Gambling.Common.Events;
|
||||
|
||||
public class ReactionEvent : 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 IEmote emote;
|
||||
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 System.Collections.Concurrent.ConcurrentQueue<ulong> _toAward = new();
|
||||
private readonly Timer _t;
|
||||
private readonly Timer _timeout;
|
||||
private readonly bool _noRecentlyJoinedServer;
|
||||
private readonly EventOptions _opts;
|
||||
private readonly GamblingConfig _config;
|
||||
|
||||
private readonly object _stopLock = new();
|
||||
|
||||
private readonly object _potLock = new();
|
||||
|
||||
public ReactionEvent(
|
||||
DiscordSocketClient client,
|
||||
ICurrencyService cs,
|
||||
SocketGuild g,
|
||||
ITextChannel ch,
|
||||
EventOptions opt,
|
||||
GamblingConfig config,
|
||||
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;
|
||||
_noRecentlyJoinedServer = false;
|
||||
_opts = opt;
|
||||
_config = config;
|
||||
|
||||
_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", "reaction"));
|
||||
|
||||
if (_isPotLimited)
|
||||
{
|
||||
await msg.ModifyAsync(m =>
|
||||
{
|
||||
m.Embed = GetEmbed(PotSize).Build();
|
||||
});
|
||||
}
|
||||
|
||||
Log.Information("Reaction Event awarded {Count} users {Amount} currency.{Remaining}",
|
||||
toAward.Count,
|
||||
_amount,
|
||||
_isPotLimited ? $" {PotSize} left." : "");
|
||||
|
||||
if (potEmpty)
|
||||
_ = StopEvent();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error adding bulk currency to users");
|
||||
}
|
||||
}
|
||||
|
||||
public async Task StartEvent()
|
||||
{
|
||||
if (Emote.TryParse(_config.Currency.Sign, out var parsedEmote))
|
||||
emote = parsedEmote;
|
||||
else
|
||||
emote = new Emoji(_config.Currency.Sign);
|
||||
msg = await _channel.EmbedAsync(GetEmbed(_opts.PotSize));
|
||||
await msg.AddReactionAsync(emote);
|
||||
_client.MessageDeleted += OnMessageDeleted;
|
||||
_client.ReactionAdded += HandleReaction;
|
||||
_t.Change(TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(2));
|
||||
}
|
||||
|
||||
private IEmbedBuilder GetEmbed(long pot)
|
||||
=> _embedFunc(CurrencyEvent.Type.Reaction, _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.ReactionAdded -= HandleReaction;
|
||||
_t.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
_timeout?.Change(Timeout.Infinite, Timeout.Infinite);
|
||||
try
|
||||
{
|
||||
_ = msg.DeleteAsync();
|
||||
}
|
||||
catch { }
|
||||
|
||||
_ = OnEnded?.Invoke(_guild.Id);
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task HandleReaction(
|
||||
Cacheable<IUserMessage, ulong> message,
|
||||
Cacheable<IMessageChannel, ulong> cacheable,
|
||||
SocketReaction r)
|
||||
{
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
if (emote.Name != r.Emote.Name)
|
||||
return;
|
||||
if ((r.User.IsSpecified
|
||||
? r.User.Value
|
||||
: null) is not IGuildUser gu // no unknown users, as they could be bots, or alts
|
||||
|| message.Id != msg.Id // same message
|
||||
|| gu.IsBot // no bots
|
||||
|| (DateTime.UtcNow - gu.CreatedAt).TotalDays <= 5 // no recently created accounts
|
||||
|| (_noRecentlyJoinedServer
|
||||
&& // if specified, no users who joined the server in the last 24h
|
||||
(gu.JoinedAt is null
|
||||
|| (DateTime.UtcNow - gu.JoinedAt.Value).TotalDays
|
||||
< 1))) // and no users for who we don't know when they joined
|
||||
return;
|
||||
// there has to be money left in the pot
|
||||
// and the user wasn't rewarded
|
||||
if (_awardedUsers.Add(r.UserId) && TryTakeFromPot())
|
||||
{
|
||||
_toAward.Enqueue(r.UserId);
|
||||
if (_isPotLimited && PotSize < _amount)
|
||||
PotEmptied = true;
|
||||
}
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private bool TryTakeFromPot()
|
||||
{
|
||||
if (_isPotLimited)
|
||||
{
|
||||
lock (_potLock)
|
||||
{
|
||||
if (PotSize < _amount)
|
||||
return false;
|
||||
|
||||
PotSize -= _amount;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
Reference in New Issue
Block a user