mirror of
https://github.com/TeamOctolings/Octobot.git
synced 2025-05-02 20:19:55 +03:00
Apply official naming guidelines to Octobot (#306)
1. The root namespace was changed from `Octobot` to `TeamOctolings.Octobot`: > DO prefix namespace names with a company name to prevent namespaces from different companies from having the same name. 2. `Octobot.cs` was renamed to `Program.cs`: > DO NOT use the same name for a namespace and a type in that namespace. 3. `IOption`, `Option` were renamed to `IGuildOption` and `GuildOption` respectively: > DO NOT introduce generic type names such as Element, Node, Log, and Message. 4. `Utility` was moved out of the `Services` namespace. It didn't belong there anyway 5. `Program` static fields were moved to `Utility` 6. Localisation files were moved back to the project source files. Looks like this fixed `Message.Designer.cs` code generation --------- Signed-off-by: Octol1ttle <l1ttleofficial@outlook.com>
This commit is contained in:
parent
19fadead91
commit
793afd0e06
61 changed files with 447 additions and 462 deletions
29
TeamOctolings.Octobot/Extensions/ChannelApiExtensions.cs
Normal file
29
TeamOctolings.Octobot/Extensions/ChannelApiExtensions.cs
Normal file
|
@ -0,0 +1,29 @@
|
|||
using OneOf;
|
||||
using Remora.Discord.API.Abstractions.Objects;
|
||||
using Remora.Discord.API.Abstractions.Rest;
|
||||
using Remora.Discord.API.Objects;
|
||||
using Remora.Rest.Core;
|
||||
using Remora.Results;
|
||||
|
||||
namespace TeamOctolings.Octobot.Extensions;
|
||||
|
||||
public static class ChannelApiExtensions
|
||||
{
|
||||
public static async Task<Result> CreateMessageWithEmbedResultAsync(this IDiscordRestChannelAPI channelApi,
|
||||
Snowflake channelId, Optional<string> message = default, Optional<string> nonce = default,
|
||||
Optional<bool> isTextToSpeech = default, Optional<Result<Embed>> embedResult = default,
|
||||
Optional<IAllowedMentions> allowedMentions = default, Optional<IMessageReference> messageRefenence = default,
|
||||
Optional<IReadOnlyList<IMessageComponent>> components = default,
|
||||
Optional<IReadOnlyList<Snowflake>> stickerIds = default,
|
||||
Optional<IReadOnlyList<OneOf<FileData, IPartialAttachment>>> attachments = default,
|
||||
Optional<MessageFlags> flags = default, CancellationToken ct = default)
|
||||
{
|
||||
if (!embedResult.IsDefined() || !embedResult.Value.IsDefined(out var embed))
|
||||
{
|
||||
return ResultExtensions.FromError(embedResult.Value);
|
||||
}
|
||||
|
||||
return (Result)await channelApi.CreateMessageAsync(channelId, message, nonce, isTextToSpeech, new[] { embed },
|
||||
allowedMentions, messageRefenence, components, stickerIds, attachments, flags, ct);
|
||||
}
|
||||
}
|
40
TeamOctolings.Octobot/Extensions/CollectionExtensions.cs
Normal file
40
TeamOctolings.Octobot/Extensions/CollectionExtensions.cs
Normal file
|
@ -0,0 +1,40 @@
|
|||
using Remora.Results;
|
||||
|
||||
namespace TeamOctolings.Octobot.Extensions;
|
||||
|
||||
public static class CollectionExtensions
|
||||
{
|
||||
public static TResult? MaxOrDefault<TSource, TResult>(
|
||||
this IEnumerable<TSource> source, Func<TSource, TResult> selector)
|
||||
{
|
||||
var list = source.ToList();
|
||||
return list.Count > 0 ? list.Max(selector) : default;
|
||||
}
|
||||
|
||||
public static void AddIfFailed(this List<Result> list, Result result)
|
||||
{
|
||||
if (!result.IsSuccess)
|
||||
{
|
||||
list.Add(result);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Return an appropriate result for a list of failed results. The list must only contain failed results.
|
||||
/// </summary>
|
||||
/// <param name="list">The list of failed results.</param>
|
||||
/// <returns>
|
||||
/// A successful result if the list is empty, the only Result in the list, or <see cref="AggregateError" />
|
||||
/// containing all results from the list.
|
||||
/// </returns>
|
||||
/// <exception cref="InvalidOperationException"></exception>
|
||||
public static Result AggregateErrors(this List<Result> list)
|
||||
{
|
||||
return list.Count switch
|
||||
{
|
||||
0 => Result.Success,
|
||||
1 => list[0],
|
||||
_ => new AggregateError(list.Cast<IResult>().ToArray())
|
||||
};
|
||||
}
|
||||
}
|
19
TeamOctolings.Octobot/Extensions/CommandContextExtensions.cs
Normal file
19
TeamOctolings.Octobot/Extensions/CommandContextExtensions.cs
Normal file
|
@ -0,0 +1,19 @@
|
|||
using Remora.Discord.Commands.Contexts;
|
||||
using Remora.Discord.Commands.Extensions;
|
||||
using Remora.Rest.Core;
|
||||
|
||||
namespace TeamOctolings.Octobot.Extensions;
|
||||
|
||||
public static class CommandContextExtensions
|
||||
{
|
||||
public static bool TryGetContextIDs(
|
||||
this ICommandContext context, out Snowflake guildId,
|
||||
out Snowflake channelId, out Snowflake executorId)
|
||||
{
|
||||
channelId = default;
|
||||
executorId = default;
|
||||
return context.TryGetGuildID(out guildId)
|
||||
&& context.TryGetChannelID(out channelId)
|
||||
&& context.TryGetUserID(out executorId);
|
||||
}
|
||||
}
|
31
TeamOctolings.Octobot/Extensions/DiffPaneModelExtensions.cs
Normal file
31
TeamOctolings.Octobot/Extensions/DiffPaneModelExtensions.cs
Normal file
|
@ -0,0 +1,31 @@
|
|||
using System.Text;
|
||||
using DiffPlex.DiffBuilder.Model;
|
||||
|
||||
namespace TeamOctolings.Octobot.Extensions;
|
||||
|
||||
public static class DiffPaneModelExtensions
|
||||
{
|
||||
public static string AsMarkdown(this DiffPaneModel model)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
foreach (var line in model.Lines)
|
||||
{
|
||||
if (line.Type is ChangeType.Deleted)
|
||||
{
|
||||
builder.Append("-- ");
|
||||
}
|
||||
|
||||
if (line.Type is ChangeType.Inserted)
|
||||
{
|
||||
builder.Append("++ ");
|
||||
}
|
||||
|
||||
if (line.Type is not ChangeType.Imaginary)
|
||||
{
|
||||
builder.AppendLine(line.Text.SanitizeForDiffBlock());
|
||||
}
|
||||
}
|
||||
|
||||
return builder.ToString().InBlockCode("diff");
|
||||
}
|
||||
}
|
149
TeamOctolings.Octobot/Extensions/EmbedBuilderExtensions.cs
Normal file
149
TeamOctolings.Octobot/Extensions/EmbedBuilderExtensions.cs
Normal file
|
@ -0,0 +1,149 @@
|
|||
using Remora.Discord.API;
|
||||
using Remora.Discord.API.Abstractions.Objects;
|
||||
using Remora.Discord.API.Objects;
|
||||
using Remora.Discord.Extensions.Embeds;
|
||||
using Remora.Rest.Core;
|
||||
|
||||
namespace TeamOctolings.Octobot.Extensions;
|
||||
|
||||
public static class EmbedBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Adds a footer representing that an action was performed by a <paramref name="user" />.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder to add the footer to.</param>
|
||||
/// <param name="user">The user that performed the action whose tag and avatar to use.</param>
|
||||
/// <returns>The builder with the added footer.</returns>
|
||||
public static EmbedBuilder WithActionFooter(this EmbedBuilder builder, IUser user)
|
||||
{
|
||||
var avatarUrlResult = CDN.GetUserAvatarUrl(user, imageSize: 256);
|
||||
var avatarUrl = avatarUrlResult.IsSuccess
|
||||
? avatarUrlResult.Entity.AbsoluteUri
|
||||
: CDN.GetDefaultUserAvatarUrl(user, imageSize: 256).Entity.AbsoluteUri;
|
||||
|
||||
return builder.WithFooter(
|
||||
new EmbedFooter($"{Messages.IssuedBy}:\n{user.GetTag()}", avatarUrl));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a title using the author field, making it smaller than using the title field.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder to add the small title to.</param>
|
||||
/// <param name="text">The text of the small title.</param>
|
||||
/// <param name="avatarSource">The user whose avatar to use in the small title.</param>
|
||||
/// <returns>The builder with the added small title in the author field.</returns>
|
||||
public static EmbedBuilder WithSmallTitle(
|
||||
this EmbedBuilder builder, string text, IUser? avatarSource = null)
|
||||
{
|
||||
Uri? avatarUrl = null;
|
||||
if (avatarSource is not null)
|
||||
{
|
||||
var avatarUrlResult = CDN.GetUserAvatarUrl(avatarSource, imageSize: 256);
|
||||
|
||||
avatarUrl = avatarUrlResult.IsSuccess
|
||||
? avatarUrlResult.Entity
|
||||
: CDN.GetDefaultUserAvatarUrl(avatarSource, imageSize: 256).Entity;
|
||||
}
|
||||
|
||||
builder.Author = new EmbedAuthorBuilder(text, iconUrl: avatarUrl?.AbsoluteUri);
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a user avatar in the thumbnail field.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder to add the thumbnail to.</param>
|
||||
/// <param name="avatarSource">The user whose avatar to use in the thumbnail field.</param>
|
||||
/// <returns>The builder with the added avatar in the thumbnail field.</returns>
|
||||
public static EmbedBuilder WithLargeUserAvatar(
|
||||
this EmbedBuilder builder, IUser avatarSource)
|
||||
{
|
||||
var avatarUrlResult = CDN.GetUserAvatarUrl(avatarSource, imageSize: 256);
|
||||
var avatarUrl = avatarUrlResult.IsSuccess
|
||||
? avatarUrlResult.Entity
|
||||
: CDN.GetDefaultUserAvatarUrl(avatarSource, imageSize: 256).Entity;
|
||||
|
||||
return builder.WithThumbnailUrl(avatarUrl.AbsoluteUri);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a guild icon in the thumbnail field.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder to add the thumbnail to.</param>
|
||||
/// <param name="iconSource">The guild whose icon to use in the thumbnail field.</param>
|
||||
/// <returns>The builder with the added icon in the thumbnail field.</returns>
|
||||
public static EmbedBuilder WithLargeGuildIcon(
|
||||
this EmbedBuilder builder, IGuild iconSource)
|
||||
{
|
||||
var iconUrlResult = CDN.GetGuildIconUrl(iconSource, imageSize: 256);
|
||||
return iconUrlResult.IsSuccess
|
||||
? builder.WithThumbnailUrl(iconUrlResult.Entity.AbsoluteUri)
|
||||
: builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a guild banner in the image field.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder to add the image to.</param>
|
||||
/// <param name="bannerSource">The guild whose banner to use in the image field.</param>
|
||||
/// <returns>The builder with the added banner in the image field.</returns>
|
||||
public static EmbedBuilder WithGuildBanner(
|
||||
this EmbedBuilder builder, IGuild bannerSource)
|
||||
{
|
||||
return bannerSource.Banner is not null
|
||||
? builder.WithImageUrl(CDN.GetGuildBannerUrl(bannerSource).Entity.AbsoluteUri)
|
||||
: builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a footer representing that the action was performed in the <paramref name="guild" />.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder to add the footer to.</param>
|
||||
/// <param name="guild">The guild whose name and icon to use.</param>
|
||||
/// <returns>The builder with the added footer.</returns>
|
||||
public static EmbedBuilder WithGuildFooter(this EmbedBuilder builder, IGuild guild)
|
||||
{
|
||||
var iconUrlResult = CDN.GetGuildIconUrl(guild, imageSize: 256);
|
||||
var iconUrl = iconUrlResult.IsSuccess
|
||||
? iconUrlResult.Entity.AbsoluteUri
|
||||
: default(Optional<string>);
|
||||
|
||||
return builder.WithFooter(new EmbedFooter(guild.Name, iconUrl));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a title representing that the action happened in the <paramref name="guild" />.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder to add the title to.</param>
|
||||
/// <param name="guild">The guild whose name and icon to use.</param>
|
||||
/// <returns>The builder with the added title.</returns>
|
||||
public static EmbedBuilder WithGuildTitle(this EmbedBuilder builder, IGuild guild)
|
||||
{
|
||||
var iconUrlResult = CDN.GetGuildIconUrl(guild, imageSize: 256);
|
||||
var iconUrl = iconUrlResult.IsSuccess
|
||||
? iconUrlResult.Entity.AbsoluteUri
|
||||
: null;
|
||||
|
||||
builder.Author = new EmbedAuthorBuilder(guild.Name, iconUrl: iconUrl);
|
||||
return builder;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Adds a scheduled event's cover image.
|
||||
/// </summary>
|
||||
/// <param name="builder">The builder to add the image to.</param>
|
||||
/// <param name="eventId">The ID of the scheduled event whose image to use.</param>
|
||||
/// <param name="imageHashOptional">The Optional containing the image hash.</param>
|
||||
/// <returns>The builder with the added cover image.</returns>
|
||||
public static EmbedBuilder WithEventCover(
|
||||
this EmbedBuilder builder, Snowflake eventId, Optional<IImageHash?> imageHashOptional)
|
||||
{
|
||||
if (!imageHashOptional.IsDefined(out var imageHash))
|
||||
{
|
||||
return builder;
|
||||
}
|
||||
|
||||
var iconUrlResult = CDN.GetGuildScheduledEventCoverUrl(eventId, imageHash, imageSize: 1024);
|
||||
return iconUrlResult.IsDefined(out var iconUrl) ? builder.WithImageUrl(iconUrl.AbsoluteUri) : builder;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
using Remora.Discord.API.Objects;
|
||||
using Remora.Discord.Commands.Feedback.Messages;
|
||||
using Remora.Discord.Commands.Feedback.Services;
|
||||
using Remora.Results;
|
||||
|
||||
namespace TeamOctolings.Octobot.Extensions;
|
||||
|
||||
public static class FeedbackServiceExtensions
|
||||
{
|
||||
public static async Task<Result> SendContextualEmbedResultAsync(
|
||||
this IFeedbackService feedback, Result<Embed> embedResult,
|
||||
FeedbackMessageOptions? options = null, CancellationToken ct = default)
|
||||
{
|
||||
if (!embedResult.IsDefined(out var embed))
|
||||
{
|
||||
return ResultExtensions.FromError(embedResult);
|
||||
}
|
||||
|
||||
return (Result)await feedback.SendContextualEmbedAsync(embed, options, ct);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
using Remora.Discord.API.Abstractions.Objects;
|
||||
using Remora.Rest.Core;
|
||||
using Remora.Results;
|
||||
|
||||
namespace TeamOctolings.Octobot.Extensions;
|
||||
|
||||
public static class GuildScheduledEventExtensions
|
||||
{
|
||||
public static Result TryGetExternalEventData(this IGuildScheduledEvent scheduledEvent, out DateTimeOffset endTime,
|
||||
out string? location)
|
||||
{
|
||||
endTime = default;
|
||||
location = default;
|
||||
if (!scheduledEvent.EntityMetadata.AsOptional().IsDefined(out var metadata))
|
||||
{
|
||||
return new ArgumentNullError(nameof(scheduledEvent.EntityMetadata));
|
||||
}
|
||||
|
||||
if (!metadata.Location.IsDefined(out location))
|
||||
{
|
||||
return new ArgumentNullError(nameof(metadata.Location));
|
||||
}
|
||||
|
||||
return scheduledEvent.ScheduledEndTime.AsOptional().IsDefined(out endTime)
|
||||
? Result.Success
|
||||
: new ArgumentNullError(nameof(scheduledEvent.ScheduledEndTime));
|
||||
}
|
||||
}
|
40
TeamOctolings.Octobot/Extensions/LoggerExtensions.cs
Normal file
40
TeamOctolings.Octobot/Extensions/LoggerExtensions.cs
Normal file
|
@ -0,0 +1,40 @@
|
|||
using Microsoft.Extensions.Logging;
|
||||
using Remora.Results;
|
||||
|
||||
namespace TeamOctolings.Octobot.Extensions;
|
||||
|
||||
public static class LoggerExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks if the <paramref name="result" /> has failed due to an error that has resulted from neither invalid user
|
||||
/// input nor the execution environment and logs the error using the provided <paramref name="logger" />.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// This has special behavior for <see cref="ExceptionError" /> - its exception will be passed to the
|
||||
/// <paramref name="logger" />
|
||||
/// </remarks>
|
||||
/// <param name="logger">The logger to use.</param>
|
||||
/// <param name="result">The Result whose error check.</param>
|
||||
/// <param name="message">The message to use if this result has failed.</param>
|
||||
public static void LogResult(this ILogger logger, IResult result, string? message = "")
|
||||
{
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (result.Error is ExceptionError exe)
|
||||
{
|
||||
if (exe.Exception is TaskCanceledException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogError(exe.Exception, "{ErrorMessage}", message);
|
||||
return;
|
||||
}
|
||||
|
||||
logger.LogWarning("{UserMessage}{NewLine}{ResultErrorMessage}", message, Environment.NewLine,
|
||||
result.Error.Message);
|
||||
}
|
||||
}
|
16
TeamOctolings.Octobot/Extensions/MarkdownExtensions.cs
Normal file
16
TeamOctolings.Octobot/Extensions/MarkdownExtensions.cs
Normal file
|
@ -0,0 +1,16 @@
|
|||
namespace TeamOctolings.Octobot.Extensions;
|
||||
|
||||
public static class MarkdownExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Formats a string to use Markdown Bullet formatting.
|
||||
/// </summary>
|
||||
/// <param name="text">The input text to format.</param>
|
||||
/// <returns>
|
||||
/// A markdown-formatted bullet string.
|
||||
/// </returns>
|
||||
public static string BulletPoint(string text)
|
||||
{
|
||||
return $"- {text}";
|
||||
}
|
||||
}
|
65
TeamOctolings.Octobot/Extensions/ResultExtensions.cs
Normal file
65
TeamOctolings.Octobot/Extensions/ResultExtensions.cs
Normal file
|
@ -0,0 +1,65 @@
|
|||
using System.Diagnostics;
|
||||
using Microsoft.Extensions.Logging;
|
||||
using Remora.Results;
|
||||
|
||||
namespace TeamOctolings.Octobot.Extensions;
|
||||
|
||||
public static class ResultExtensions
|
||||
{
|
||||
public static Result FromError(Result result)
|
||||
{
|
||||
LogResultStackTrace(result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public static Result FromError<T>(Result<T> result)
|
||||
{
|
||||
var casted = (Result)result;
|
||||
LogResultStackTrace(casted);
|
||||
|
||||
return casted;
|
||||
}
|
||||
|
||||
private static void LogResultStackTrace(Result result)
|
||||
{
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (Utility.StaticLogger is null)
|
||||
{
|
||||
throw new InvalidOperationException();
|
||||
}
|
||||
|
||||
Utility.StaticLogger.LogError("{ErrorType}: {ErrorMessage}{NewLine}{StackTrace}",
|
||||
result.Error.GetType().FullName, result.Error.Message, Environment.NewLine, ConstructStackTrace());
|
||||
|
||||
var inner = result.Inner;
|
||||
while (inner is { IsSuccess: false })
|
||||
{
|
||||
Utility.StaticLogger.LogError("Caused by: {ResultType}: {ResultMessage}",
|
||||
inner.Error.GetType().FullName, inner.Error.Message);
|
||||
|
||||
inner = inner.Inner;
|
||||
}
|
||||
}
|
||||
|
||||
private static string ConstructStackTrace()
|
||||
{
|
||||
var stackArray = new StackTrace(3, true).ToString().Split(Environment.NewLine).ToList();
|
||||
for (var i = stackArray.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var frame = stackArray[i];
|
||||
var trimmed = frame.TrimStart();
|
||||
if (trimmed.StartsWith("at System.Threading", StringComparison.Ordinal)
|
||||
|| trimmed.StartsWith("at System.Runtime.CompilerServices", StringComparison.Ordinal))
|
||||
{
|
||||
stackArray.RemoveAt(i);
|
||||
}
|
||||
}
|
||||
|
||||
return string.Join(Environment.NewLine, stackArray);
|
||||
}
|
||||
}
|
32
TeamOctolings.Octobot/Extensions/SnowflakeExtensions.cs
Normal file
32
TeamOctolings.Octobot/Extensions/SnowflakeExtensions.cs
Normal file
|
@ -0,0 +1,32 @@
|
|||
using Remora.Rest.Core;
|
||||
|
||||
namespace TeamOctolings.Octobot.Extensions;
|
||||
|
||||
public static class SnowflakeExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Checks whether this Snowflake has any value set.
|
||||
/// </summary>
|
||||
/// <param name="snowflake">The Snowflake to check.</param>
|
||||
/// <returns>true if the Snowflake has no value set or it's set to 0, false otherwise.</returns>
|
||||
public static bool Empty(this Snowflake snowflake)
|
||||
{
|
||||
return snowflake.Value is 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Checks whether this snowflake is empty (see <see cref="Empty" />) or it's equal to
|
||||
/// <paramref name="anotherSnowflake" />
|
||||
/// </summary>
|
||||
/// <param name="snowflake">The Snowflake to check for emptiness</param>
|
||||
/// <param name="anotherSnowflake">The Snowflake to check for equality with <paramref name="snowflake" />.</param>
|
||||
/// <returns>
|
||||
/// true if <paramref name="snowflake" /> is empty or is equal to <paramref name="anotherSnowflake" />, false
|
||||
/// otherwise.
|
||||
/// </returns>
|
||||
/// <seealso cref="Empty" />
|
||||
public static bool EmptyOrEqualTo(this Snowflake snowflake, Snowflake anotherSnowflake)
|
||||
{
|
||||
return snowflake.Empty() || snowflake == anotherSnowflake;
|
||||
}
|
||||
}
|
62
TeamOctolings.Octobot/Extensions/StringBuilderExtensions.cs
Normal file
62
TeamOctolings.Octobot/Extensions/StringBuilderExtensions.cs
Normal file
|
@ -0,0 +1,62 @@
|
|||
using System.Text;
|
||||
|
||||
namespace TeamOctolings.Octobot.Extensions;
|
||||
|
||||
public static class StringBuilderExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Appends the input string with Markdown Bullet formatting to the specified <see cref="StringBuilder" /> object.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="StringBuilder" /> object.</param>
|
||||
/// <param name="value">The string to append with bullet point.</param>
|
||||
/// <returns>
|
||||
/// The builder with the appended string with Markdown Bullet formatting.
|
||||
/// </returns>
|
||||
public static StringBuilder AppendBulletPoint(this StringBuilder builder, string? value)
|
||||
{
|
||||
return builder.Append("- ").Append(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends the input string with Markdown Sub-Bullet formatting to the specified <see cref="StringBuilder" /> object.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="StringBuilder" /> object.</param>
|
||||
/// <param name="value">The string to append with sub-bullet point.</param>
|
||||
/// <returns>
|
||||
/// The builder with the appended string with Markdown Sub-Bullet formatting.
|
||||
/// </returns>
|
||||
public static StringBuilder AppendSubBulletPoint(this StringBuilder builder, string? value)
|
||||
{
|
||||
return builder.Append(" - ").Append(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends the input string with Markdown Bullet formatting followed by
|
||||
/// the default line terminator to the end of specified <see cref="StringBuilder" /> object.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="StringBuilder" /> object.</param>
|
||||
/// <param name="value">The string to append with bullet point.</param>
|
||||
/// <returns>
|
||||
/// The builder with the appended string with Markdown Bullet formatting
|
||||
/// and default line terminator at the end.
|
||||
/// </returns>
|
||||
public static StringBuilder AppendBulletPointLine(this StringBuilder builder, string? value)
|
||||
{
|
||||
return builder.Append("- ").AppendLine(value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Appends the input string with Markdown Sub-Bullet formatting followed by
|
||||
/// the default line terminator to the end of specified <see cref="StringBuilder" /> object.
|
||||
/// </summary>
|
||||
/// <param name="builder">The <see cref="StringBuilder" /> object.</param>
|
||||
/// <param name="value">The string to append with sub-bullet point.</param>
|
||||
/// <returns>
|
||||
/// The builder with the appended string with Markdown Sub-Bullet formatting
|
||||
/// and default line terminator at the end.
|
||||
/// </returns>
|
||||
public static StringBuilder AppendSubBulletPointLine(this StringBuilder builder, string? value)
|
||||
{
|
||||
return builder.Append(" - ").AppendLine(value);
|
||||
}
|
||||
}
|
66
TeamOctolings.Octobot/Extensions/StringExtensions.cs
Normal file
66
TeamOctolings.Octobot/Extensions/StringExtensions.cs
Normal file
|
@ -0,0 +1,66 @@
|
|||
using System.Net;
|
||||
using Remora.Discord.Extensions.Formatting;
|
||||
|
||||
namespace TeamOctolings.Octobot.Extensions;
|
||||
|
||||
public static class StringExtensions
|
||||
{
|
||||
private const string ZeroWidthSpace = "";
|
||||
|
||||
/// <summary>
|
||||
/// Sanitizes a string for use in <see cref="Markdown.BlockCode(string)" /> by inserting zero-width spaces in between
|
||||
/// symbols used to format the string with block code.
|
||||
/// </summary>
|
||||
/// <param name="s">The string to sanitize.</param>
|
||||
/// <returns>The sanitized string that can be safely used in <see cref="Markdown.BlockCode(string)" />.</returns>
|
||||
private static string SanitizeForBlockCode(this string s)
|
||||
{
|
||||
return s.Replace("```", $"{ZeroWidthSpace}`{ZeroWidthSpace}`{ZeroWidthSpace}`{ZeroWidthSpace}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sanitizes a string for use in <see cref="Markdown.BlockCode(string, string)" /> when "language" is "diff" by
|
||||
/// prepending a zero-width space before the input string to prevent Discord from applying syntax highlighting.
|
||||
/// </summary>
|
||||
/// <remarks>This does not call <see cref="SanitizeForBlockCode"/>, you have to do so yourself if needed.</remarks>
|
||||
/// <param name="s">The string to sanitize.</param>
|
||||
/// <returns>The sanitized string that can be safely used in <see cref="Markdown.BlockCode(string, string)" /> with "diff" as the language.</returns>
|
||||
public static string SanitizeForDiffBlock(this string s)
|
||||
{
|
||||
return $"{ZeroWidthSpace}{s}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sanitizes a string (see <see cref="SanitizeForBlockCode" />) and formats the string to use Markdown Block Code
|
||||
/// formatting with a specified
|
||||
/// language for syntax highlighting.
|
||||
/// </summary>
|
||||
/// <param name="s">The string to sanitize and format.</param>
|
||||
/// <param name="language"></param>
|
||||
/// <returns>
|
||||
/// The sanitized string formatted to use Markdown Block Code with a specified
|
||||
/// language for syntax highlighting.
|
||||
/// </returns>
|
||||
public static string InBlockCode(this string s, string language = "")
|
||||
{
|
||||
s = s.SanitizeForBlockCode();
|
||||
return
|
||||
$"```{language}\n{s.SanitizeForBlockCode()}{(s.EndsWith('`') || string.IsNullOrWhiteSpace(s) ? " " : "")}```";
|
||||
}
|
||||
|
||||
public static string Localized(this string key)
|
||||
{
|
||||
return Messages.ResourceManager.GetString(key, Messages.Culture) ?? key;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Encodes a string to allow its transmission in request headers.
|
||||
/// </summary>
|
||||
/// <remarks>Used when encountering "Request headers must contain only ASCII characters".</remarks>
|
||||
/// <param name="s">The string to encode.</param>
|
||||
/// <returns>An encoded string with spaces kept intact.</returns>
|
||||
public static string EncodeHeader(this string s)
|
||||
{
|
||||
return WebUtility.UrlEncode(s).Replace('+', ' ');
|
||||
}
|
||||
}
|
12
TeamOctolings.Octobot/Extensions/UInt64Extensions.cs
Normal file
12
TeamOctolings.Octobot/Extensions/UInt64Extensions.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
using Remora.Discord.API;
|
||||
using Remora.Rest.Core;
|
||||
|
||||
namespace TeamOctolings.Octobot.Extensions;
|
||||
|
||||
public static class UInt64Extensions
|
||||
{
|
||||
public static Snowflake ToSnowflake(this ulong id)
|
||||
{
|
||||
return DiscordSnowflake.New(id);
|
||||
}
|
||||
}
|
11
TeamOctolings.Octobot/Extensions/UserExtensions.cs
Normal file
11
TeamOctolings.Octobot/Extensions/UserExtensions.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
using Remora.Discord.API.Abstractions.Objects;
|
||||
|
||||
namespace TeamOctolings.Octobot.Extensions;
|
||||
|
||||
public static class UserExtensions
|
||||
{
|
||||
public static string GetTag(this IUser user)
|
||||
{
|
||||
return user.Discriminator is 0000 ? $"@{user.Username}" : $"{user.Username}#{user.Discriminator:0000}";
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue