Refactors. Cleanup. Refactored responses source gen a little. Parametrized localized strings are now generic. Refactored .cash bank interaction. Updated changelog. Reverted clubapps/ban/unban to use .ToString method and configured it to not have right to left unicode. Added extension methods to SocketMessageComponent akin to ones on the IMessageChannel (RespondConfirm, etc...)

This commit is contained in:
Kwoth
2022-05-05 22:59:07 +02:00
parent 9a96ef76ba
commit d80cbb4647
17 changed files with 341 additions and 110 deletions

View File

@@ -3,7 +3,7 @@
Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
## Unreleased
## [4.1.3] - 06.05.2022
### Added
@@ -28,8 +28,16 @@ Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.
- Supports multiple exclusivity groups per message
- Supports level requirements
- However they can only be added one by one
- Use the following commands for more information
- `.h .reroa`
- `.h .reroli`
- `.h .rerot`
- `.h .rerorm`
- `.h .rerodela`
- Pagination is now using buttons instead of reactions
- Bot will now support much higher XP values for global and server levels
- [dev] Small change and generation perf improvement for the localized response strings
### Fixed

View File

@@ -62,6 +62,7 @@ namespace NadekoBot.Generators
sw.WriteLine("{");
sw.Indent++;
var typedParamStrings = new List<string>(10);
foreach (var field in fields)
{
var matches = Regex.Matches(field.Value, @"{(?<num>\d)[}:]");
@@ -71,20 +72,30 @@ namespace NadekoBot.Generators
max = Math.Max(max, int.Parse(match.Groups["num"].Value) + 1);
}
List<string> typedParamStrings = new List<string>();
var paramStrings = string.Empty;
typedParamStrings.Clear();
var typeParams = new string[max];
var passedParamString = string.Empty;
for (var i = 0; i < max; i++)
{
typedParamStrings.Add($"object p{i}");
paramStrings += $", p{i}";
typedParamStrings.Add($"in T{i} p{i}");
passedParamString += $", p{i}";
typeParams[i] = $"T{i}";
}
var sig = string.Empty;
if(max > 0)
var typeParamStr = string.Empty;
if (max > 0)
{
sig = $"({string.Join(", ", typedParamStrings)})";
sw.WriteLine($"public static LocStr {field.Name}{sig} => new LocStr(\"{field.Name}\"{paramStrings});");
typeParamStr = $"<{string.Join(", ", typeParams)}>";
}
sw.WriteLine("public static LocStr {0}{1}{2} => new LocStr(\"{3}\"{4});",
field.Name,
typeParamStr,
sig,
field.Name,
passedParamString);
}
sw.Indent--;

View File

@@ -67,12 +67,13 @@ public sealed class Bot
? GatewayIntents.All
: GatewayIntents.AllUnprivileged,
LogGatewayIntentWarnings = false,
FormatUsersInBidirectionalUnicode = false,
});
_commandService = new(new()
{
CaseSensitiveCommands = false,
DefaultRunMode = RunMode.Sync
DefaultRunMode = RunMode.Sync,
});
// _interactionService = new(Client.Rest);

View File

@@ -0,0 +1,27 @@
namespace NadekoBot;
public sealed class NadekoActionInteraction : NadekoOwnInteraction
{
private readonly NadekoInteractionData _data;
private readonly Func<SocketMessageComponent, Task> _action;
public NadekoActionInteraction(
DiscordSocketClient client,
ulong authorId,
NadekoInteractionData data,
Func<SocketMessageComponent, Task> action
)
: base(client, authorId)
{
_data = data;
_action = action;
}
public override string Name
=> _data.CustomId;
public override IEmote Emote
=> _data.Emote;
public override Task ExecuteOnActionAsync(SocketMessageComponent smc)
=> _action(smc);
}

View File

