- Added a simple bank system. Users can deposit, withdraw and check the balance of their currency in the bank.

- Users can't check other user's bank balances.
- Added a button on a .$ command which, when clicked, sends you a message with your bank balance that only you can see.
- Updated pagination, it now uses buttons instead of reactions
- using .h <command group> (atm only .bank is a proper group) will list commands with their descriptions in that group
This commit is contained in:
Kwoth
2022-04-29 06:47:00 +02:00
parent 3b6b3bcf07
commit f132aa2624
25 changed files with 10588 additions and 78 deletions

View File

@@ -1,3 +1,4 @@
# Changelog
Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
@@ -13,6 +14,13 @@ Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.
- Old embed format will still work
- There shouldn't be any breaking changes
- Added `.stondel` command which, when toggled, will make the bot delete online stream messages on the server when the stream goes offline
- Added a simple bank system.
- Users can deposit, withdraw and check the balance of their currency in the bank.
- Users can't check other user's bank balances.
- Added a button on a .$ command which, when clicked, sends you a message with your bank balance that only you can see.
- Added `.h <command group>`
- Using this command will list all commands in the specified group
- Atm only .bank is a proper group (`.h bank`)
### Changed

View File

@@ -5,6 +5,20 @@ namespace NadekoBot;
public static class MedusaExtensions
{
// public static Task<IUserMessage> EmbedAsync(this IMessageChannel ch,
// IEmbedBuilder embed,
// string msg = "",
// MessageComponent? components = null)
// {
// return ch.SendMessageAsync(msg,
// embed: embed.Build(),
// components: components,
// options: new()
// {
// RetryMode = RetryMode.AlwaysRetry
// });
// }
public static Task<IUserMessage> EmbedAsync(this IMessageChannel ch,
IEmbedBuilder embed,
string msg = "",
@@ -37,7 +51,7 @@ public static class MedusaExtensions
public static Task<IUserMessage> SendErrorAsync(this AnyContext ctx, string msg)
=> ctx.Channel.SendErrorAsync(ctx, msg);
// localized
// reaction responses
public static Task ConfirmAsync(this AnyContext ctx)
=> ctx.Message.AddReactionAsync(new Emoji("✅"));
@@ -50,6 +64,7 @@ public static class MedusaExtensions
public static Task WaitAsync(this AnyContext ctx)
=> ctx.Message.AddReactionAsync(new Emoji("🤔"));
// localized
public static Task<IUserMessage> ErrorLocalizedAsync(this AnyContext ctx, string key, params object[]? args)
=> ctx.SendErrorAsync(ctx.GetText(key));

View File

@@ -0,0 +1,80 @@
namespace NadekoBot;
public abstract class NadekoInteraction
{
// improvements:
// - state in OnAction
// - configurable delay
// -
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;
protected NadekoInteraction(DiscordSocketClient client, ulong authorId, Func<SocketMessageComponent, Task> onAction)
{
_client = client;
_authorId = authorId;
OnAction = onAction;
_interactionCompletedSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
}
public async Task RunAsync(IUserMessage msg)
{
message = msg;
_client.InteractionCreated += OnInteraction;
await Task.WhenAny(Task.Delay(10_000), _interactionCompletedSource.Task);
_client.InteractionCreated -= OnInteraction;
await msg.ModifyAsync(m => m.Components = new ComponentBuilder().Build());
}
private async Task OnInteraction(SocketInteraction arg)
{
if (arg is not SocketMessageComponent smc)
return;
if (smc.Message.Id != message.Id)
return;
if (smc.Data.CustomId != Name)
return;
if (smc.User.Id != _authorId)
{
await arg.DeferAsync();
return;
}
_ = Task.Run(async () =>
{
await OnAction(smc);
// this should only be a thing on single-response buttons
_interactionCompletedSource.TrySetResult(true);
if (!smc.HasResponded)
{
await smc.DeferAsync();
}
});
}
public MessageComponent CreateComponent()
{
var comp = new ComponentBuilder()
.WithButton(new ButtonBuilder(style: ButtonStyle.Secondary, emote: Emote, customId: Name));
return comp.Build();
}
}

View File

@@ -1,5 +1,6 @@
#nullable disable
using System.Globalization;
using MessageType = NadekoBot.Extensions.MessageType;
// ReSharper disable InconsistentNaming
@@ -30,19 +31,14 @@ public abstract class NadekoModule : ModuleBase
protected string GetText(in LocStr data)
=> Strings.GetText(data, Culture);
public Task<IUserMessage> SendErrorAsync(string error)
=> ctx.Channel.SendErrorAsync(_eb, error);
public Task<IUserMessage> SendErrorAsync(
string title,
string error,
string url = null,
string footer = null)
string footer = null,
NadekoInteraction inter = null)
=> ctx.Channel.SendErrorAsync(_eb, title, error, url, footer);
public Task<IUserMessage> SendConfirmAsync(string text)
=> ctx.Channel.SendConfirmAsync(_eb, text);
public Task<IUserMessage> SendConfirmAsync(
string title,
string text,
@@ -50,25 +46,33 @@ public abstract class NadekoModule : ModuleBase
string footer = null)
=> ctx.Channel.SendConfirmAsync(_eb, title, text, url, footer);
public Task<IUserMessage> SendPendingAsync(string text)
=> ctx.Channel.SendPendingAsync(_eb, text);
//
public Task<IUserMessage> SendErrorAsync(string text, NadekoInteraction inter = null)
=> ctx.Channel.SendAsync(_eb, text, MessageType.Error, inter);
public Task<IUserMessage> SendConfirmAsync(string text, NadekoInteraction inter = null)
=> ctx.Channel.SendAsync(_eb, text, MessageType.Ok, inter);
public Task<IUserMessage> SendPendingAsync(string text, NadekoInteraction inter = null)
=> ctx.Channel.SendAsync(_eb, text, MessageType.Pending, inter);
public Task<IUserMessage> ErrorLocalizedAsync(LocStr str)
=> SendErrorAsync(GetText(str));
public Task<IUserMessage> PendingLocalizedAsync(LocStr str)
=> SendPendingAsync(GetText(str));
// localized normal
public Task<IUserMessage> ErrorLocalizedAsync(LocStr str, NadekoInteraction inter = null)
=> SendErrorAsync(GetText(str), inter);
public Task<IUserMessage> ConfirmLocalizedAsync(LocStr str)
=> SendConfirmAsync(GetText(str));
public Task<IUserMessage> PendingLocalizedAsync(LocStr str, NadekoInteraction inter = null)
=> SendPendingAsync(GetText(str), inter);
public Task<IUserMessage> ReplyErrorLocalizedAsync(LocStr str)
public Task<IUserMessage> ConfirmLocalizedAsync(LocStr str, NadekoInteraction inter = null)
=> SendConfirmAsync(GetText(str), inter);
// localized replies
public Task<IUserMessage> ReplyErrorLocalizedAsync(LocStr str, NadekoInteraction inter = null)
=> SendErrorAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}");
public Task<IUserMessage> ReplyPendingLocalizedAsync(LocStr str)
public Task<IUserMessage> ReplyPendingLocalizedAsync(LocStr str, NadekoInteraction inter = null)
=> SendPendingAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}");
public Task<IUserMessage> ReplyConfirmLocalizedAsync(LocStr str)
public Task<IUserMessage> ReplyConfirmLocalizedAsync(LocStr str, NadekoInteraction inter = null)
=> SendConfirmAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}");
public async Task<bool> PromptUserConfirmAsync(IEmbedBuilder embed)

