mirror of
https://gitlab.com/Kwoth/nadekobot.git
synced 2025-11-04 16:44:28 -05: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,10 @@
|
||||
namespace NadekoBot.Extensions;
|
||||
|
||||
public static class BotCredentialsExtensions
|
||||
{
|
||||
public static bool IsOwner(this IBotCredentials creds, IUser user)
|
||||
=> creds.IsOwner(user.Id);
|
||||
|
||||
public static bool IsOwner(this IBotCredentials creds, ulong userId)
|
||||
=> creds.OwnerIds.Contains(userId);
|
||||
}
|
||||
207
src/NadekoBot/_common/_Extensions/Extensions.cs
Normal file
207
src/NadekoBot/_common/_Extensions/Extensions.cs
Normal file
@@ -0,0 +1,207 @@
|
||||
using Humanizer.Localisation;
|
||||
using System.Diagnostics;
|
||||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.RegularExpressions;
|
||||
using Nadeko.Common.Medusa;
|
||||
|
||||
namespace NadekoBot.Extensions;
|
||||
|
||||
public static class Extensions
|
||||
{
|
||||
public static DateOnly ToDateOnly(this DateTime dateTime)
|
||||
=> DateOnly.FromDateTime(dateTime);
|
||||
|
||||
public static bool IsBeforeToday(this DateTime date)
|
||||
=> date < DateTime.UtcNow.Date;
|
||||
|
||||
private static readonly Regex _urlRegex =
|
||||
new(@"^(https?|ftp)://(?<path>[^\s/$.?#].[^\s]*)$", RegexOptions.Compiled);
|
||||
|
||||
public static IEmbedBuilder WithAuthor(this IEmbedBuilder eb, IUser author)
|
||||
=> eb.WithAuthor(author.ToString()!, author.RealAvatarUrl().ToString());
|
||||
|
||||
public static Task EditAsync(this IUserMessage msg, SmartText text)
|
||||
=> text switch
|
||||
{
|
||||
SmartEmbedText set => msg.ModifyAsync(x =>
|
||||
{
|
||||
x.Embed = set.IsValid ? set.GetEmbed().Build() : null;
|
||||
x.Content = set.PlainText?.SanitizeMentions() ?? "";
|
||||
}),
|
||||
SmartEmbedTextArray set => msg.ModifyAsync(x =>
|
||||
{
|
||||
x.Embeds = set.GetEmbedBuilders().Map(eb => eb.Build());
|
||||
x.Content = set.Content?.SanitizeMentions() ?? "";
|
||||
}),
|
||||
SmartPlainText spt => msg.ModifyAsync(x =>
|
||||
{
|
||||
x.Content = spt.Text.SanitizeMentions();
|
||||
x.Embed = null;
|
||||
}),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(text))
|
||||
};
|
||||
|
||||
public static ulong[] GetGuildIds(this DiscordSocketClient client)
|
||||
=> client.Guilds
|
||||
.Map(x => x.Id);
|
||||
|
||||
/// <summary>
|
||||
/// Generates a string in the format HHH:mm if timespan is >= 2m.
|
||||
/// Generates a string in the format 00:mm:ss if timespan is less than 2m.
|
||||
/// </summary>
|
||||
/// <param name="span">Timespan to convert to string</param>
|
||||
/// <returns>Formatted duration string</returns>
|
||||
public static string ToPrettyStringHm(this TimeSpan span)
|
||||
=> span.Humanize(2, minUnit: TimeUnit.Second);
|
||||
|
||||
public static bool TryGetUrlPath(this string input, out string path)
|
||||
{
|
||||
var match = _urlRegex.Match(input);
|
||||
if (match.Success)
|
||||
{
|
||||
path = match.Groups["path"].Value;
|
||||
return true;
|
||||
}
|
||||
|
||||
path = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
public static IEmote ToIEmote(this string emojiStr)
|
||||
=> Emote.TryParse(emojiStr, out var maybeEmote) ? maybeEmote : new Emoji(emojiStr);
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// First 10 characters of teh bot token.
|
||||
/// </summary>
|
||||
public static string RedisKey(this IBotCredentials bc)
|
||||
=> bc.Token[..10];
|
||||
|
||||
public static bool IsAuthor(this IMessage msg, IDiscordClient client)
|
||||
=> msg.Author?.Id == client.CurrentUser.Id;
|
||||
|
||||
public static string RealSummary(
|
||||
this CommandInfo cmd,
|
||||
IBotStrings strings,
|
||||
IMedusaLoaderService medusae,
|
||||
CultureInfo culture,
|
||||
string prefix)
|
||||
{
|
||||
string description;
|
||||
if (cmd.Remarks?.StartsWith("medusa///") ?? false)
|
||||
{
|
||||
// command method name is kept in Summary
|
||||
// medusa///<medusa-name-here> is kept in remarks
|
||||
// this way I can find the name of the medusa, and then name of the command for which
|
||||
// the description should be loaded
|
||||
var medusaName = cmd.Remarks.Split("///")[1];
|
||||
description = medusae.GetCommandDescription(medusaName, cmd.Summary, culture);
|
||||
}
|
||||
else
|
||||
{
|
||||
description = strings.GetCommandStrings(cmd.Summary, culture).Desc;
|
||||
}
|
||||
|
||||
return string.Format(description, prefix);
|
||||
}
|
||||
|
||||
public static string[] RealRemarksArr(
|
||||
this CommandInfo cmd,
|
||||
IBotStrings strings,
|
||||
IMedusaLoaderService medusae,
|
||||
CultureInfo culture,
|
||||
string prefix)
|
||||
{
|
||||
string[] args;
|
||||
if (cmd.Remarks?.StartsWith("medusa///") ?? false)
|
||||
{
|
||||
// command method name is kept in Summary
|
||||
// medusa///<medusa-name-here> is kept in remarks
|
||||
// this way I can find the name of the medusa,
|
||||
// and command for which data should be loaded
|
||||
var medusaName = cmd.Remarks.Split("///")[1];
|
||||
args = medusae.GetCommandExampleArgs(medusaName, cmd.Summary, culture);
|
||||
}
|
||||
else
|
||||
{
|
||||
args = strings.GetCommandStrings(cmd.Summary, culture).Args;
|
||||
}
|
||||
|
||||
return args.Map(arg => GetFullUsage(cmd.Aliases.First(), arg, prefix));
|
||||
}
|
||||
|
||||
private static string GetFullUsage(string commandName, string args, string prefix)
|
||||
=> $"{prefix}{commandName} {string.Format(args, prefix)}".TrimEnd();
|
||||
|
||||
public static IEmbedBuilder AddPaginatedFooter(this IEmbedBuilder embed, int curPage, int? lastPage)
|
||||
{
|
||||
if (lastPage is not null)
|
||||
return embed.WithFooter($"{curPage + 1} / {lastPage + 1}");
|
||||
return embed.WithFooter(curPage.ToString());
|
||||
}
|
||||
|
||||
public static IEmbedBuilder WithOkColor(this IEmbedBuilder eb)
|
||||
=> eb.WithColor(EmbedColor.Ok);
|
||||
|
||||
public static IEmbedBuilder WithPendingColor(this IEmbedBuilder eb)
|
||||
=> eb.WithColor(EmbedColor.Pending);
|
||||
|
||||
public static IEmbedBuilder WithErrorColor(this IEmbedBuilder eb)
|
||||
=> eb.WithColor(EmbedColor.Error);
|
||||
|
||||
public static IMessage DeleteAfter(this IUserMessage msg, float seconds, ILogCommandService? logService = null)
|
||||
{
|
||||
Task.Run(async () =>
|
||||
{
|
||||
await Task.Delay((int)(seconds * 1000));
|
||||
if (logService is not null)
|
||||
logService.AddDeleteIgnore(msg.Id);
|
||||
|
||||
try
|
||||
{
|
||||
await msg.DeleteAsync();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
});
|
||||
return msg;
|
||||
}
|
||||
|
||||
public static ModuleInfo GetTopLevelModule(this ModuleInfo module)
|
||||
{
|
||||
while (module.Parent is not null)
|
||||
module = module.Parent;
|
||||
|
||||
return module;
|
||||
}
|
||||
|
||||
public static string GetGroupName(this ModuleInfo module)
|
||||
=> module.Name.Replace("Commands", "", StringComparison.InvariantCulture);
|
||||
|
||||
public static async Task<IEnumerable<IGuildUser>> GetMembersAsync(this IRole role)
|
||||
{
|
||||
var users = await role.Guild.GetUsersAsync(CacheMode.CacheOnly);
|
||||
return users.Where(u => u.RoleIds.Contains(role.Id));
|
||||
}
|
||||
|
||||
public static string ToJson<T>(this T any, JsonSerializerOptions? options = null)
|
||||
=> JsonSerializer.Serialize(any, options);
|
||||
|
||||
public static Stream ToStream(this IEnumerable<byte> bytes, bool canWrite = false)
|
||||
{
|
||||
var ms = new MemoryStream(bytes as byte[] ?? bytes.ToArray(), canWrite);
|
||||
ms.Seek(0, SeekOrigin.Begin);
|
||||
return ms;
|
||||
}
|
||||
|
||||
public static IEnumerable<IRole> GetRoles(this IGuildUser user)
|
||||
=> user.RoleIds.Select(r => user.Guild.GetRole(r)).Where(r => r is not null);
|
||||
|
||||
public static void Lap(this Stopwatch sw, string checkpoint)
|
||||
{
|
||||
Log.Information("Checkpoint {CheckPoint}: {Time}ms", checkpoint, sw.Elapsed.TotalMilliseconds);
|
||||
sw.Restart();
|
||||
}
|
||||
}
|
||||
329
src/NadekoBot/_common/_Extensions/IMessageChannelExtensions.cs
Normal file
329
src/NadekoBot/_common/_Extensions/IMessageChannelExtensions.cs
Normal file
@@ -0,0 +1,329 @@
|
||||
|
||||
namespace NadekoBot.Extensions;
|
||||
|
||||
public static class MessageChannelExtensions
|
||||
{
|
||||
// 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)
|
||||
{
|
||||
plainText = sanitizeAll
|
||||
? plainText?.SanitizeAllMentions() ?? ""
|
||||
: plainText?.SanitizeMentions() ?? "";
|
||||
|
||||
return channel.SendMessageAsync(plainText,
|
||||
embed: embed,
|
||||
embeds: embeds is null
|
||||
? null
|
||||
: embeds as Embed[] ?? embeds.ToArray(),
|
||||
components: components);
|
||||
}
|
||||
|
||||
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,
|
||||
SmartText text,
|
||||
bool sanitizeAll = false)
|
||||
=> text switch
|
||||
{
|
||||
SmartEmbedText set => channel.SendAsync(set.PlainText,
|
||||
set.IsValid ? set.GetEmbed().Build() : null,
|
||||
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))
|
||||
};
|
||||
|
||||
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,
|
||||
MsgType type,
|
||||
NadekoInteraction? inter = null)
|
||||
{
|
||||
var builder = eb.Create().WithDescription(text);
|
||||
|
||||
builder = (type switch
|
||||
{
|
||||
MsgType.Error => builder.WithErrorColor(),
|
||||
MsgType.Ok => builder.WithOkColor(),
|
||||
MsgType.Pending => builder.WithPendingColor(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(type))
|
||||
});
|
||||
|
||||
return ch.EmbedAsync(builder, inter: inter);
|
||||
}
|
||||
|
||||
public static Task<IUserMessage> SendConfirmAsync(this IMessageChannel ch, IEmbedBuilderService eb, string text)
|
||||
=> ch.SendAsync(eb, text, MsgType.Ok);
|
||||
|
||||
public static Task<IUserMessage> SendAsync(
|
||||
this IMessageChannel ch,
|
||||
IEmbedBuilderService eb,
|
||||
MsgType type,
|
||||
string? title,
|
||||
string text,
|
||||
string? url = null,
|
||||
string? footer = null)
|
||||
{
|
||||
var embed = eb.Create()
|
||||
.WithDescription(text)
|
||||
.WithTitle(title);
|
||||
|
||||
if (url is not null && Uri.IsWellFormedUriString(url, UriKind.Absolute))
|
||||
embed.WithUrl(url);
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(footer))
|
||||
embed.WithFooter(footer);
|
||||
|
||||
embed = type switch
|
||||
{
|
||||
MsgType.Error => embed.WithErrorColor(),
|
||||
MsgType.Ok => embed.WithOkColor(),
|
||||
MsgType.Pending => embed.WithPendingColor(),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(type))
|
||||
};
|
||||
|
||||
return ch.EmbedAsync(embed);
|
||||
}
|
||||
|
||||
// embed title and optional footer overloads
|
||||
|
||||
public static Task<IUserMessage> SendConfirmAsync(
|
||||
this IMessageChannel ch,
|
||||
IEmbedBuilderService eb,
|
||||
string? title,
|
||||
string text,
|
||||
string? url = null,
|
||||
string? footer = null)
|
||||
=> ch.SendAsync(eb, MsgType.Ok, title, text, url, footer);
|
||||
|
||||
public static Task<IUserMessage> SendErrorAsync(
|
||||
this IMessageChannel ch,
|
||||
IEmbedBuilderService eb,
|
||||
string title,
|
||||
string text,
|
||||
string? url = null,
|
||||
string? footer = null)
|
||||
=> ch.SendAsync(eb, MsgType.Error, title, text, url, footer);
|
||||
|
||||
// regular send overloads
|
||||
public static Task<IUserMessage> SendErrorAsync(this IMessageChannel ch, IEmbedBuilderService eb, string text)
|
||||
=> ch.SendAsync(eb, text, MsgType.Error);
|
||||
|
||||
public static Task SendPaginatedConfirmAsync(
|
||||
this ICommandContext ctx,
|
||||
int currentPage,
|
||||
Func<int, IEmbedBuilder> pageFunc,
|
||||
int totalElements,
|
||||
int itemsPerPage,
|
||||
bool addPaginatedFooter = true)
|
||||
=> ctx.SendPaginatedConfirmAsync(currentPage,
|
||||
x => Task.FromResult(pageFunc(x)),
|
||||
totalElements,
|
||||
itemsPerPage,
|
||||
addPaginatedFooter);
|
||||
|
||||
private const string BUTTON_LEFT = "BUTTON_LEFT";
|
||||
private const string BUTTON_RIGHT = "BUTTON_RIGHT";
|
||||
|
||||
private static readonly IEmote _arrowLeft = Emote.Parse("<:x:1232256519844790302>");
|
||||
private static readonly IEmote _arrowRight = Emote.Parse("<:x:1232256515298295838>");
|
||||
|
||||
public static Task SendPaginatedConfirmAsync(
|
||||
this ICommandContext ctx,
|
||||
int currentPage,
|
||||
Func<int, Task<IEmbedBuilder>> pageFunc,
|
||||
int totalElements,
|
||||
int itemsPerPage,
|
||||
bool addPaginatedFooter = true)
|
||||
=> ctx.SendPaginatedConfirmAsync(currentPage,
|
||||
pageFunc,
|
||||
default(Func<int, ValueTask<SimpleInteraction<object>?>>),
|
||||
totalElements,
|
||||
itemsPerPage,
|
||||
addPaginatedFooter);
|
||||
|
||||
public static async Task SendPaginatedConfirmAsync<T>(
|
||||
this ICommandContext ctx,
|
||||
int currentPage,
|
||||
Func<int, Task<IEmbedBuilder>> pageFunc,
|
||||
Func<int, ValueTask<SimpleInteraction<T>?>>? interFactory,
|
||||
int totalElements,
|
||||
int itemsPerPage,
|
||||
bool addPaginatedFooter = true)
|
||||
{
|
||||
var lastPage = (totalElements - 1) / itemsPerPage;
|
||||
|
||||
var embed = await pageFunc(currentPage);
|
||||
|
||||
if (addPaginatedFooter)
|
||||
embed.AddPaginatedFooter(currentPage, lastPage);
|
||||
|
||||
SimpleInteraction<T>? maybeInter = null;
|
||||
async Task<ComponentBuilder> GetComponentBuilder()
|
||||
{
|
||||
var cb = new ComponentBuilder();
|
||||
|
||||
cb.WithButton(new ButtonBuilder()
|
||||
.WithStyle(ButtonStyle.Primary)
|
||||
.WithCustomId(BUTTON_LEFT)
|
||||
.WithDisabled(lastPage == 0)
|
||||
.WithEmote(_arrowLeft)
|
||||
.WithDisabled(currentPage <= 0));
|
||||
|
||||
if (interFactory is not null)
|
||||
{
|
||||
maybeInter = await interFactory(currentPage);
|
||||
|
||||
if (maybeInter is not null)
|
||||
cb.WithButton(maybeInter.Button);
|
||||
}
|
||||
|
||||
cb.WithButton(new ButtonBuilder()
|
||||
.WithStyle(ButtonStyle.Primary)
|
||||
.WithCustomId(BUTTON_RIGHT)
|
||||
.WithDisabled(lastPage == 0 || currentPage >= lastPage)
|
||||
.WithEmote(_arrowRight));
|
||||
|
||||
return cb;
|
||||
}
|
||||
|
||||
async Task UpdatePageAsync(SocketMessageComponent smc)
|
||||
{
|
||||
var toSend = await pageFunc(currentPage);
|
||||
if (addPaginatedFooter)
|
||||
toSend.AddPaginatedFooter(currentPage, lastPage);
|
||||
|
||||
var component = (await GetComponentBuilder()).Build();
|
||||
|
||||
await smc.ModifyOriginalResponseAsync(x =>
|
||||
{
|
||||
x.Embed = toSend.Build();
|
||||
x.Components = component;
|
||||
});
|
||||
}
|
||||
|
||||
var component = (await GetComponentBuilder()).Build();
|
||||
var msg = await ctx.Channel.SendAsync(null, embed: embed.Build(), components: component);
|
||||
|
||||
async Task OnInteractionAsync(SocketInteraction si)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (si is not SocketMessageComponent smc)
|
||||
return;
|
||||
|
||||
if (smc.Message.Id != msg.Id)
|
||||
return;
|
||||
|
||||
await si.DeferAsync();
|
||||
if (smc.User.Id != ctx.User.Id)
|
||||
return;
|
||||
|
||||
if (smc.Data.CustomId == BUTTON_LEFT)
|
||||
{
|
||||
if (currentPage == 0)
|
||||
return;
|
||||
|
||||
--currentPage;
|
||||
_ = UpdatePageAsync(smc);
|
||||
}
|
||||
else if (smc.Data.CustomId == BUTTON_RIGHT)
|
||||
{
|
||||
if (currentPage >= lastPage)
|
||||
return;
|
||||
|
||||
++currentPage;
|
||||
_ = UpdatePageAsync(smc);
|
||||
}
|
||||
else if (maybeInter is { } inter && inter.Button.CustomId == smc.Data.CustomId)
|
||||
{
|
||||
await inter.TriggerAsync(smc);
|
||||
_ = UpdatePageAsync(smc);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "Error in pagination: {ErrorMessage}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
if (lastPage == 0 && interFactory is null)
|
||||
return;
|
||||
|
||||
var client = (DiscordSocketClient)ctx.Client;
|
||||
|
||||
client.InteractionCreated += OnInteractionAsync;
|
||||
|
||||
await Task.Delay(30_000);
|
||||
|
||||
client.InteractionCreated -= OnInteractionAsync;
|
||||
|
||||
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, MsgType type)
|
||||
{
|
||||
var emoji = type switch
|
||||
{
|
||||
MsgType.Error => _errorEmoji,
|
||||
MsgType.Pending => _warnEmoji,
|
||||
MsgType.Ok => _okEmoji,
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(type)),
|
||||
};
|
||||
|
||||
return ctx.Message.AddReactionAsync(emoji);
|
||||
}
|
||||
|
||||
public static Task OkAsync(this ICommandContext ctx)
|
||||
=> ctx.ReactAsync(MsgType.Ok);
|
||||
|
||||
public static Task ErrorAsync(this ICommandContext ctx)
|
||||
=> ctx.ReactAsync(MsgType.Error);
|
||||
|
||||
public static Task WarningAsync(this ICommandContext ctx)
|
||||
=> ctx.ReactAsync(MsgType.Pending);
|
||||
}
|
||||
18
src/NadekoBot/_common/_Extensions/LinkedListExtensions.cs
Normal file
18
src/NadekoBot/_common/_Extensions/LinkedListExtensions.cs
Normal file
@@ -0,0 +1,18 @@
|
||||
namespace NadekoBot.Extensions;
|
||||
|
||||
public static class LinkedListExtensions
|
||||
{
|
||||
public static LinkedListNode<T>? FindNode<T>(this LinkedList<T> list, Func<T, bool> predicate)
|
||||
{
|
||||
var node = list.First;
|
||||
while (node is not null)
|
||||
{
|
||||
if (predicate(node.Value))
|
||||
return node;
|
||||
|
||||
node = node.Next;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
}
|
||||
7
src/NadekoBot/_common/_Extensions/NumberExtensions.cs
Normal file
7
src/NadekoBot/_common/_Extensions/NumberExtensions.cs
Normal file
@@ -0,0 +1,7 @@
|
||||
namespace NadekoBot.Extensions;
|
||||
|
||||
public static class NumberExtensions
|
||||
{
|
||||
public static DateTimeOffset ToUnixTimestamp(this double number)
|
||||
=> new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero).AddSeconds(number);
|
||||
}
|
||||
23
src/NadekoBot/_common/_Extensions/ReflectionExtensions.cs
Normal file
23
src/NadekoBot/_common/_Extensions/ReflectionExtensions.cs
Normal file
@@ -0,0 +1,23 @@
|
||||
namespace NadekoBot.Extensions;
|
||||
|
||||
public static class ReflectionExtensions
|
||||
{
|
||||
public static bool IsAssignableToGenericType(this Type givenType, Type genericType)
|
||||
{
|
||||
var interfaceTypes = givenType.GetInterfaces();
|
||||
|
||||
foreach (var it in interfaceTypes)
|
||||
{
|
||||
if (it.IsGenericType && it.GetGenericTypeDefinition() == genericType)
|
||||
return true;
|
||||
}
|
||||
|
||||
if (givenType.IsGenericType && givenType.GetGenericTypeDefinition() == genericType)
|
||||
return true;
|
||||
|
||||
var baseType = givenType.BaseType;
|
||||
if (baseType == null) return false;
|
||||
|
||||
return IsAssignableToGenericType(baseType, genericType);
|
||||
}
|
||||
}
|
||||
57
src/NadekoBot/_common/_Extensions/Rgba32Extensions.cs
Normal file
57
src/NadekoBot/_common/_Extensions/Rgba32Extensions.cs
Normal file
@@ -0,0 +1,57 @@
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Formats;
|
||||
using SixLabors.ImageSharp.Formats.Gif;
|
||||
using SixLabors.ImageSharp.Formats.Png;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
|
||||
namespace NadekoBot.Extensions;
|
||||
|
||||
public static class Rgba32Extensions
|
||||
{
|
||||
public static Image<Rgba32> Merge(this IEnumerable<Image<Rgba32>> images)
|
||||
=> images.Merge(out _);
|
||||
|
||||
public static Image<Rgba32> Merge(this IEnumerable<Image<Rgba32>> images, out IImageFormat format)
|
||||
{
|
||||
format = PngFormat.Instance;
|
||||
|
||||
void DrawFrame(IList<Image<Rgba32>> imgArray, Image<Rgba32> imgFrame, int frameNumber)
|
||||
{
|
||||
var xOffset = 0;
|
||||
for (var i = 0; i < imgArray.Count; i++)
|
||||
{
|
||||
using var frame = imgArray[i].Frames.CloneFrame(frameNumber % imgArray[i].Frames.Count);
|
||||
var offset = xOffset;
|
||||
imgFrame.Mutate(x => x.DrawImage(frame, new Point(offset, 0), new GraphicsOptions()));
|
||||
xOffset += imgArray[i].Bounds().Width;
|
||||
}
|
||||
}
|
||||
|
||||
var imgs = images.ToList();
|
||||
var frames = imgs.Max(x => x.Frames.Count);
|
||||
|
||||
var width = imgs.Sum(img => img.Width);
|
||||
var height = imgs.Max(img => img.Height);
|
||||
var canvas = new Image<Rgba32>(width, height);
|
||||
if (frames == 1)
|
||||
{
|
||||
DrawFrame(imgs, canvas, 0);
|
||||
return canvas;
|
||||
}
|
||||
|
||||
format = GifFormat.Instance;
|
||||
for (var j = 0; j < frames; j++)
|
||||
{
|
||||
using var imgFrame = new Image<Rgba32>(width, height);
|
||||
DrawFrame(imgs, imgFrame, j);
|
||||
|
||||
var frameToAdd = imgFrame.Frames[0];
|
||||
frameToAdd.Metadata.GetGifMetadata().DisposalMethod = GifDisposalMethod.RestoreToBackground;
|
||||
canvas.Frames.AddFrame(frameToAdd);
|
||||
}
|
||||
|
||||
canvas.Frames.RemoveFrame(0);
|
||||
return canvas;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
public static Task RespondAsync(
|
||||
this SocketMessageComponent smc,
|
||||
SmartText text,
|
||||
bool sanitizeAll = false,
|
||||
bool ephemeral = true)
|
||||
=> text switch
|
||||
{
|
||||
SmartEmbedText set => smc.RespondAsync(set.PlainText,
|
||||
set.IsValid ? set.GetEmbed().Build() : null,
|
||||
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()),
|
||||
ephemeral: ephemeral);
|
||||
|
||||
public static Task RespondAsync(
|
||||
this SocketMessageComponent ch,
|
||||
IEmbedBuilderService eb,
|
||||
string text,
|
||||
MsgType type,
|
||||
bool ephemeral = false,
|
||||
NadekoInteraction? inter = null)
|
||||
{
|
||||
var builder = eb.Create().WithDescription(text);
|
||||
|
||||
builder = (type switch
|
||||
{
|
||||
MsgType.Error => builder.WithErrorColor(),
|
||||
MsgType.Ok => builder.WithOkColor(),
|
||||
MsgType.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, MsgType.Error, ephemeral);
|
||||
|
||||
public static Task RespondConfirmAsync(
|
||||
this SocketMessageComponent smc,
|
||||
IEmbedBuilderService eb,
|
||||
string text,
|
||||
bool ephemeral = false)
|
||||
=> smc.RespondAsync(eb, text, MsgType.Ok, ephemeral);
|
||||
}
|
||||
39
src/NadekoBot/_common/_Extensions/UserExtensions.cs
Normal file
39
src/NadekoBot/_common/_Extensions/UserExtensions.cs
Normal file
@@ -0,0 +1,39 @@
|
||||
using NadekoBot.Db.Models;
|
||||
|
||||
namespace NadekoBot.Extensions;
|
||||
|
||||
public static class UserExtensions
|
||||
{
|
||||
public static async Task<IUserMessage> EmbedAsync(this IUser user, IEmbedBuilder embed, string msg = "")
|
||||
{
|
||||
var ch = await user.CreateDMChannelAsync();
|
||||
return await ch.EmbedAsync(embed, msg);
|
||||
}
|
||||
|
||||
public static async Task<IUserMessage> SendAsync(this IUser user, SmartText text, bool sanitizeAll = false)
|
||||
{
|
||||
var ch = await user.CreateDMChannelAsync();
|
||||
return await ch.SendAsync(text, sanitizeAll);
|
||||
}
|
||||
|
||||
public static async Task<IUserMessage> SendConfirmAsync(this IUser user, IEmbedBuilderService eb, string text)
|
||||
=> await user.SendMessageAsync("", embed: eb.Create().WithOkColor().WithDescription(text).Build());
|
||||
|
||||
public static async Task<IUserMessage> SendErrorAsync(this IUser user, IEmbedBuilderService eb, string error)
|
||||
=> await user.SendMessageAsync("", embed: eb.Create().WithErrorColor().WithDescription(error).Build());
|
||||
|
||||
public static async Task<IUserMessage> SendPendingAsync(this IUser user, IEmbedBuilderService eb, string message)
|
||||
=> await user.SendMessageAsync("", embed: eb.Create().WithPendingColor().WithDescription(message).Build());
|
||||
|
||||
// This method is used by everything that fetches the avatar from a user
|
||||
public static Uri RealAvatarUrl(this IUser usr, ushort size = 256)
|
||||
=> usr.AvatarId is null ? new(usr.GetDefaultAvatarUrl()) : new Uri(usr.GetAvatarUrl(ImageFormat.Auto, size));
|
||||
|
||||
// This method is only used for the xp card
|
||||
public static Uri RealAvatarUrl(this DiscordUser usr)
|
||||
=> usr.AvatarId is null
|
||||
? new(CDN.GetDefaultUserAvatarUrl(ushort.Parse(usr.Discriminator)))
|
||||
: new Uri(usr.AvatarId.StartsWith("a_", StringComparison.InvariantCulture)
|
||||
? $"{DiscordConfig.CDNUrl}avatars/{usr.UserId}/{usr.AvatarId}.gif"
|
||||
: $"{DiscordConfig.CDNUrl}avatars/{usr.UserId}/{usr.AvatarId}.png");
|
||||
}
|
||||
Reference in New Issue
Block a user