@@ -1,6 +1,5 @@
namespace NadekoBot;
public abstract class NadekoInteraction
{
// improvements:
@@ -9,20 +8,16 @@ public abstract class NadekoInteraction
// -
public abstract string Name { get; }
public abstract IEmote Emote { get; }
public Func<SocketMessageComponent, Task> OnAction { get; }
protected readonly DiscordSocketClient _client;
protected readonly TaskCompletionSource<bool> _interactionCompletedSource;
protected ulong _authorId;
protected IUserMessage message = null!;
protected NadekoInteraction(DiscordSocketClient client, ulong authorId, Func<SocketMessageComponent, Task> onAction)
protected NadekoInteraction(DiscordSocketClient client)
{
_client = client;
_authorId = authorId;
OnAction = onAction;
_interactionCompletedSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
}
@@ -37,6 +32,7 @@ public abstract class NadekoInteraction
await msg.ModifyAsync(m => m.Components = new ComponentBuilder().Build());
}
protected abstract ValueTask<bool> Validate(SocketMessageComponent smc);
private async Task OnInteraction(SocketInteraction arg)
{
if (arg is not SocketMessageComponent smc)
@@ -48,15 +44,15 @@ public abstract class NadekoInteraction
if (smc.Data.CustomId != Name)
return;
if (smc.User.Id != _authorId)
if (!await Validate(smc))
{
await arg.DeferAsync();
await smc.DeferAsync();
return;
}
_ = Task.Run(async () =>
{
await OnAction(smc);
await ExecuteOnActionAsync(smc);
// this should only be a thing on single-response buttons
_interactionCompletedSource.TrySetResult(true);
@@ -76,5 +72,6 @@ public abstract class NadekoInteraction
return comp.Build();
}
}
public abstract Task ExecuteOnActionAsync(SocketMessageComponent smc);
}

View File

@@ -0,0 +1,41 @@
namespace NadekoBot;
/// <summary>
/// Builder class for NadekoInteractions
/// </summary>
public class NadekoInteractionBuilder
{
private NadekoInteractionData? iData;
private Func<SocketMessageComponent, Task>? action;
// private bool isOwn;
public NadekoInteractionBuilder WithData<T>(in T data)
where T : NadekoInteractionData
{
iData = data;
return this;
}
// public NadekoOwnInteractionBuiler WithIsOwn(bool isOwn = true)
// {
// this.isOwn = isOwn;
// return this;
// }
public NadekoInteractionBuilder WithAction(in Func<SocketMessageComponent, Task> fn)
{
this.action = fn;
return this;
}
public NadekoActionInteraction Build(DiscordSocketClient client, ulong userId)
{
if (iData is null)
throw new InvalidOperationException("You have to specify the data before building the interaction");
if (action is null)
throw new InvalidOperationException("You have to specify the action before building the interaction");
return new(client, userId, iData, action);
}
}

View File

@@ -0,0 +1,8 @@
namespace NadekoBot;
/// <summary>
/// Represents essential interacation data
/// </summary>
/// <param name="Emote">Emote which will show on a button</param>
/// <param name="CustomId">Custom interaction id</param>
public record NadekoInteractionData(IEmote Emote, string CustomId);

View File

@@ -0,0 +1,15 @@
namespace NadekoBot;
/// <summary>
/// Interaction which only the author can use
/// </summary>
public abstract class NadekoOwnInteraction : NadekoInteraction
{
protected readonly ulong _authorId;
protected NadekoOwnInteraction(DiscordSocketClient client, ulong authorId) : base(client)
=> _authorId = authorId;
protected override ValueTask<bool> Validate(SocketMessageComponent smc)
=> new(smc.User.Id == _authorId);
}

View File

@@ -54,7 +54,19 @@ public partial class Gambling
{
var bal = await _bank.GetBalanceAsync(ctx.User.Id);
await ReplyConfirmLocalizedAsync(strs.bank_balance(N(bal)));
var eb = _eb.Create(ctx)
.WithOkColor()
.WithDescription(GetText(strs.bank_balance(N(bal))));
try
{
await ctx.User.EmbedAsync(eb);
await ctx.OkAsync();
}
catch
{
await ReplyErrorLocalizedAsync(strs.unable_to_dm_user);
}
}
}
}

View File

@@ -0,0 +1,17 @@
#nullable disable
namespace NadekoBot.Modules.Gambling;
public class CashInteraction
{
public static NadekoInteractionData Data =
new NadekoInteractionData(new Emoji("🏦"), "cash:bank_show_balance");
public static NadekoInteraction CreateInstance(
DiscordSocketClient client,
ulong userId,
Func<SocketMessageComponent, Task> action)
=> new NadekoInteractionBuilder()
.WithData(Data)
.WithAction(action)
.Build(client, userId);
}

View File

@@ -60,7 +60,7 @@ public partial class Gambling : GamblingModule<GamblingService>
_cache = cache;
_client = client;
_bank = bank;
_enUsCulture = new CultureInfo("en-US", false).NumberFormat;
_enUsCulture.NumberDecimalDigits = 0;
_enUsCulture.NumberGroupSeparator = "";
@@ -89,8 +89,7 @@ public partial class Gambling : GamblingModule<GamblingService>
// [21:03] Bob Page: Kinda remids me of US economy
var embed = _eb.Create()
.WithTitle(GetText(strs.economy_state))
.AddField(GetText(strs.currency_owned),
N(ec.Cash - ec.Bot))
.AddField(GetText(strs.currency_owned), N(ec.Cash - ec.Bot))
.AddField(GetText(strs.currency_one_percent), (onePercent * 100).ToString("F2") + "%")
.AddField(GetText(strs.currency_planted), N(ec.Planted))
.AddField(GetText(strs.owned_waifus_total), N(ec.Waifus))
@@ -237,7 +236,6 @@ public partial class Gambling : GamblingModule<GamblingService>
var kwumId = new kwum(tr.Id).ToString();
var date = $"#{Format.Code(kwumId)} `〖{GetFormattedCurtrDate(tr)}〗`";
sb.AppendLine($"\\{change} {date} {Format.Bold(N(tr.Amount))}");
var transactionString = GetHumanReadableTransaction(tr.Type, tr.Extra, tr.OtherId);
if (transactionString is not null)
@@ -265,8 +263,7 @@ public partial class Gambling : GamblingModule<GamblingService>
int intId = id;
await using var uow = _db.GetDbContext();
var tr = await uow.CurrencyTransactions
.ToLinqToDBTable()
var tr = await uow.CurrencyTransactions.ToLinqToDBTable()
.Where(x => x.Id == intId && x.UserId == ctx.User.Id)
.FirstOrDefaultAsync();
@@ -276,8 +273,7 @@ public partial class Gambling : GamblingModule<GamblingService>
return;
}
var eb = _eb.Create(ctx)
.WithOkColor();
var eb = _eb.Create(ctx).WithOkColor();
eb.WithAuthor(ctx.User);
eb.WithTitle(GetText(strs.transaction));
@@ -296,7 +292,6 @@ public partial class Gambling : GamblingModule<GamblingService>
eb.AddField("Note", tr.Note);
}
eb.WithFooter(GetFormattedCurtrDate(tr));
await ctx.Channel.EmbedAsync(eb);
@@ -316,31 +311,6 @@ public partial class Gambling : GamblingModule<GamblingService>
(_, _, ulong userId) => $"{type.Titleize()} - {subType.Titleize()} | [{userId}]",
_ => $"{type.Titleize()} - {subType.Titleize()}"
};
public sealed class CashInteraction : NadekoInteraction
{
public override string Name
=> "CASH_OPEN_BANK";
public override IEmote Emote
=> new Emoji("🏦");
public CashInteraction(
[NotNull] DiscordSocketClient client,
ulong authorId,
Func<SocketMessageComponent, Task> onAction)
: base(client, authorId, onAction)
{
}
public static CashInteraction Create(
DiscordSocketClient client,
ulong userId,
Func<SocketMessageComponent, Task> onAction)
=> new(client, userId, onAction);
}
[Cmd]
[Priority(0)]
@@ -349,30 +319,37 @@ public partial class Gambling : GamblingModule<GamblingService>
var cur = await GetBalanceStringAsync(userId);
await ReplyConfirmLocalizedAsync(strs.has(Format.Code(userId.ToString()), cur));
}
private async Task BankAction(SocketMessageComponent smc)
{
var balance = await _bank.GetBalanceAsync(ctx.User.Id);
await smc.RespondAsync(GetText(strs.bank_balance(N(balance))), ephemeral: true);
await N(balance)
.Pipe(strs.bank_balance)
.Pipe(GetText)
.Pipe(text => smc.RespondConfirmAsync(_eb, text, ephemeral: true));
}
private NadekoInteraction CreateCashInteraction()
=> CashInteraction.CreateInstance(_client, ctx.User.Id, BankAction);
[Cmd]
[Priority(1)]
public async partial Task Cash([Leftover] IUser user = null)
{
user ??= ctx.User;
var cur = await GetBalanceStringAsync(user.Id);
var inter = user == ctx.User
? CreateCashInteraction()
: null;
if (user == ctx.User)
{
var inter = CashInteraction.Create(_client, ctx.User.Id, BankAction);
await ConfirmLocalizedAsync(strs.has(Format.Bold(user.ToString()), cur), inter);
}
else
{
await ConfirmLocalizedAsync(strs.has(Format.Bold(user.ToString()), cur));
}
await ConfirmLocalizedAsync(
user.ToString()
.Pipe(Format.Bold)
.With(cur)
.Pipe(strs.has),
inter);
}
[Cmd]
@@ -432,10 +409,7 @@ public partial class Gambling : GamblingModule<GamblingService>
return;
}
await _cs.AddAsync(usr.Id,
amount,
new("award", ctx.User.ToString()!, msg, ctx.User.Id)
);
await _cs.AddAsync(usr.Id, amount, new("award", ctx.User.ToString()!, msg, ctx.User.Id));
await ReplyConfirmLocalizedAsync(strs.awarded(N(amount), $"<@{usrId}>"));
}
@@ -449,10 +423,7 @@ public partial class Gambling : GamblingModule<GamblingService>
await _cs.AddBulkAsync(users.Select(x => x.Id).ToList(),
amount,
new("award",
ctx.User.ToString()!,
role.Name,
ctx.User.Id));
new("award", ctx.User.ToString()!, role.Name, ctx.User.Id));
await ReplyConfirmLocalizedAsync(strs.mass_award(N(amount),
Format.Bold(users.Count.ToString()),
@@ -469,10 +440,7 @@ public partial class Gambling : GamblingModule<GamblingService>
await _cs.RemoveBulkAsync(users.Select(x => x.Id).ToList(),
amount,
new("take",
ctx.User.ToString()!,
null,
ctx.User.Id));
new("take", ctx.User.ToString()!, null, ctx.User.Id));
await ReplyConfirmLocalizedAsync(strs.mass_take(N(amount),
Format.Bold(users.Count.ToString()),
@@ -490,10 +458,7 @@ public partial class Gambling : GamblingModule<GamblingService>
return;
}
var extra = new TxData("take",
ctx.User.ToString()!,
null,
ctx.User.Id);
var extra = new TxData("take", ctx.User.ToString()!, null, ctx.User.Id);
if (await _cs.RemoveAsync(user.Id, amount, extra))
{
@@ -505,7 +470,6 @@ public partial class Gambling : GamblingModule<GamblingService>
}
}
[Cmd]
[OwnerOnly]
public async partial Task Take(long amount, [Leftover] ulong usrId)
@@ -515,10 +479,7 @@ public partial class Gambling : GamblingModule<GamblingService>
return;
}
var extra = new TxData("take",
ctx.User.ToString()!,
null,
ctx.User.Id);
var extra = new TxData("take", ctx.User.ToString()!, null, ctx.User.Id);
if (await _cs.RemoveAsync(usrId, amount, extra))
{
@@ -606,10 +567,7 @@ public partial class Gambling : GamblingModule<GamblingService>
}
else
{
await rdMsg.ModifyAsync(x =>
{
x.Embed = embed.Build();
});
await rdMsg.ModifyAsync(x => { x.Embed = embed.Build(); });
}
}
@@ -659,7 +617,6 @@ public partial class Gambling : GamblingModule<GamblingService>
var result = br.Roll();
var str = Format.Bold(ctx.User.ToString()) + Format.Code(GetText(strs.roll(result.Roll)));
if (result.Multiplier > 0)
{
@@ -788,9 +745,7 @@ public partial class Gambling : GamblingModule<GamblingService>
if (amount > 0)
{
if (!await _cs.RemoveAsync(ctx.User.Id,
amount,
new("rps", "bet", "")))
if (!await _cs.RemoveAsync(ctx.User.Id, amount, new("rps", "bet", "")))
{
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
return;

View File

@@ -39,13 +39,19 @@ public abstract class GamblingModule<TService> : NadekoModule<TService>
return true;
}
public static string N(long cur, IFormatProvider format)
=> cur.ToString("C0", format);
public static string N(decimal cur, IFormatProvider format)
=> cur.ToString("C0", format);
protected string N(long cur)
=> cur.ToString("C0", GetFlowersCiInternal());
=> N(cur, GetFlowersCiInternal());
protected string N(decimal cur)
=> cur.ToString("C0", GetFlowersCiInternal());
=> N(cur, GetFlowersCiInternal());
private IFormatProvider GetFlowersCiInternal()
protected IFormatProvider GetFlowersCiInternal()
{
var flowersCi = (CultureInfo)Culture.Clone();
flowersCi.NumberFormat.CurrencySymbol = CurrencySign;

View File

@@ -240,7 +240,7 @@ public partial class Xp
[Cmd]
[Priority(1)]
public partial Task ClubAccept(IUser user)
=> ClubAccept($"{user.Username}#{user.Discriminator}");
=> ClubAccept(user.ToString());
[Cmd]
[Priority(0)]
@@ -282,7 +282,7 @@ public partial class Xp
[Cmd]
[Priority(1)]
public partial Task ClubBan([Leftover] IUser user)
=> ClubBan($"{user.Username}#{user.Discriminator}");
=> ClubBan(user.ToString());
[Cmd]
[Priority(0)]
@@ -300,7 +300,7 @@ public partial class Xp
[Cmd]
[Priority(1)]
public partial Task ClubUnBan([Leftover] IUser user)
=> ClubUnBan($"{user.Username}#{user.Discriminator}");
=> ClubUnBan(user.ToString());
[Cmd]
[Priority(0)]

View File

@@ -0,0 +1,17 @@
#nullable disable
using System.Globalization;
namespace NadekoBot.Services;
public static class BotStringsExtensions
{
// this one is for pipe fun, see PipeExtensions.cs
public static string GetText(this IBotStrings strings, in LocStr str, in ulong guildId)
=> strings.GetText(str.Key, guildId, str.Params);
public static string GetText(this IBotStrings strings, in LocStr str, ulong? guildId = null)
=> strings.GetText(str.Key, guildId, str.Params);
public static string GetText(this IBotStrings strings, in LocStr str, CultureInfo culture)
=> strings.GetText(str.Key, culture, str.Params);
}

View File

@@ -214,10 +214,4 @@ public static class Extensions
=> msg.Content.Headers.ContentLength is long length
? length
: long.MaxValue;
public static string GetText(this IBotStrings strings, in LocStr str, ulong? guildId = null)
=> strings.GetText(str.Key, guildId, str.Params);
public static string GetText(this IBotStrings strings, in LocStr str, CultureInfo culture)
=> strings.GetText(str.Key, culture, str.Params);
}

View File

@@ -0,0 +1,23 @@
namespace NadekoBot.Extensions;
public delegate TOut PipeFunc<TIn, out TOut>(in TIn a);
public delegate TOut PipeFunc<TIn1, TIn2, out TOut>(in TIn1 a, in TIn2 b);
public static class PipeExtensions
{
public static TOut Pipe<TIn, TOut>(this TIn a, Func<TIn, TOut> fn)
=> fn(a);
public static TOut Pipe<TIn, TOut>(this TIn a, PipeFunc<TIn, TOut> fn)
=> fn(a);
public static TOut Pipe<TIn1, TIn2, TOut>(this (TIn1, TIn2) a, PipeFunc<TIn1, TIn2, TOut> fn)
=> fn(a.Item1, a.Item2);
public static (TIn, TExtra) With<TIn, TExtra>(this TIn a, TExtra b)
=> (a, b);
public static async Task<TOut> Pipe<TIn, TOut>(this Task<TIn> a, Func<TIn, TOut> fn)
=> fn(await a);
}

View File

@@ -0,0 +1,99 @@
namespace NadekoBot.Extensions;
public static class SocketMessageComponentExtensions
{
public static Task RespondAsync(
this SocketMessageComponent smc,
string? plainText,
Embed? embed = null,
IReadOnlyCollection<Embed>? embeds = null,
bool sanitizeAll = false,
MessageComponent? components = null,
bool ephemeral = true)
{
plainText = sanitizeAll
? plainText?.SanitizeAllMentions() ?? ""
: plainText?.SanitizeMentions() ?? "";
return smc.RespondAsync(plainText,
embed: embed,
embeds: embeds is null
? null
: embeds as Embed[] ?? embeds.ToArray(),
components: components,
ephemeral: ephemeral,
options: new()
{
RetryMode = RetryMode.AlwaysRetry
});
}
public static Task RespondAsync(
this SocketMessageComponent smc,
SmartText text,
bool sanitizeAll = false,
bool ephemeral = true)
=> text switch
{
SmartEmbedText set => smc.RespondAsync(set.PlainText,
set.GetEmbed().Build(),
sanitizeAll: sanitizeAll,
ephemeral: ephemeral),
SmartPlainText st => smc.RespondAsync(st.Text,
default(Embed),
sanitizeAll: sanitizeAll,
ephemeral: ephemeral),
SmartEmbedTextArray arr => smc.RespondAsync(arr.Content,
embeds: arr.GetEmbedBuilders().Map(e => e.Build()),
ephemeral: ephemeral),
_ => throw new ArgumentOutOfRangeException(nameof(text))
};
public static Task EmbedAsync(
this SocketMessageComponent smc,
IEmbedBuilder? embed,
string plainText = "",
IReadOnlyCollection<IEmbedBuilder>? embeds = null,
NadekoInteraction? inter = null,
bool ephemeral = false)
=> smc.RespondAsync(plainText,
embed: embed?.Build(),
embeds: embeds?.Map(x => x.Build()));
public static Task RespondAsync(
this SocketMessageComponent ch,
IEmbedBuilderService eb,
string text,
MessageType type,
bool ephemeral = false,
NadekoInteraction? inter = null)
{
var builder = eb.Create().WithDescription(text);
builder = (type switch
{
MessageType.Error => builder.WithErrorColor(),
MessageType.Ok => builder.WithOkColor(),
MessageType.Pending => builder.WithPendingColor(),
_ => throw new ArgumentOutOfRangeException(nameof(type))
});
return ch.EmbedAsync(builder, inter: inter, ephemeral: ephemeral);
}
// embed title and optional footer overloads
public static Task RespondErrorAsync(
this SocketMessageComponent smc,
IEmbedBuilderService eb,
string text,
bool ephemeral = false)
=> smc.RespondAsync(eb, text, MessageType.Error, ephemeral);
public static Task RespondConfirmAsync(
this SocketMessageComponent smc,
IEmbedBuilderService eb,
string text,
bool ephemeral = false)
=> smc.RespondAsync(eb, text, MessageType.Ok, ephemeral);
}