View File

@@ -0,0 +1,9 @@
using NadekoBot.Services.Database.Models;
namespace NadekoBot.Db.Models;
public class BankUser : DbEntity
{
public ulong UserId { get; set; }
public long Balance { get; set; }
}

View File

@@ -52,6 +52,8 @@ public abstract class NadekoContext : DbContext
public DbSet<Permissionv2> Permissions { get; set; }
public DbSet<BankUser> BankUsers { get; set; }
#region Mandatory Provider-Specific Values
protected abstract string CurrencyTransactionOtherIdDefaultValue { get; }
@@ -402,6 +404,12 @@ public abstract class NadekoContext : DbContext
x.ChannelId,
x.UserId
}));
#region BANK
modelBuilder.Entity<BankUser>(bu => bu.HasIndex(x => x.UserId).IsUnique());
#endregion
}
#if DEBUG

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,42 @@
using System;
using Microsoft.EntityFrameworkCore.Metadata;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.Migrations.Mysql
{
public partial class bank : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "bankusers",
columns: table => new
{
id = table.Column<int>(type: "int", nullable: false)
.Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
userid = table.Column<ulong>(type: "bigint unsigned", nullable: false),
balance = table.Column<long>(type: "bigint", nullable: false),
dateadded = table.Column<DateTime>(type: "datetime(6)", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_bankusers", x => x.id);
})
.Annotation("MySql:CharSet", "utf8mb4");
migrationBuilder.CreateIndex(
name: "ix_bankusers_userid",
table: "bankusers",
column: "userid",
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "bankusers");
}
}
}

View File

@@ -19,6 +19,35 @@ namespace NadekoBot.Migrations.Mysql
.HasAnnotation("ProductVersion", "6.0.3")
.HasAnnotation("Relational:MaxIdentifierLength", 64);
modelBuilder.Entity("NadekoBot.Db.Models.BankUser", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("int")
.HasColumnName("id");
b.Property<long>("Balance")
.HasColumnType("bigint")
.HasColumnName("balance");
b.Property<DateTime?>("DateAdded")
.HasColumnType("datetime(6)")
.HasColumnName("dateadded");
b.Property<ulong>("UserId")
.HasColumnType("bigint unsigned")
.HasColumnName("userid");
b.HasKey("Id")
.HasName("pk_bankusers");
b.HasIndex("UserId")
.IsUnique()
.HasDatabaseName("ix_bankusers_userid");
b.ToTable("bankusers", (string)null);
});
modelBuilder.Entity("NadekoBot.Db.Models.ClubApplicants", b =>
{
b.Property<int>("ClubId")

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,41 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata;
#nullable disable
namespace NadekoBot.Migrations.PostgreSql
{
public partial class bank : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "bankusers",
columns: table => new
{
id = table.Column<int>(type: "integer", nullable: false)
.Annotation("Npgsql:ValueGenerationStrategy", NpgsqlValueGenerationStrategy.IdentityByDefaultColumn),
userid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
balance = table.Column<long>(type: "bigint", nullable: false),
dateadded = table.Column<DateTime>(type: "timestamp with time zone", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("pk_bankusers", x => x.id);
});
migrationBuilder.CreateIndex(
name: "ix_bankusers_userid",
table: "bankusers",
column: "userid",
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "bankusers");
}
}
}

View File

@@ -22,6 +22,37 @@ namespace NadekoBot.Migrations.PostgreSql
NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
modelBuilder.Entity("NadekoBot.Db.Models.BankUser", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("integer")
.HasColumnName("id");
NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
b.Property<long>("Balance")
.HasColumnType("bigint")
.HasColumnName("balance");
b.Property<DateTime?>("DateAdded")
.HasColumnType("timestamp with time zone")
.HasColumnName("dateadded");
b.Property<decimal>("UserId")
.HasColumnType("numeric(20,0)")
.HasColumnName("userid");
b.HasKey("Id")
.HasName("pk_bankusers");
b.HasIndex("UserId")
.IsUnique()
.HasDatabaseName("ix_bankusers_userid");
b.ToTable("bankusers", (string)null);
});
modelBuilder.Entity("NadekoBot.Db.Models.ClubApplicants", b =>
{
b.Property<int>("ClubId")

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,40 @@
using System;
using Microsoft.EntityFrameworkCore.Migrations;
#nullable disable
namespace NadekoBot.Migrations
{
public partial class bank : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "BankUsers",
columns: table => new
{
Id = table.Column<int>(type: "INTEGER", nullable: false)
.Annotation("Sqlite:Autoincrement", true),
UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
Balance = table.Column<long>(type: "INTEGER", nullable: false),
DateAdded = table.Column<DateTime>(type: "TEXT", nullable: true)
},
constraints: table =>
{
table.PrimaryKey("PK_BankUsers", x => x.Id);
});
migrationBuilder.CreateIndex(
name: "IX_BankUsers_UserId",
table: "BankUsers",
column: "UserId",
unique: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(
name: "BankUsers");
}
}
}

View File

@@ -17,6 +17,29 @@ namespace NadekoBot.Migrations
#pragma warning disable 612, 618
modelBuilder.HasAnnotation("ProductVersion", "6.0.3");
modelBuilder.Entity("NadekoBot.Db.Models.BankUser", b =>
{
b.Property<int>("Id")
.ValueGeneratedOnAdd()
.HasColumnType("INTEGER");
b.Property<long>("Balance")
.HasColumnType("INTEGER");
b.Property<DateTime?>("DateAdded")
.HasColumnType("TEXT");
b.Property<ulong>("UserId")
.HasColumnType("INTEGER");
b.HasKey("Id");
b.HasIndex("UserId")
.IsUnique();
b.ToTable("BankUsers");
});
modelBuilder.Entity("NadekoBot.Db.Models.ClubApplicants", b =>
{
b.Property<int>("ClubId")

View File

@@ -0,0 +1,61 @@
using NadekoBot.Modules.Gambling.Bank;
using NadekoBot.Modules.Gambling.Common;
using NadekoBot.Modules.Gambling.Services;
namespace NadekoBot.Modules.Gambling;
// todo .h [group] should show commands in that group
public partial class Gambling
{
[Name("Bank")]
[Group("bank")]
public partial class BankCommands : GamblingModule<IBankService>
{
private readonly IBankService _bank;
public BankCommands(GamblingConfigService gcs, IBankService bank) : base(gcs)
{
_bank = bank;
}
[Cmd]
public async partial Task BankDeposit(ShmartNumber amount)
{
if (amount <= 0)
return;
if (await _bank.DepositAsync(ctx.User.Id, amount))
{
await ReplyConfirmLocalizedAsync(strs.bank_deposited(N(amount)));
}
else
{
await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
}
}
[Cmd]
public async partial Task BankWithdraw(ShmartNumber amount)
{
if (amount <= 0)
return;
if (await _bank.WithdrawAsync(ctx.User.Id, amount))
{
await ReplyConfirmLocalizedAsync(strs.bank_withdrew(N(amount)));
}
else
{
await ReplyErrorLocalizedAsync(strs.bank_withdraw_insuff(CurrencySign));
}
}
[Cmd]
public async partial Task BankBalance()
{
var bal = await _bank.GetBalanceAsync(ctx.User.Id);
await ReplyConfirmLocalizedAsync(strs.bank_balance(N(bal)));
}
}
}

View File

@@ -0,0 +1,77 @@
using LinqToDB;
using LinqToDB.EntityFrameworkCore;
namespace NadekoBot.Modules.Gambling.Bank;
public sealed class BankService : IBankService, INService
{
private readonly ICurrencyService _cur;
private readonly DbService _db;
public BankService(ICurrencyService cur, DbService db)
{
_cur = cur;
_db = db;
}
public async Task<bool> DepositAsync(ulong userId, long amount)
{
if (amount <= 0)
throw new ArgumentOutOfRangeException(nameof(amount));
if (!await _cur.RemoveAsync(userId, amount, new("bank", "deposit")))
return false;
await using var ctx = _db.GetDbContext();
await ctx.BankUsers
.ToLinqToDBTable()
.InsertOrUpdateAsync(() => new()
{
UserId = userId,
Balance = amount
},
(old) => new()
{
Balance = old.Balance + amount
},
() => new()
{
UserId = userId
});
return true;
}
public async Task<bool> WithdrawAsync(ulong userId, long amount)
{
if (amount <= 0)
throw new ArgumentOutOfRangeException(nameof(amount));
await using var ctx = _db.GetDbContext();
var rows = await ctx.BankUsers
.ToLinqToDBTable()
.Where(x => x.UserId == userId && x.Balance >= amount)
.UpdateAsync((old) => new()
{
Balance = old.Balance - amount
});
if (rows > 0)
{
await _cur.AddAsync(userId, amount, new("bank", "withdraw"));
return true;
}
return false;
}
public async Task<long> GetBalanceAsync(ulong userId)
{
await using var ctx = _db.GetDbContext();
return (await ctx.BankUsers
.ToLinqToDBTable()
.FirstOrDefaultAsync(x => x.UserId == userId))
?.Balance
?? 0;
}
}

View File

@@ -0,0 +1,8 @@
namespace NadekoBot.Modules.Gambling.Bank;
public interface IBankService
{
Task<bool> DepositAsync(ulong userId, long amount);
Task<bool> WithdrawAsync(ulong userId, long amount);
Task<long> GetBalanceAsync(ulong userId);
}

View File

@@ -3,6 +3,7 @@ using LinqToDB;
using LinqToDB.EntityFrameworkCore;
using NadekoBot.Db;
using NadekoBot.Db.Models;
using NadekoBot.Modules.Gambling.Bank;
using NadekoBot.Modules.Gambling.Common;
using NadekoBot.Modules.Gambling.Services;
using NadekoBot.Services.Currency;
@@ -40,6 +41,7 @@ public partial class Gambling : GamblingModule<GamblingService>
private readonly NumberFormatInfo _enUsCulture;
private readonly DownloadTracker _tracker;
private readonly GamblingConfigService _configService;
private readonly IBankService _bank;
private IUserMessage rdMsg;
@@ -49,13 +51,16 @@ public partial class Gambling : GamblingModule<GamblingService>
IDataCache cache,
DiscordSocketClient client,
DownloadTracker tracker,
GamblingConfigService configService)
GamblingConfigService configService,
IBankService bank)
: base(configService)
{
_db = db;
_cs = currency;
_cache = cache;
_client = client;
_bank = bank;
_enUsCulture = new CultureInfo("en-US", false).NumberFormat;
_enUsCulture.NumberDecimalDigits = 0;
_enUsCulture.NumberGroupSeparator = "";
@@ -312,6 +317,31 @@ public partial class Gambling : GamblingModule<GamblingService>
_ => $"{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)]
public async partial Task Cash(ulong userId)
@@ -320,15 +350,31 @@ public partial class Gambling : GamblingModule<GamblingService>
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);
}
[Cmd]
[Priority(1)]
public async partial Task Cash([Leftover] IUser user = null)
{
user ??= ctx.User;
var cur = await GetBalanceStringAsync(user.Id);
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));
}
}
[Cmd]
[RequireContext(ContextType.Guild)]
[Priority(0)]

View File

@@ -266,6 +266,20 @@ public partial class Help : NadekoModule<HelpService>
await ctx.Channel.EmbedAsync(embed);
}
private async Task Group(ModuleInfo group)
{
var eb = _eb.Create(ctx)
.WithTitle(GetText(strs.cmd_group_commands(group.Name)))
.WithOkColor();
foreach (var cmd in group.Commands)
{
eb.AddField(prefix + cmd.Aliases.First(), cmd.RealSummary(_strings, _medusae, Culture, prefix));
}
await ctx.Channel.EmbedAsync(eb);
}
[Cmd]
[Priority(0)]
public async partial Task H([Leftover] string fail)
@@ -278,6 +292,20 @@ public partial class Help : NadekoModule<HelpService>
return;
}
if (fail.StartsWith(prefix))
fail = fail.Substring(prefix.Length);
var group = _cmds.Modules
.SelectMany(x => x.Submodules)
.Where(x => !string.IsNullOrWhiteSpace(x.Group))
.FirstOrDefault(x => x.Group.Equals(fail, StringComparison.InvariantCultureIgnoreCase));
if (group is not null)
{
await Group(group);
return;
}
await ReplyErrorLocalizedAsync(strs.command_not_found);
}

View File

@@ -121,7 +121,7 @@ public static class Extensions
args = strings.GetCommandStrings(cmd.Summary, culture).Args;
}
return args.Map(arg => GetFullUsage(cmd.Name, arg, prefix));
return args.Map(arg => GetFullUsage(cmd.Aliases.First(), arg, prefix));
}
private static string GetFullUsage(string commandName, string args, string prefix)

View File

@@ -2,53 +2,116 @@ namespace NadekoBot.Extensions;
public static class MessageChannelExtensions
{
public static Task<IUserMessage> EmbedAsync(
this IMessageChannel ch,
IEmbedBuilder embed,
string msg = "",
// main overload that all other send methods reduce to
public static Task<IUserMessage> SendAsync(
this IMessageChannel channel,
string? plainText,
Embed? embed = null,
IReadOnlyCollection<Embed>? embeds = null,
bool sanitizeAll = false,
MessageComponent? components = null)
=> ch.SendMessageAsync(msg,
embed: embed.Build(),
{
plainText = sanitizeAll
? plainText?.SanitizeAllMentions() ?? ""
: plainText?.SanitizeMentions() ?? "";
return channel.SendMessageAsync(plainText,
embed: embed,
embeds: embeds is null
? null
: embeds as Embed[] ?? embeds.ToArray(),
components: components,
options: new()
{
RetryMode = RetryMode.AlwaysRetry
});
}
public static async Task<IUserMessage> SendAsync(
this IMessageChannel channel,
string? plainText,
NadekoInteraction? inter,
Embed? embed = null,
IReadOnlyCollection<Embed>? embeds = null,
bool sanitizeAll = false)
{
var msg = await channel.SendAsync(plainText,
embed,
embeds,
sanitizeAll,
inter?.CreateComponent());
if (inter is not null)
await inter.RunAsync(msg);
return msg;
}
public static Task<IUserMessage> SendAsync(
this IMessageChannel channel,
string? plainText,
Embed? embed = null,
Embed[]? embeds = null,
SmartText text,
bool sanitizeAll = false)
{
plainText = sanitizeAll ? plainText?.SanitizeAllMentions() ?? "" : plainText?.SanitizeMentions() ?? "";
return channel.SendMessageAsync(plainText, embed: embed, embeds: embeds);
}
public static Task<IUserMessage> SendAsync(this IMessageChannel channel, SmartText text, bool sanitizeAll = false)
=> text switch
{
SmartEmbedText set => channel.SendAsync(set.PlainText, set.GetEmbed().Build(), sanitizeAll: sanitizeAll),
SmartPlainText st => channel.SendAsync(st.Text, null, sanitizeAll: sanitizeAll),
SmartEmbedText set => channel.SendAsync(set.PlainText,
set.GetEmbed().Build(),
sanitizeAll: sanitizeAll),
SmartPlainText st => channel.SendAsync(st.Text,
default(Embed),
sanitizeAll: sanitizeAll),
SmartEmbedTextArray arr => channel.SendAsync(arr.Content,
embeds: arr.GetEmbedBuilders().Map(e => e.Build())),
_ => throw new ArgumentOutOfRangeException(nameof(text))
};
// this is a huge problem, because now i don't have
// access to embed builder service
// as this is an extension of the message channel
public static Task<IUserMessage> SendErrorAsync(
public static Task<IUserMessage> EmbedAsync(
this IMessageChannel ch,
IEmbedBuilder? embed,
string plainText = "",
IReadOnlyCollection<IEmbedBuilder>? embeds = null,
NadekoInteraction? inter = null)
=> ch.SendAsync(plainText,
inter,
embed: embed?.Build(),
embeds: embeds?.Map(x => x.Build()));
public static Task<IUserMessage> SendAsync(
this IMessageChannel ch,
IEmbedBuilderService eb,
string text,
MessageType type,
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);
}
// regular send overloads
public static Task<IUserMessage> SendErrorAsync(this IMessageChannel ch, IEmbedBuilderService eb, string text)
=> ch.SendAsync(eb, text, MessageType.Error);
public static Task<IUserMessage> SendConfirmAsync(this IMessageChannel ch, IEmbedBuilderService eb, string text)
=> ch.SendAsync(eb, text, MessageType.Ok);
public static Task<IUserMessage> SendAsync(
this IMessageChannel ch,
IEmbedBuilderService eb,
MessageType type,
string title,
string error,
string text,
string? url = null,
string? footer = null)
{
var embed = eb.Create().WithErrorColor().WithDescription(error).WithTitle(title);
var embed = eb.Create().WithDescription(text).WithTitle(title);
if (url is not null && Uri.IsWellFormedUriString(url, UriKind.Absolute))
embed.WithUrl(url);
@@ -56,14 +119,18 @@ public static class MessageChannelExtensions
if (!string.IsNullOrWhiteSpace(footer))
embed.WithFooter(footer);
return ch.SendMessageAsync("", embed: embed.Build());
embed = type switch
{
MessageType.Error => embed.WithErrorColor(),
MessageType.Ok => embed.WithOkColor(),
MessageType.Pending => embed.WithPendingColor(),
_ => throw new ArgumentOutOfRangeException(nameof(type))
};
return ch.EmbedAsync(embed);
}
public static Task<IUserMessage> SendErrorAsync(this IMessageChannel ch, IEmbedBuilderService eb, string error)
=> ch.SendMessageAsync("", embed: eb.Create().WithErrorColor().WithDescription(error).Build());
public static Task<IUserMessage> SendPendingAsync(this IMessageChannel ch, IEmbedBuilderService eb, string message)
=> ch.SendMessageAsync("", embed: eb.Create().WithPendingColor().WithDescription(message).Build());
// embed title and optional footer overloads
public static Task<IUserMessage> SendConfirmAsync(
this IMessageChannel ch,
@@ -72,20 +139,18 @@ public static class MessageChannelExtensions
string text,
string? url = null,
string? footer = null)
{
var embed = eb.Create().WithOkColor().WithDescription(text).WithTitle(title);
=> ch.SendAsync(eb, MessageType.Ok, title, text, url, footer);
if (url is not null && Uri.IsWellFormedUriString(url, UriKind.Absolute))
embed.WithUrl(url);
public static Task<IUserMessage> SendErrorAsync(
this IMessageChannel ch,
IEmbedBuilderService eb,
string title,
string text,
string? url = null,
string? footer = null)
=> ch.SendAsync(eb, MessageType.Error, title, text, url, footer);
if (!string.IsNullOrWhiteSpace(footer))
embed.WithFooter(footer);
return ch.SendMessageAsync("", embed: embed.Build());
}
public static Task<IUserMessage> SendConfirmAsync(this IMessageChannel ch, IEmbedBuilderService eb, string text)
=> ch.SendMessageAsync("", embed: eb.Create().WithOkColor().WithDescription(text).Build());
// weird stuff
public static Task<IUserMessage> SendTableAsync<T>(
this IMessageChannel ch,
@@ -142,7 +207,7 @@ public static class MessageChannelExtensions
var component = new ComponentBuilder()
.WithButton(new ButtonBuilder()
.WithStyle(ButtonStyle.Secondary)
.WithStyle(ButtonStyle.Primary)
.WithCustomId(BUTTON_LEFT)
.WithDisabled(lastPage == 0)
.WithEmote(_arrowLeft))
@@ -165,6 +230,9 @@ public static class MessageChannelExtensions
if (smc.Message.Id != msg.Id)
return;
if (smc.Data.CustomId != BUTTON_LEFT && smc.Data.CustomId != BUTTON_RIGHT)
return;
await si.DeferAsync();
if (smc.User.Id != ctx.User.Id)
return;
@@ -210,12 +278,36 @@ public static class MessageChannelExtensions
await msg.ModifyAsync(mp => mp.Components = new ComponentBuilder().Build());
}
private static readonly Emoji _okEmoji = new Emoji("✅");
private static readonly Emoji _warnEmoji = new Emoji("⚠️");
private static readonly Emoji _errorEmoji = new Emoji("❌");
public static Task ReactAsync(this ICommandContext ctx, MessageType type)
{
var emoji = type switch
{
MessageType.Error => _errorEmoji,
MessageType.Pending => _warnEmoji,
MessageType.Ok => _okEmoji,
_ => throw new ArgumentOutOfRangeException(nameof(type)),
};
return ctx.Message.AddReactionAsync(emoji);
}
public static Task OkAsync(this ICommandContext ctx)
=> ctx.Message.AddReactionAsync(new Emoji("✅"));
=> ctx.ReactAsync(MessageType.Ok);
public static Task ErrorAsync(this ICommandContext ctx)
=> ctx.Message.AddReactionAsync(new Emoji("❌"));
=> ctx.ReactAsync(MessageType.Error);
public static Task WarningAsync(this ICommandContext ctx)
=> ctx.Message.AddReactionAsync(new Emoji("⚠️"));
=> ctx.ReactAsync(MessageType.Pending);
}
public enum MessageType
{
Ok,
Pending,
Error
}

View File

@@ -519,7 +519,7 @@ streamoffline:
- sto
- stoff
streamonlinedelete:
- streamonlinedelte
- streamonlinedelete
- stondel
streammessage:
- streammsg
@@ -1286,3 +1286,15 @@ medusalist:
medusainfo:
- medusainfo
- meinfo
bankdeposit:
- deposit
- d
- dep
bankwithdraw:
- withdraw
- w
- with
bankbalance:
- balance
- b
- bal

View File

@@ -2185,4 +2185,15 @@ medusalist:
Read about the medusa system [here](https://nadekobot.readthedocs.io/en/latest/medusa/creating-a-medusa/)
args:
- ""
bankdeposit:
desc: "Deposits the specified amount of currency into the bank for later use."
args:
- "50"
bankwithdraw:
desc: "Withdraws the specified amount of currency from the bank if available."
args:
- "49"
bankbalance:
desc: "Shows your current bank balance available for withdrawal."
args:
- ""

View File

@@ -993,5 +993,10 @@
"medusa_unloaded": "Medusa {0} has been unloaded.",
"medusa_empty": "Medusa wasn't loaded as it didn't contain any Sneks.",
"medusa_already_loaded": "Medusa {0} is already loaded",
"medusa_invalid_not_found": "Medusa with that name wasn't found or the file was invalid"
"medusa_invalid_not_found": "Medusa with that name wasn't found or the file was invalid",
"bank_balance": "You have {0} in your bank account.",
"bank_deposited": "You deposited {0} to your bank account.",
"bank_withdrew": "You withdrew {0} from your bank account.",
"bank_withdraw_insuff": "You don't have sufficient {0} in your bank account.",
"cmd_group_commands": "'{0}' command group"
}