diff --git a/Directory.Build.props b/Directory.Build.props
index faba107..0bfa760 100644
--- a/Directory.Build.props
+++ b/Directory.Build.props
@@ -1,9 +1,5 @@
-
- enable
-
-
Mara Contributors
LuzFaltex
@@ -20,5 +16,47 @@
..\output\$(Configuration)\
+
+ enable
+ true
+
+
+ CS8600;
+ CS8601;
+ CS8602;
+ CS8603;
+ CS8604;
+ CS8608;
+ CS8609;
+ CS8610;
+ CS8611;
+ CS8612;
+ CS8613;
+ CS8614;
+ CS8615;
+ CS8616;
+ CS8617;
+ CS8618;
+ CS8619;
+ CS8620;
+ CS8621;
+ CS8622;
+ CS8625;
+ CS8626;
+ CS8629;
+ CS8631;
+ CS8633;
+ CS8634;
+ CS8638;
+ CS8639;
+ CS8643;
+ CS8644;
+ CS8645;
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Mara.Common/Constants.cs b/Mara.Common/Constants.cs
index 0d2ffd8..ecc67c3 100644
--- a/Mara.Common/Constants.cs
+++ b/Mara.Common/Constants.cs
@@ -1,13 +1,10 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace Mara.Common
+namespace Mara.Common
{
- public class Constants
+ ///
+ /// Provides a set of constant values
+ ///
+ public static class Constants
{
-
+
}
}
diff --git a/Mara.Common/Discord/EmbedBuilder.cs b/Mara.Common/Discord/EmbedBuilder.cs
index b439af6..12888a9 100644
--- a/Mara.Common/Discord/EmbedBuilder.cs
+++ b/Mara.Common/Discord/EmbedBuilder.cs
@@ -4,7 +4,6 @@
using System.ComponentModel.DataAnnotations;
using System.Drawing;
using System.Linq;
-using System.Security.Cryptography;
using Mara.Common.Extensions;
using Mara.Common.Results;
using Remora.Discord.API;
@@ -21,18 +20,29 @@ public class EmbedBuilder
public string Title { get; set; }
public EmbedType Type { get; set; }
+
[MaxLength(EmbedConstants.MaxDescriptionLength)]
public string Description { get; set; }
+
[Url]
+
public string Url { get; set; }
+
public DateTimeOffset Timestamp { get; set; }
+
public Color Color { get; set; }
+
public IEmbedFooter Footer { get; set; }
- public IEmbedImage Image { get; set; }
- public IEmbedThumbnail Thumbnail { get; set; }
- public IEmbedAuthor Author { get; set; }
+
+ public IEmbedImage? Image { get; set; }
+
+ public IEmbedThumbnail? Thumbnail { get; set; }
+
+ public IEmbedAuthor? Author { get; set; }
+
public IReadOnlyList Fields => new ReadOnlyCollection(_fields);
+
private IList _fields;
public EmbedBuilder() : this(new List(EmbedConstants.MaxFieldCount))
@@ -55,6 +65,7 @@ private EmbedBuilder(List fields)
Footer = EmbedConstants.DefaultFooter;
Image = null;
Thumbnail = null;
+ Author = null;
_fields = fields;
}
@@ -68,18 +79,19 @@ public static EmbedBuilder FromEmbed(Embed embed)
Timestamp = embed.Timestamp.GetValueOrDefault(DateTimeOffset.UtcNow),
Color = embed.Colour.GetValueOrDefault(EmbedConstants.DefaultColor),
Footer = embed.Footer.GetValueOrDefault(EmbedConstants.DefaultFooter),
- Image = embed.Image.GetValueOrDefault(default),
- Thumbnail = embed.Thumbnail.GetValueOrDefault(default)
+ Image = embed.Image.HasValue ? embed.Image.Value : null,
+ Thumbnail = embed.Thumbnail.HasValue ? embed.Thumbnail.Value : null
};
+ ///
+ /// Returns the overall length of the embed.
+ ///
public int Length
{
get
{
int titleLength = Title.Length;
- int authorLength = Author.Name.HasValue
- ? Author.Name.Value!.Length
- : 0;
+ int authorLength = Author?.Name.Length ?? 0;
int descriptionLength = Description.Length;
int footerLength = Footer?.Text.Length ?? 0;
int fieldSum = _fields.Sum(field => field.Name.Length + field.Value.Length);
@@ -182,9 +194,9 @@ public EmbedBuilder WithTimestamp(DateTimeOffset dateTimeOffset)
///
/// The current builder.
///
- public EmbedBuilder WithColor(Color colour)
+ public EmbedBuilder WithColor(Color color)
{
- Color = colour;
+ Color = color;
return this;
}
@@ -194,7 +206,7 @@ public EmbedBuilder WithColor(Color colour)
///
/// The current builder.
///
- public EmbedBuilder WithAuthor([MaxLength(EmbedConstants.MaxAuthorNameLength)] string name, [Url] string url = default, [Url] string iconUrl = default)
+ public EmbedBuilder WithAuthor([MaxLength(EmbedConstants.MaxAuthorNameLength)] string name, [Url] string url = "", [Url] string iconUrl = "")
{
Author = new EmbedAuthor(name, url, iconUrl);
return this;
@@ -220,7 +232,11 @@ public EmbedBuilder WithUserAsAuthor(IUser user)
///
/// The current builder.
///
- public EmbedBuilder WithFooter(string text, [Url] string iconUrl = default)
+ public EmbedBuilder WithFooter
+ (
+ [MaxLength(EmbedConstants.MaxFooterTextLength)] string text,
+ [Url] string iconUrl = ""
+ )
{
if (text.Length > EmbedConstants.MaxFooterTextLength)
throw new ArgumentException(
@@ -254,7 +270,7 @@ public EmbedBuilder AddField(string name, string value, bool inline = false)
/// .
///
/// The field builder class containing the field properties.
- /// Field count exceeds .
+ /// Field count exceeds .
///
/// The current builder.
///
@@ -286,23 +302,23 @@ public EmbedBuilder SetFields(IList fields)
///
/// The built embed object.
///
- /// Total embed length exceeds .
+ /// Total embed length exceeds .
public Embed Build()
=> Length > EmbedConstants.MaxEmbedLength
? throw new InvalidOperationException(
$"Total embed length must be less than or equal to {EmbedConstants.MaxEmbedLength}.")
: new Embed()
{
- Title = new Optional(Title),
- Type = new Optional(Type),
- Description = new Optional(Description),
- Url = new Optional(Url),
- Timestamp = new Optional(Timestamp),
- Colour = new Optional(Color),
+ Title = Title,
+ Type = Type,
+ Description = Description,
+ Url = Url,
+ Timestamp = Timestamp,
+ Colour = Color,
Footer = new Optional(Footer),
- Image = new Optional(Image),
- Thumbnail = new Optional(Thumbnail),
- Author = new Optional(Author),
+ Image = Image is null ? default : new Optional(Image),
+ Thumbnail = Thumbnail is null ? default : new Optional(Thumbnail),
+ Author = Author is null ? default : new Optional(Author),
Fields = new Optional>(Fields)
};
}
diff --git a/Mara.Common/Discord/EmbedConstants.cs b/Mara.Common/Discord/EmbedConstants.cs
index c83d3a1..2af5db4 100644
--- a/Mara.Common/Discord/EmbedConstants.cs
+++ b/Mara.Common/Discord/EmbedConstants.cs
@@ -3,6 +3,9 @@
namespace Mara.Common.Discord
{
+ ///
+ /// A set of constants which represent restraits on Embeds.
+ ///
public static class EmbedConstants
{
///
@@ -38,6 +41,14 @@ public static class EmbedConstants
///
/// Default embed footer. "React with ❌ to remove this embed."
///
- public static readonly EmbedFooter DefaultFooter = new("React with ❌ to remove this embed.");
+ public static readonly EmbedFooter DefaultFooter = new("React with ❌ to remove this embed");
+
+ ///
+ /// A set of image assets in url form for use in embeds.
+ ///
+ public static class ImageUrls
+ {
+
+ }
}
}
diff --git a/Mara.Common/Discord/Feedback/IdentityInformationConfiguration.cs b/Mara.Common/Discord/Feedback/IdentityInformationConfiguration.cs
index a7fbe4a..0e712b4 100644
--- a/Mara.Common/Discord/Feedback/IdentityInformationConfiguration.cs
+++ b/Mara.Common/Discord/Feedback/IdentityInformationConfiguration.cs
@@ -1,7 +1,12 @@
-using Remora.Discord.Core;
+using System;
+using Remora.Discord.API.Abstractions.Objects;
+using Remora.Discord.Core;
namespace Mara.Common.Discord.Feedback
{
+ ///
+ /// Provides information about the identity of the current application.
+ ///
public sealed class IdentityInformationConfiguration
{
///
@@ -10,13 +15,8 @@ public sealed class IdentityInformationConfiguration
public Snowflake Id { get; set; }
///
- /// Gets the application id if the bot.
+ /// Gets the application behind this bot.
///
- public Snowflake ApplicationId { get; set; }
-
- ///
- /// Gets the id of the bot's owner.
- ///
- public Snowflake OwnerId { get; set; }
+ public IApplication Application { get; set; }
}
}
diff --git a/Mara.Common/Discord/FormatUtilities.cs b/Mara.Common/Discord/FormatUtilities.cs
index aec2a20..3e92c6f 100644
--- a/Mara.Common/Discord/FormatUtilities.cs
+++ b/Mara.Common/Discord/FormatUtilities.cs
@@ -1,14 +1,25 @@
-using System.Linq;
+using System;
+using System.Linq;
using System.Text.RegularExpressions;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Objects;
namespace Mara.Common.Discord
{
- public static class FormatUtilities
+ public static class StringUtilities
{
+ ///
+ /// Represents a zero width space character.
+ ///
public const char ZeroWidthSpace = '\x200b';
+ ///
+ /// Represents an empty string. This field is constant.
+ ///
+ public const string EmptyString = "";
+ }
+ public static class FormatUtilities
+ {
// Characters which need escaping
private static readonly string[] SensitiveCharacters = { "\\", "*", "_", "~", "`", "|", ">" };
@@ -66,6 +77,8 @@ public static string Mention(IChannel channel)
/// Returns a string with spoiler formatting.
public static string Spoiler(string text) => $"||{text}||";
/// Returns a markdown-formatted URL. Only works in descriptions and fields.
+ public static string Url(string url) => $"[{url}]({url})";
+ /// Returns a markdown-formatted URL. Only works in descriptions and fields.
public static string Url(string text, string url) => $"[{text}]({url})";
/// Escapes a URL so that a preview is not generated.
public static string EscapeUrl(string url) => $"<{url}>";
@@ -76,9 +89,71 @@ public static string Mention(IChannel channel)
/// The text to wrap in a code block.
/// The code language. Ignored for single-line code blocks.
///
- public static string Code(string text, string language = null)
+ public static string Code(string text, string? language = null)
=> language is not null || text.Contains("\n")
? $"```{language ?? ""}\n{text}\n```"
: $"`{text}`";
+
+ ///
+ /// Returns a markdown timestamp which renders the specified date and time in the user's timezone and locale.
+ ///
+ /// The date and time to display.
+ /// What format to display it in.
+ /// Discord Message Formatting Timestamp Styles
+ public static string DynamicTimeStamp(DateTime timeStamp, TimeStampStyle style)
+ => DynamicTimeStamp(new DateTimeOffset(timeStamp), style);
+
+ ///
+ public static string DynamicTimeStamp(DateTimeOffset timeStamp, TimeStampStyle style = TimeStampStyle.ShortDateTime)
+ {
+ char format = style switch
+ {
+ TimeStampStyle.ShortTime => 't',
+ TimeStampStyle.LongTime => 'T',
+ TimeStampStyle.ShortDate => 'd',
+ TimeStampStyle.LongDate => 'D',
+ TimeStampStyle.ShortDateTime => 'f',
+ TimeStampStyle.LongDateTime => 'F',
+ TimeStampStyle.RelativeTime => 'R',
+ _ => 'f'
+ };
+
+ return $"";
+ }
+
+ ///
+ /// Determines the output style for the Dynamic Time Stamp
+ ///
+ public enum TimeStampStyle
+ {
+ ///
+ /// 16:20
+ ///
+ ShortTime,
+ ///
+ /// 16:20:30
+ ///
+ LongTime,
+ ///
+ /// 20/04/2021
+ ///
+ ShortDate,
+ ///
+ /// 20 April 2021
+ ///
+ LongDate,
+ ///
+ /// 20 April 2021 16:20
+ ///
+ ShortDateTime,
+ ///
+ /// Tuesday, 20 April 2021 16:20
+ ///
+ LongDateTime,
+ ///
+ /// 2 months ago
+ ///
+ RelativeTime
+ }
}
}
diff --git a/Mara.Common/Extensions/JsonExtensions.cs b/Mara.Common/Extensions/JsonExtensions.cs
deleted file mode 100644
index 5a70384..0000000
--- a/Mara.Common/Extensions/JsonExtensions.cs
+++ /dev/null
@@ -1,30 +0,0 @@
-using System.Text.Json;
-using Microsoft.Extensions.Logging;
-
-namespace Mara.Common.Extensions
-{
- public static class JsonExtensions
- {
- public static TResult? ToObject(this JsonElement element, JsonSerializerOptions jsonSerializerOptions)
- => ToObject(element.GetRawText(), jsonSerializerOptions);
- public static TResult? ToObject(this JsonElement element, ILogger logger, JsonSerializerOptions jsonSerializerOptions)
- {
- var json = element.GetRawText();
- logger.LogTrace(json);
- return ToObject(json, jsonSerializerOptions);
- }
-
- public static TResult? ToObject(this JsonDocument document, JsonSerializerOptions jsonSerializerOptions)
- => ToObject(document.RootElement.GetRawText(), jsonSerializerOptions);
-
- public static TResult? ToObject(this JsonDocument document, ILogger logger, JsonSerializerOptions jsonSerializerOptions)
- {
- var json = document.RootElement.GetRawText();
- logger.LogTrace(json);
- return ToObject(json, jsonSerializerOptions);
- }
-
- private static TResult? ToObject(string rawText, JsonSerializerOptions jsonSerializerOptions)
- => JsonSerializer.Deserialize(rawText, jsonSerializerOptions);
- }
-}
diff --git a/Mara.Common/Extensions/LoggerExtensions.cs b/Mara.Common/Extensions/LoggerExtensions.cs
new file mode 100644
index 0000000..1c3b29a
--- /dev/null
+++ b/Mara.Common/Extensions/LoggerExtensions.cs
@@ -0,0 +1,24 @@
+using Microsoft.Extensions.Logging;
+using Remora.Results;
+
+namespace Mara.Common.Extensions
+{
+ public static class LoggerExtensions
+ {
+ ///
+ /// Formats and writes an error log message
+ ///
+ /// The object this logger writes for.
+ /// The logger.
+ /// An representing the problem that occurred.
+ public static void LogError(this ILogger logger, IResultError error)
+ {
+ if (error is ExceptionError exceptionError)
+ {
+ logger.LogError(exceptionError.Exception, exceptionError.Message);
+ }
+
+ logger.LogError(error.Message);
+ }
+ }
+}
diff --git a/Mara.Common/ISkippedPlugin.cs b/Mara.Common/ISkippedPlugin.cs
index 99855b7..9bf169e 100644
--- a/Mara.Common/ISkippedPlugin.cs
+++ b/Mara.Common/ISkippedPlugin.cs
@@ -1,6 +1,4 @@
-using System;
-
-namespace Mara.Common
+namespace Mara.Common
{
///
/// Marks a plugin that should be skipped and ignored during loading.
diff --git a/Mara.Common/Mara.Common.csproj b/Mara.Common/Mara.Common.csproj
index 35d6c8c..f34e1f6 100644
--- a/Mara.Common/Mara.Common.csproj
+++ b/Mara.Common/Mara.Common.csproj
@@ -1,17 +1,22 @@
- net5.0
+ net6.0
1.0.0
$(Version)
$(Version)
-
-
-
-
+
+
+
+
+
+
+
+
+
diff --git a/Mara.Common/Models/MaraConfig.cs b/Mara.Common/Models/MaraConfig.cs
index f40c6a3..30bba9c 100644
--- a/Mara.Common/Models/MaraConfig.cs
+++ b/Mara.Common/Models/MaraConfig.cs
@@ -2,7 +2,7 @@
namespace Mara.Common.Models
{
- public class MaraConfig
+ public sealed class MaraConfig
{
public static readonly MaraConfig Default = new()
{
@@ -11,8 +11,8 @@ public class MaraConfig
PrivacyPolicyUrl = ""
};
- public string DiscordToken { get; init; }
- public Dictionary ConnectionStrings { get; init; }
- public string PrivacyPolicyUrl { get; init; }
+ public string DiscordToken { get; init; } = "";
+ public Dictionary ConnectionStrings { get; init; } = new();
+ public string PrivacyPolicyUrl { get; init; } = "";
}
}
diff --git a/Mara.Common/Results/DatabaseValueNotFoundError.cs b/Mara.Common/Results/DatabaseValueNotFoundError.cs
new file mode 100644
index 0000000..141eb24
--- /dev/null
+++ b/Mara.Common/Results/DatabaseValueNotFoundError.cs
@@ -0,0 +1,9 @@
+using Remora.Results;
+
+namespace Mara.Common.Results
+{
+ ///
+ /// Represents an error where no database models could be found with the provided query.
+ ///
+ public sealed record DatabaseValueNotFoundError() : ResultError("The requested database model(s) could not be found with the provided query.");
+}
diff --git a/Mara.Common/Results/EmbedError.cs b/Mara.Common/Results/EmbedError.cs
index 2d614e3..13a222b 100644
--- a/Mara.Common/Results/EmbedError.cs
+++ b/Mara.Common/Results/EmbedError.cs
@@ -5,5 +5,5 @@ namespace Mara.Common.Results
///
/// Represents an error which occurs when building an Embed.
///
- public record EmbedError(string Reason) : ResultError($"Failed to create an embed. {Reason}");
+ public record EmbedError(string Reason) : ResultError($"Failed to create an embed: {Reason}");
}
diff --git a/Mara.Common/ValueConverters/SnowflakeToNumberConverter.cs b/Mara.Common/ValueConverters/SnowflakeToNumberConverter.cs
new file mode 100644
index 0000000..d8d5f78
--- /dev/null
+++ b/Mara.Common/ValueConverters/SnowflakeToNumberConverter.cs
@@ -0,0 +1,21 @@
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using DiscordConstants = Remora.Discord.API.Constants;
+using Remora.Rest.Core;
+
+namespace Mara.Common.ValueConverters
+{
+ ///
+ /// Converts a Snowflake unto a ulong and back.
+ ///
+ public sealed class SnowflakeToNumberConverter : ValueConverter
+ {
+ private static readonly ConverterMappingHints _defaultHints = new(precision: 20, scale: 0);
+
+ ///
+ /// Creates a new instance of the type.
+ ///
+ public SnowflakeToNumberConverter() : base(sf => sf.Value, value => new(value, DiscordConstants.DiscordEpoch), _defaultHints)
+ {
+ }
+ }
+}
diff --git a/Mara.Runtime/Extensions/ServiceCollectionExtensions.cs b/Mara.Runtime/Extensions/ServiceCollectionExtensions.cs
deleted file mode 100644
index fa52196..0000000
--- a/Mara.Runtime/Extensions/ServiceCollectionExtensions.cs
+++ /dev/null
@@ -1,25 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using Microsoft.Extensions.DependencyInjection;
-
-namespace Mara.Runtime.Extensions
-{
- public static class ServiceCollectionExtensions
- {
- public static IServiceCollection AddMara(this IServiceCollection services)
- {
- services.AddHttpClient();
-
- // services.AddCommandHelp();
-
- services.AddMemoryCache();
-
- services.AddHostedService();
-
- return services;
- }
- }
-}
diff --git a/Mara.Runtime/Mara.Runtime.csproj b/Mara.Runtime/Mara.Runtime.csproj
index bf32754..57fb839 100644
--- a/Mara.Runtime/Mara.Runtime.csproj
+++ b/Mara.Runtime/Mara.Runtime.csproj
@@ -2,7 +2,7 @@
Exe
- net5.0
+ net6.0
add0e956-c141-46d8-b042-67f19259e4f3
0.1.0
$(Version)
@@ -10,22 +10,22 @@
-
-
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
-
+
diff --git a/Mara.Runtime/MaraBot.cs b/Mara.Runtime/MaraBot.cs
index 75d743b..c062add 100644
--- a/Mara.Runtime/MaraBot.cs
+++ b/Mara.Runtime/MaraBot.cs
@@ -2,16 +2,13 @@
using System.Globalization;
using System.Threading;
using System.Threading.Tasks;
-using Mara.Common;
-using Mara.Common.Models;
+using Mara.Common.Extensions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
-using Remora.Discord.Commands.Services;
using Remora.Discord.Gateway;
-using Remora.Plugins.Abstractions;
using Remora.Plugins.Services;
+using Remora.Results;
namespace Mara.Runtime
{
@@ -19,21 +16,17 @@ public sealed class MaraBot : BackgroundService
{
private readonly DiscordGatewayClient _discordClient;
private readonly IServiceProvider _services;
- private readonly MaraConfig _config;
private readonly IHostApplicationLifetime _applicationLifetime;
- private readonly IHostEnvironment _environment;
private readonly ILogger _logger;
private readonly PluginService _pluginService;
- private IServiceScope _scope;
+ private IServiceScope? _scope = null;
public MaraBot
(
DiscordGatewayClient discordClient,
IServiceProvider services,
- IOptions config,
IHostApplicationLifetime applicationLifetime,
- IHostEnvironment environment,
ILogger logger,
PluginService pluginService
)
@@ -41,49 +34,61 @@ PluginService pluginService
_discordClient = discordClient;
_services = services;
_applicationLifetime = applicationLifetime;
- _environment = environment;
_logger = logger;
_pluginService = pluginService;
- _config = config.Value;
}
+
+ ///
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
Thread.CurrentThread.CurrentCulture = new CultureInfo("en-us");
Thread.CurrentThread.CurrentUICulture = Thread.CurrentThread.CurrentCulture;
- _logger.LogInformation("Starting bot service...");;
+ var initResult = await InitializeAsync(stoppingToken);
+ if (!initResult.IsSuccess)
+ {
+ _logger.LogError(initResult.Error);
+ return;
+ }
+
+ _logger.LogInformation("Logging into Discord and starting the client.");
+
+ var runResult = await _discordClient.RunAsync(stoppingToken);
+
+ if (!runResult.IsSuccess)
+ {
+ _logger.LogCritical("A critical error has occurred: {Error}", runResult.Error!.Message);
+ }
+ }
- IServiceScope? scope = null;
+ private async Task InitializeAsync(CancellationToken stoppingToken)
+ {
+ _logger.LogInformation("Initializing bot service...");
+
try
{
// Create new scope for this session
- scope = _services.CreateScope();
+ _scope = _services.CreateScope();
// Register the OnStopping method with the cancellation token
stoppingToken.Register(OnStopping);
// Load plugins
- var plugins = _pluginService.LoadAvailablePlugins();
+ var pluginTree = _pluginService.LoadPluginTree();
- foreach (var plugin in plugins)
+ var initResult = await pluginTree.InitializeAsync(_scope.ServiceProvider, stoppingToken);
+ if (!initResult.IsSuccess)
{
- if (plugin is ISkippedPlugin)
- continue;
-
- var serviceScope = _services.CreateScope();
-
- if (plugin is IMigratablePlugin migratablePlugin)
- {
- if (await migratablePlugin.HasCreatedPersistentStoreAsync(serviceScope.ServiceProvider))
- {
- await migratablePlugin.MigratePluginAsync(serviceScope.ServiceProvider);
- }
- }
+ return initResult;
+ }
- await plugin.InitializeAsync(serviceScope.ServiceProvider);
+ var migrateResult = await pluginTree.MigrateAsync(_scope.ServiceProvider, stoppingToken);
+ if (migrateResult.IsSuccess)
+ {
+ return migrateResult;
}
- _scope = scope;
+ return Result.FromSuccess();
}
catch (Exception ex)
{
@@ -91,33 +96,24 @@ protected override async Task ExecuteAsync(CancellationToken stoppingToken)
OnStopping();
- throw;
+ return ex;
}
+ }
- _logger.LogInformation("Logging into Discord and starting the client.");
-
- var runResult = await _discordClient.RunAsync(stoppingToken);
-
- if (!runResult.IsSuccess)
+ private void OnStopping()
+ {
+ _logger.LogInformation("Stopping background service.");
+ try
{
- _logger.LogCritical("A critical error has occurred: {Error}", runResult.Error!.Message);
+ _applicationLifetime.StopApplication();
}
-
- void OnStopping()
+ finally
{
- _logger.LogInformation("Stopping background service.");
- try
- {
- _applicationLifetime.StopApplication();
- }
- finally
- {
- scope?.Dispose();
- _scope = null;
- }
+ _scope = null;
}
}
+ ///
public override void Dispose()
{
try
diff --git a/Mara.Runtime/Program.cs b/Mara.Runtime/Program.cs
index aeaa852..837e215 100644
--- a/Mara.Runtime/Program.cs
+++ b/Mara.Runtime/Program.cs
@@ -1,25 +1,22 @@
using System;
+using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
-using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Threading.Tasks;
-using Mara.Common;
+using Mara.Common.Discord.Feedback;
using Mara.Common.Models;
-using Mara.Runtime.Extensions;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
-using Remora.Commands.Extensions;
using Remora.Discord.API.Abstractions.Gateway.Commands;
-using Remora.Discord.API.Extensions;
using Remora.Discord.Commands.Extensions;
using Remora.Discord.Gateway;
using Remora.Discord.Gateway.Extensions;
-using Remora.Discord.Rest.Extensions;
using Remora.Plugins.Services;
+using Remora.Results;
using Serilog;
using Serilog.Events;
@@ -62,43 +59,46 @@ public static async Task Main(string[] args)
.UseDefaultServiceProvider(x => x.ValidateScopes = true)
.ConfigureServices((context, services) =>
{
- var pluginService = new PluginService();
- var plugins = pluginService.LoadAvailablePlugins().ToList();
+ var pluginServiceOptions = new PluginServiceOptions(new List() { "./Plugins" });
+ var pluginService = new PluginService(Options.Create(pluginServiceOptions));
- Console.WriteLine($"Discovered {plugins.Count} plugins.");
+ var plugins = pluginService.LoadPluginTree();
+ var configurePluginsResult = plugins.ConfigureServices(services);
+ if (!configurePluginsResult.IsSuccess)
+ {
+ Console.WriteLine($"Failed to load plugins! {configurePluginsResult.Error.Message}");
+ if (configurePluginsResult.Error is ExceptionError exe)
+ {
+ Console.WriteLine(exe.Exception.ToString());
+ }
+ }
services.Configure(context.Configuration);
services.AddSingleton(pluginService);
+ services.AddSingleton();
Debug.Assert(!string.IsNullOrEmpty(context.Configuration[nameof(MaraConfig.DiscordToken)]));
- services.AddDiscordApi();
services.AddDiscordGateway(x => context.Configuration[nameof(MaraConfig.DiscordToken)]);
- services.AddDiscordRest(x => context.Configuration[nameof(MaraConfig.DiscordToken)]);
- services.AddCommands();
services.AddDiscordCommands(enableSlash: true);
- services.AddMara();
+
+ services.AddMemoryCache();
+ services.AddHostedService();
services.Configure(x
=> x.Intents |=
GatewayIntents.DirectMessages |
GatewayIntents.GuildBans |
+ GatewayIntents.GuildEmojisAndStickers |
GatewayIntents.GuildIntegrations |
GatewayIntents.GuildInvites |
GatewayIntents.GuildMembers |
- GatewayIntents.GuildMessageReactions);
-
- foreach (var plugin in plugins)
- {
- if (plugin is ISkippedPlugin)
- continue;
-
- Console.Write($"Configuring {plugin.Name} version {plugin.Version.ToString(3)}...");
- plugin.ConfigureServices(services);
- Console.WriteLine("Done");
- }
+ GatewayIntents.GuildMessageReactions |
+ GatewayIntents.GuildMessages |
+ GatewayIntents.Guilds |
+ GatewayIntents.GuildWebhooks);
})
- .ConfigureLogging((context, builder) =>
+ .ConfigureLogging((_, builder) =>
{
Serilog.Core.Logger seriLogger = new LoggerConfiguration()
.MinimumLevel.Verbose()
@@ -110,8 +110,11 @@ public static async Task Main(string[] args)
builder.AddSerilog(seriLogger);
Log.Logger = seriLogger;
- })
- .UseConsoleLifetime();
+ });
+
+ hostBuilder = (Debugger.IsAttached && Environment.UserInteractive)
+ ? hostBuilder.UseConsoleLifetime()
+ : hostBuilder.UseWindowsService();
using var host = hostBuilder.Build();
diff --git a/Mara.Tests/Mara.Tests.csproj b/Mara.Tests/Mara.Tests.csproj
new file mode 100644
index 0000000..d2478b5
--- /dev/null
+++ b/Mara.Tests/Mara.Tests.csproj
@@ -0,0 +1,28 @@
+
+
+
+ net6.0
+
+ false
+
+
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
+
+
+
+
diff --git a/Mara.Tests/SnowflakeValueConverterTests.cs b/Mara.Tests/SnowflakeValueConverterTests.cs
new file mode 100644
index 0000000..eed40e5
--- /dev/null
+++ b/Mara.Tests/SnowflakeValueConverterTests.cs
@@ -0,0 +1,67 @@
+using Mara.Common.ValueConverters;
+using Microsoft.EntityFrameworkCore.Storage.ValueConversion;
+using Remora.Rest.Core;
+using Xunit;
+
+using DiscordConstants = Remora.Discord.API.Constants;
+
+namespace Mara.Tests
+{
+ public class SnowflakeValueConverterTests
+ {
+ private static readonly ValueConverter _snowflakeToNumber = new SnowflakeToNumberConverter();
+
+ private static readonly ulong foxtrekId = 197291773133979648;
+ private static readonly ulong maraId = 801312393069199370;
+
+ [Fact]
+ public void CanConvertSnowflakesToNumbers()
+ {
+ var converter = _snowflakeToNumber.ConvertToProviderExpression.Compile();
+
+ var foxtrek = new Snowflake(foxtrekId, DiscordConstants.DiscordEpoch);
+ var mara = new Snowflake(maraId, DiscordConstants.DiscordEpoch);
+
+ Assert.Equal(foxtrekId, converter(foxtrek));
+ Assert.Equal(maraId, converter(mara));
+ }
+
+ [Fact]
+ public void CanConvertSnowflakesToNumbersObject()
+ {
+ var converter = _snowflakeToNumber.ConvertToProvider;
+
+ var foxtrek = new Snowflake(foxtrekId, DiscordConstants.DiscordEpoch);
+ var mara = new Snowflake(maraId, DiscordConstants.DiscordEpoch);
+
+ Assert.Equal(foxtrekId, converter(foxtrek));
+ Assert.Equal(maraId, converter(mara));
+ Assert.Null(converter(null));
+ }
+
+ [Fact]
+ public void CanConvertNumbersToSnowflakes()
+ {
+ var converter = _snowflakeToNumber.ConvertFromProviderExpression.Compile();
+
+ var foxtrek = new Snowflake(foxtrekId, DiscordConstants.DiscordEpoch);
+ var mara = new Snowflake(maraId, DiscordConstants.DiscordEpoch);
+
+ Assert.Equal(foxtrek, converter(foxtrekId));
+ Assert.Equal(mara, converter(maraId));
+ }
+
+ [Fact]
+ public void CanConvertNumbersToSnowflakesObject()
+ {
+ var converter = _snowflakeToNumber.ConvertFromProvider;
+
+ var foxtrek = new Snowflake(foxtrekId, DiscordConstants.DiscordEpoch);
+ var mara = new Snowflake(maraId, DiscordConstants.DiscordEpoch);
+
+ Assert.Equal(foxtrek, converter(foxtrekId));
+ Assert.Equal(mara, converter(maraId));
+ Assert.Null(converter(null));
+ }
+ }
+}
diff --git a/Mara.sln b/Mara.sln
index aacbfe7..1688f5c 100644
--- a/Mara.sln
+++ b/Mara.sln
@@ -1,25 +1,39 @@
Microsoft Visual Studio Solution File, Format Version 12.00
-# Visual Studio Version 16
-VisualStudioVersion = 16.0.31321.278
+# Visual Studio Version 17
+VisualStudioVersion = 17.0.31903.59
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Plugins", "Plugins", "{5095E260-6923-41C6-9DF8-A395F2EE275C}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mara.Runtime", "Mara.Runtime\Mara.Runtime.csproj", "{CBD53B34-977B-4CD0-BD6E-6D03B98A191F}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mara.Runtime", "Mara.Runtime\Mara.Runtime.csproj", "{CBD53B34-977B-4CD0-BD6E-6D03B98A191F}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mara.Plugins.Core", "Plugins\Mara.Plugins.Core\Mara.Plugins.Core.csproj", "{9B5BAB08-4F92-420B-B45E-149B566664A7}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mara.Plugins.Core", "Plugins\Mara.Plugins.Core\Mara.Plugins.Core.csproj", "{9B5BAB08-4F92-420B-B45E-149B566664A7}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mara.Common", "Mara.Common\Mara.Common.csproj", "{F0E906C4-5187-48A6-864E-B526AA2FDB9D}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mara.Common", "Mara.Common\Mara.Common.csproj", "{F0E906C4-5187-48A6-864E-B526AA2FDB9D}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mara.Plugins.Mediator", "Plugins\Mara.Plugins.MediatR\Mara.Plugins.Mediator.csproj", "{189647A0-1828-4640-9504-C70AB7971370}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mara.Plugins.Mediator", "Plugins\Mara.Plugins.MediatR\Mara.Plugins.Mediator.csproj", "{189647A0-1828-4640-9504-C70AB7971370}"
EndProject
-Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mara.Plugins.BetterEmbeds", "Plugins\Mara.Plugins.BetterEmbed\Mara.Plugins.BetterEmbeds.csproj", "{AC75C1EC-34E5-4BBE-8060-78D174F4D6D3}"
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mara.Plugins.BetterEmbeds", "Plugins\Mara.Plugins.BetterEmbed\Mara.Plugins.BetterEmbeds.csproj", "{AC75C1EC-34E5-4BBE-8060-78D174F4D6D3}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{39EA8E1A-E1D9-419D-AF42-860058C74536}"
ProjectSection(SolutionItems) = preProject
Directory.Build.props = Directory.Build.props
EndProjectSection
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mara.Plugins.Consent", "Plugins\Mara.Plugins.Consent\Mara.Plugins.Consent.csproj", "{EE0456BD-8336-4C69-9E03-143B5B37BC51}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mara.Plugins.Moderation", "Plugins\Mara.Plugins.Moderation\Mara.Plugins.Moderation.csproj", "{D7B323BE-10DD-4C87-99C3-D1E56A9A95C4}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mara.Plugins.BetterEmbeds.Tests", "Plugins\Mara.Plugins.BetterEmbeds.Tests\Mara.Plugins.BetterEmbeds.Tests.csproj", "{164CD06A-7F1D-4B35-B52F-7D9751DA3719}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mara.Plugins.Core.Tests", "Plugins\Mara.Plugins.Core.Tests\Mara.Plugins.Core.Tests.csproj", "{667C812B-DC53-4AF1-A759-CEB3E186DE25}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mara.Plugins.Mediator.Tests", "Plugins\Mara.Plugins.Mediator.Tests\Mara.Plugins.Mediator.Tests.csproj", "{708A306D-15C9-4872-A894-2419BF271689}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mara.Plugins.Moderation.Tests", "Plugins\Mara.Plugins.Moderation.Tests\Mara.Plugins.Moderation.Tests.csproj", "{BA0B9865-7049-4963-8462-BC517F23F594}"
+EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Mara.Tests", "Mara.Tests\Mara.Tests.csproj", "{BA3B9272-B100-463A-8C67-16CBFDDA68BB}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -46,6 +60,34 @@ Global
{AC75C1EC-34E5-4BBE-8060-78D174F4D6D3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{AC75C1EC-34E5-4BBE-8060-78D174F4D6D3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{AC75C1EC-34E5-4BBE-8060-78D174F4D6D3}.Release|Any CPU.Build.0 = Release|Any CPU
+ {EE0456BD-8336-4C69-9E03-143B5B37BC51}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {EE0456BD-8336-4C69-9E03-143B5B37BC51}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {EE0456BD-8336-4C69-9E03-143B5B37BC51}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {EE0456BD-8336-4C69-9E03-143B5B37BC51}.Release|Any CPU.Build.0 = Release|Any CPU
+ {D7B323BE-10DD-4C87-99C3-D1E56A9A95C4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {D7B323BE-10DD-4C87-99C3-D1E56A9A95C4}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {D7B323BE-10DD-4C87-99C3-D1E56A9A95C4}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {D7B323BE-10DD-4C87-99C3-D1E56A9A95C4}.Release|Any CPU.Build.0 = Release|Any CPU
+ {164CD06A-7F1D-4B35-B52F-7D9751DA3719}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {164CD06A-7F1D-4B35-B52F-7D9751DA3719}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {164CD06A-7F1D-4B35-B52F-7D9751DA3719}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {164CD06A-7F1D-4B35-B52F-7D9751DA3719}.Release|Any CPU.Build.0 = Release|Any CPU
+ {667C812B-DC53-4AF1-A759-CEB3E186DE25}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {667C812B-DC53-4AF1-A759-CEB3E186DE25}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {667C812B-DC53-4AF1-A759-CEB3E186DE25}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {667C812B-DC53-4AF1-A759-CEB3E186DE25}.Release|Any CPU.Build.0 = Release|Any CPU
+ {708A306D-15C9-4872-A894-2419BF271689}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {708A306D-15C9-4872-A894-2419BF271689}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {708A306D-15C9-4872-A894-2419BF271689}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {708A306D-15C9-4872-A894-2419BF271689}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BA0B9865-7049-4963-8462-BC517F23F594}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BA0B9865-7049-4963-8462-BC517F23F594}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BA0B9865-7049-4963-8462-BC517F23F594}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BA0B9865-7049-4963-8462-BC517F23F594}.Release|Any CPU.Build.0 = Release|Any CPU
+ {BA3B9272-B100-463A-8C67-16CBFDDA68BB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {BA3B9272-B100-463A-8C67-16CBFDDA68BB}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {BA3B9272-B100-463A-8C67-16CBFDDA68BB}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {BA3B9272-B100-463A-8C67-16CBFDDA68BB}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -54,6 +96,12 @@ Global
{9B5BAB08-4F92-420B-B45E-149B566664A7} = {5095E260-6923-41C6-9DF8-A395F2EE275C}
{189647A0-1828-4640-9504-C70AB7971370} = {5095E260-6923-41C6-9DF8-A395F2EE275C}
{AC75C1EC-34E5-4BBE-8060-78D174F4D6D3} = {5095E260-6923-41C6-9DF8-A395F2EE275C}
+ {EE0456BD-8336-4C69-9E03-143B5B37BC51} = {5095E260-6923-41C6-9DF8-A395F2EE275C}
+ {D7B323BE-10DD-4C87-99C3-D1E56A9A95C4} = {5095E260-6923-41C6-9DF8-A395F2EE275C}
+ {164CD06A-7F1D-4B35-B52F-7D9751DA3719} = {5095E260-6923-41C6-9DF8-A395F2EE275C}
+ {667C812B-DC53-4AF1-A759-CEB3E186DE25} = {5095E260-6923-41C6-9DF8-A395F2EE275C}
+ {708A306D-15C9-4872-A894-2419BF271689} = {5095E260-6923-41C6-9DF8-A395F2EE275C}
+ {BA0B9865-7049-4963-8462-BC517F23F594} = {5095E260-6923-41C6-9DF8-A395F2EE275C}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {7F5007AB-7A59-4CAF-B60F-5FA522B11A6C}
diff --git a/Plugins/Mara.Plugins.BetterEmbed/API/RedditRestAPI.cs b/Plugins/Mara.Plugins.BetterEmbed/API/RedditRestAPI.cs
index 956b94e..7fd9a50 100644
--- a/Plugins/Mara.Plugins.BetterEmbed/API/RedditRestAPI.cs
+++ b/Plugins/Mara.Plugins.BetterEmbed/API/RedditRestAPI.cs
@@ -1,13 +1,10 @@
-using System;
-using System.Net.Http;
-using System.Text.Json;
+using System.Text.Json;
using System.Threading;
using System.Threading.Tasks;
-using Mara.Common.Extensions;
using Mara.Plugins.BetterEmbeds.Models.Reddit;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
-using Remora.Discord.Rest.Results;
+using Remora.Rest;
using Remora.Results;
namespace Mara.Plugins.BetterEmbeds.API
@@ -17,15 +14,15 @@ public sealed class RedditRestAPI
public const string PostUrl = "https://www.reddit.com/r/{0}/comments/{1}/.json";
public const string ProfileUrl = "https://www.reddit.com/user/{0}/about.json";
- private readonly HttpClient _client;
private readonly JsonSerializerOptions _serializerOptions;
private readonly ILogger _logger;
-
- public RedditRestAPI(IHttpClientFactory factory, IOptions serializerOptions, ILogger logger)
+ private readonly IRestHttpClient _restClient;
+
+ public RedditRestAPI(IRestHttpClient restClient, IOptions serializerOptions, ILogger logger)
{
- _logger = logger;
+ _restClient = restClient;
_serializerOptions = serializerOptions.Value;
- _client = factory.CreateClient();
+ _logger = logger;
}
///
@@ -33,11 +30,12 @@ public RedditRestAPI(IHttpClientFactory factory, IOptions
///
/// The subreddit this post belongs to.
/// The unique id of this post.
+ /// Whether or not to allow an empty return value.
/// The cancellation token for this operation.
/// A retrieval result which may or may not have succeeded.
public async Task> GetRedditPostAsync
(
- string subredditName,
+ string subredditName,
string postId,
bool allowNullReturn = false,
CancellationToken cancellationToken = default
@@ -45,9 +43,14 @@ public async Task> GetRedditPostAsync
{
var redditUrl = string.Format(PostUrl, subredditName, postId);
- var response = await _client.GetAsync(redditUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
-
- return await UnpackResponseAsync(response, "$[0].data.children[0].data", allowNullReturn, cancellationToken);
+ return await _restClient.GetAsync
+ (
+ redditUrl,
+ "$[0].data.children[0].data",
+ x => x.Build(),
+ allowNullReturn,
+ cancellationToken
+ );
}
public async Task> GetRedditUserAsync
@@ -59,75 +62,14 @@ public async Task> GetRedditUserAsync
{
var redditUrl = string.Format(ProfileUrl, username);
- var response = await _client.GetAsync(redditUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
-
- return await UnpackResponseAsync(response, "$.data.subreddit", allowNullReturn, cancellationToken);
- }
-
- private async Task> UnpackResponseAsync
- (
- HttpResponseMessage response,
- string path = "",
- bool allowNullReturn = false,
- CancellationToken cancellationToken = default
- )
- {
- if (!response.IsSuccessStatusCode)
- {
- return new HttpResultError(response.StatusCode, response.ReasonPhrase);
- }
-
- if (response.Content.Headers.ContentLength == 0)
- {
- return allowNullReturn
- ? Result.FromSuccess(default!)
- : throw new InvalidOperationException("Response content null, but null returns not allowed.");
- }
-
- TEntity? entity;
-
- if (string.IsNullOrEmpty(path))
- {
- entity = await JsonSerializer.DeserializeAsync
- (
- await response.Content.ReadAsStreamAsync(cancellationToken),
- new JsonSerializerOptions(JsonSerializerDefaults.Web),
- cancellationToken
- );
- }
- else
- {
- var doc = await JsonSerializer.DeserializeAsync
- (
- await response.Content.ReadAsStreamAsync(cancellationToken),
- new JsonSerializerOptions(JsonSerializerDefaults.Web),
- cancellationToken
- );
-
- var element = doc.SelectElement(path);
-
- if (!element.HasValue)
- {
- return allowNullReturn
- ? Result.FromSuccess(default!)
- : throw new InvalidOperationException("Response content null, but null returns not allowed.");
- }
-
- // _logger.LogTrace(element.Value.GetRawText());
-
- entity = element.Value.ToObject(_serializerOptions);
- }
-
- if (entity is not null)
- {
- _logger.LogTrace(JsonSerializer.Serialize(entity));
- return Result.FromSuccess(entity);
- }
-
- return allowNullReturn
- ? Result.FromSuccess(default!) // Might be TEntity?
- : throw new InvalidOperationException("Response content null, but null returns not allowed.");
-
+ return await _restClient.GetAsync
+ (
+ redditUrl,
+ "$.data.subreddit",
+ x => x.Build(),
+ allowNullReturn,
+ cancellationToken
+ );
}
}
}
diff --git a/Plugins/Mara.Plugins.BetterEmbed/BetterEmbedPlugin.cs b/Plugins/Mara.Plugins.BetterEmbed/BetterEmbedPlugin.cs
index d5c6611..aab613e 100644
--- a/Plugins/Mara.Plugins.BetterEmbed/BetterEmbedPlugin.cs
+++ b/Plugins/Mara.Plugins.BetterEmbed/BetterEmbedPlugin.cs
@@ -3,17 +3,22 @@
using System.Text.Json;
using Microsoft.Extensions.DependencyInjection;
using Mara.Plugins.BetterEmbeds;
-using Mara.Plugins.BetterEmbeds.API;
using Mara.Plugins.BetterEmbeds.MessageHandlers;
using Mara.Plugins.BetterEmbeds.Models.OEmbed;
using Mara.Plugins.BetterEmbeds.Models.Reddit;
using Mara.Plugins.BetterEmbeds.Models.Reddit.Converters;
using Mara.Plugins.BetterEmbeds.Services;
-using Remora.Discord.API.Extensions;
-using Remora.Discord.API.Json;
using Remora.Discord.Gateway.Extensions;
using Remora.Plugins.Abstractions;
using Remora.Plugins.Abstractions.Attributes;
+using Remora.Rest.Results;
+using Remora.Rest.Extensions;
+using Polly;
+using System.Net.Http;
+using Polly.Contrib.WaitAndRetry;
+using Remora.Results;
+using System.Threading.Tasks;
+using Polly.Retry;
[assembly:RemoraPlugin(typeof(BetterEmbedPlugin))]
@@ -32,10 +37,52 @@ public class BetterEmbedPlugin : PluginDescriptor
public override string Description => "Provides improved embed functionality for links Discord handles poorly.";
///
- public override void ConfigureServices(IServiceCollection serviceCollection)
+ public override Result ConfigureServices(IServiceCollection serviceCollection)
{
serviceCollection.AddScoped();
- serviceCollection.AddScoped();
+ // serviceCollection.AddRestHttpClient>>();
+
+ var retryDelay = Backoff.DecorrelatedJitterBackoffV2(TimeSpan.FromSeconds(1), 5);
+
+ var clientBuilder = serviceCollection
+ .AddRestHttpClient>("Reddit")
+ .ConfigureHttpClient((services, client) =>
+ {
+ var assemblyName = Assembly.GetExecutingAssembly().GetName();
+ var name = assemblyName.Name ?? "LuzFaltex.Mara";
+ var version = assemblyName.Version ?? new Version(1, 0, 0);
+
+ client.BaseAddress = new("https://www.reddit.com/");
+ client.DefaultRequestHeaders.UserAgent.Add
+ (
+ new System.Net.Http.Headers.ProductInfoHeaderValue(name, version.ToString())
+ );
+ })
+ .AddTransientHttpErrorPolicy
+ (
+ b => b.WaitAndRetryAsync(retryDelay)
+ )
+ .AddPolicyHandler
+ (
+ Policy
+ .HandleResult(r => r.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
+ .WaitAndRetryAsync
+ (
+ 1,
+ (iteration, response, context) =>
+ {
+ if (response.Result == default)
+ {
+ return TimeSpan.FromSeconds(1);
+ }
+
+ return (TimeSpan)(response.Result.Headers.RetryAfter is null or { Delta: null }
+ ? TimeSpan.FromSeconds(1)
+ : response.Result.Headers.RetryAfter.Delta);
+ },
+ (_, _, _, _) => Task.CompletedTask
+ )
+ );
serviceCollection.AddResponder();
serviceCollection.AddResponder();
@@ -75,6 +122,8 @@ public override void ConfigureServices(IServiceCollection serviceCollection)
options.AddDataObjectConverter();
options.AddDataObjectConverter();
});
- }
+
+ return Result.FromSuccess();
+ }
}
}
diff --git a/Plugins/Mara.Plugins.BetterEmbed/Mara.Plugins.BetterEmbeds.csproj b/Plugins/Mara.Plugins.BetterEmbed/Mara.Plugins.BetterEmbeds.csproj
index e1d2d3f..0c2803e 100644
--- a/Plugins/Mara.Plugins.BetterEmbed/Mara.Plugins.BetterEmbeds.csproj
+++ b/Plugins/Mara.Plugins.BetterEmbed/Mara.Plugins.BetterEmbeds.csproj
@@ -1,7 +1,7 @@
- net5.0
+ net6.0
1.0.0
$(Version)
$(Version)
@@ -9,9 +9,10 @@
-
-
-
+
+
+
+
diff --git a/Plugins/Mara.Plugins.BetterEmbed/MessageHandlers/RedditEmbedBuilder.cs b/Plugins/Mara.Plugins.BetterEmbed/MessageHandlers/RedditEmbedBuilder.cs
index fd2a69e..b5f17d2 100644
--- a/Plugins/Mara.Plugins.BetterEmbed/MessageHandlers/RedditEmbedBuilder.cs
+++ b/Plugins/Mara.Plugins.BetterEmbed/MessageHandlers/RedditEmbedBuilder.cs
@@ -96,7 +96,8 @@ public override async ValueTask> BuildEmbedAsync(Match match, IMe
{
var user = userResult.Entity;
var url = new Uri(user.IconImage);
- userIconUrl = url.GetLeftPart(UriPartial.Path);
+ userIconUrl = url.GetLeftPart(UriPartial.Path)
+ ;
}
/*
@@ -195,35 +196,28 @@ public override async ValueTask> BuildEmbedAsync(Match match, IMe
*/
- var embed = new Embed()
- {
- Title = redditPost.Title,
- Url = string.Format(RedditRestAPI.PostUrl, subreddit, postId).Replace(".json", ""),
- Author = new EmbedAuthor(redditPost.Author,
- userUrl, userIconUrl),
- Footer = new EmbedFooter($"Posted on {redditPost.Subreddit}",
- "https://www.redditstatic.com/desktop2x/img/favicon/android-icon-192x192.png"),
- Timestamp = (DateTimeOffset) redditPost.PostDate,
- Colour = Color.DimGray,
- Type = EmbedType.Rich
- };
-
- List fields = new();
-
- fields.Add(new EmbedField("Score", $"{redditPost.Score} ({redditPost.UpvoteRatio * 100}%)", true));
+ var embedBuilder = new EmbedBuilder()
+ .WithTitle(redditPost.Title)
+ .WithUrl(string.Format(RedditRestAPI.PostUrl, subreddit, postId).Replace(".json", ""))
+ .WithAuthor(redditPost.Author, userUrl, userIconUrl)
+ .WithFooter($"Posted on {redditPost.Subreddit}",
+ "https://www.redditstatic.com/desktop2x/img/favicon/android-icon-192x192.png")
+ .WithTimestamp((DateTimeOffset) redditPost.PostDate);
+
+ embedBuilder.Color = Color.DimGray;
+
+ embedBuilder.AddField("Score", $"{redditPost.Score} ({redditPost.UpvoteRatio * 100}%)", inline: true);
if (redditPost.PostFlair.HasValue)
{
if (redditPost.PostFlair.Value.Contains(":"))
{
- var parts = redditPost.PostFlair.Value.Split(":");
- // embedBuilder.AddField($"{parts[0]}:", parts[1], true);
- fields.Add(new EmbedField($"{parts[0]}:", parts[1], true));
+ var parts = redditPost.PostFlair.Value.Split(":", StringSplitOptions.TrimEntries);
+ embedBuilder.AddField($"{parts[0]}:", parts[1], true);
}
else
{
- // embedBuilder.AddField("Post Flair:", redditPost.PostFlair.Value, true);
- fields.Add(new EmbedField("Post Flair:", redditPost.PostFlair.Value, true));
+ embedBuilder.AddField("Post Flair:", redditPost.PostFlair.Value, true);
}
}
@@ -250,11 +244,13 @@ public override async ValueTask> BuildEmbedAsync(Match match, IMe
case EmbedType.Image:
case EmbedType.GIFV:
{
- embed = embed with {Image = new EmbedImage(redditPost.Url)};
+ embedBuilder.WithImageUrl(redditPost.Url);
break;
}
case EmbedType.Video when redditPost.Media.HasValue:
{
+ // TODO: Post new embed containing video
+ /*
var media = redditPost.Media.Value;
if (media.RedditVideo.HasValue)
{
@@ -265,34 +261,34 @@ public override async ValueTask> BuildEmbedAsync(Match match, IMe
embed = embed with {Image = new EmbedImage(redditVideo.Url)};
}
}
-
+ */
break;
+
}
case EmbedType.Link:
case EmbedType.Article:
{
- embed = embed with
- {
- Thumbnail = new EmbedThumbnail(redditPost.Thumbnail),
- Description = FormatUtilities.Url(redditPost.Url, redditPost.Url)
- };
-
+ embedBuilder
+ .WithThumbnailUrl(redditPost.Thumbnail)
+ .WithDescription(FormatUtilities.Url(redditPost.Url));
+
break;
}
case EmbedType.Rich:
default:
{
- embed = embed with
- {
- Description = redditPost.Text.Value.Truncate(EmbedConstants.MaxDescriptionLength,
- $"…\n{FormatUtilities.Url("Read More", embed.Url.Value)}")
- };
+ embedBuilder.WithDescription(redditPost.Text.Value.Truncate(EmbedConstants.MaxDescriptionLength,
+ $"…\n{FormatUtilities.Url("Read More", embedBuilder.Url)}"));
+
break;
}
}
- embed = embed with {Fields = fields};
- return embed;
+ var verifyResult = embedBuilder.Ensure();
+
+ return verifyResult.IsSuccess
+ ? embedBuilder.Build()
+ : Result.FromError(verifyResult);
}
private async Task IsGuildNsfw(Snowflake guildId, CancellationToken cancellationToken = default)
diff --git a/Plugins/Mara.Plugins.BetterEmbed/Results/JsonError.cs b/Plugins/Mara.Plugins.BetterEmbed/Results/JsonError.cs
new file mode 100644
index 0000000..a037d9e
--- /dev/null
+++ b/Plugins/Mara.Plugins.BetterEmbed/Results/JsonError.cs
@@ -0,0 +1,10 @@
+using Remora.Results;
+
+namespace Mara.Plugins.BetterEmbeds.Results
+{
+ ///
+ /// Represents an error that occurred while parsing JSON.
+ ///
+ /// The error messge.
+ public sealed record JsonError(string Message) : ResultError(Message);
+}
diff --git a/Plugins/Mara.Plugins.BetterEmbeds.Tests/Mara.Plugins.BetterEmbeds.Tests.csproj b/Plugins/Mara.Plugins.BetterEmbeds.Tests/Mara.Plugins.BetterEmbeds.Tests.csproj
new file mode 100644
index 0000000..617180a
--- /dev/null
+++ b/Plugins/Mara.Plugins.BetterEmbeds.Tests/Mara.Plugins.BetterEmbeds.Tests.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net6.0
+
+ false
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
diff --git a/Plugins/Mara.Plugins.BetterEmbeds.Tests/UnitTest1.cs b/Plugins/Mara.Plugins.BetterEmbeds.Tests/UnitTest1.cs
new file mode 100644
index 0000000..f709b51
--- /dev/null
+++ b/Plugins/Mara.Plugins.BetterEmbeds.Tests/UnitTest1.cs
@@ -0,0 +1,14 @@
+using System;
+using Xunit;
+
+namespace Mara.Plugins.BetterEmbeds.Tests
+{
+ public class UnitTest1
+ {
+ [Fact]
+ public void Test1()
+ {
+
+ }
+ }
+}
diff --git a/Plugins/Mara.Plugins.Consent/ConsentPlugin.cs b/Plugins/Mara.Plugins.Consent/ConsentPlugin.cs
new file mode 100644
index 0000000..0449078
--- /dev/null
+++ b/Plugins/Mara.Plugins.Consent/ConsentPlugin.cs
@@ -0,0 +1,33 @@
+using System;
+using System.Reflection;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.DependencyInjection;
+using Remora.Plugins.Abstractions;
+using Remora.Results;
+
+namespace Mara.Plugins.Consent
+{
+ ///
+ /// A plugin which keeps track of user consents on a global basis.
+ ///
+ public sealed class ConsentPlugin : PluginDescriptor, IMigratablePlugin
+ {
+ public override string Name => "Consent";
+
+ ///
+ public override Version Version => Assembly.GetExecutingAssembly().GetName().Version ?? new Version(1, 0, 0);
+
+ public override string Description => "Retrieves and stores user consent on a global basis.";
+
+ public override Result ConfigureServices(IServiceCollection serviceCollection)
+ {
+ return base.ConfigureServices(serviceCollection);
+ }
+
+ public Task MigrateAsync(IServiceProvider serviceProvider, CancellationToken ct = default)
+ {
+ return Task.FromResult(Result.FromSuccess());
+ }
+ }
+}
diff --git a/Plugins/Mara.Plugins.Consent/Mara.Plugins.Consent.csproj b/Plugins/Mara.Plugins.Consent/Mara.Plugins.Consent.csproj
new file mode 100644
index 0000000..4d3baa4
--- /dev/null
+++ b/Plugins/Mara.Plugins.Consent/Mara.Plugins.Consent.csproj
@@ -0,0 +1,12 @@
+
+
+
+ net6.0
+
+
+
+
+
+
+
+
diff --git a/Plugins/Mara.Plugins.Core.Tests/Mara.Plugins.Core.Tests.csproj b/Plugins/Mara.Plugins.Core.Tests/Mara.Plugins.Core.Tests.csproj
new file mode 100644
index 0000000..617180a
--- /dev/null
+++ b/Plugins/Mara.Plugins.Core.Tests/Mara.Plugins.Core.Tests.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net6.0
+
+ false
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
diff --git a/Plugins/Mara.Plugins.Core.Tests/UnitTest1.cs b/Plugins/Mara.Plugins.Core.Tests/UnitTest1.cs
new file mode 100644
index 0000000..14062de
--- /dev/null
+++ b/Plugins/Mara.Plugins.Core.Tests/UnitTest1.cs
@@ -0,0 +1,14 @@
+using System;
+using Xunit;
+
+namespace Mara.Plugins.Core.Tests
+{
+ public class UnitTest1
+ {
+ [Fact]
+ public void Test1()
+ {
+
+ }
+ }
+}
diff --git a/Plugins/Mara.Plugins.Core/Attributes.cs b/Plugins/Mara.Plugins.Core/Attributes.cs
new file mode 100644
index 0000000..4b659e2
--- /dev/null
+++ b/Plugins/Mara.Plugins.Core/Attributes.cs
@@ -0,0 +1,6 @@
+using System.Runtime.CompilerServices;
+using Mara.Plugins.Core;
+using Remora.Plugins.Abstractions.Attributes;
+
+[assembly: RemoraPlugin(typeof(CorePlugin))]
+[assembly: InternalsVisibleTo("Mara.Plugins.Core.Tests")]
diff --git a/Plugins/Mara.Plugins.Core/Commands/AboutCommand.cs b/Plugins/Mara.Plugins.Core/Commands/AboutCommand.cs
index a1d55ed..669212b 100644
--- a/Plugins/Mara.Plugins.Core/Commands/AboutCommand.cs
+++ b/Plugins/Mara.Plugins.Core/Commands/AboutCommand.cs
@@ -1,23 +1,13 @@
-using System;
-using System.Collections.Generic;
-using System.ComponentModel;
+using System.ComponentModel;
using System.Drawing;
-using System.Linq;
-using System.Text;
using System.Threading.Tasks;
using Mara.Common.Discord;
using Mara.Common.Discord.Feedback;
-using Microsoft.Extensions.Caching.Memory;
-using Microsoft.Extensions.Options;
using Remora.Commands.Attributes;
using Remora.Commands.Groups;
-using Remora.Commands.Services;
+using Remora.Discord.API;
using Remora.Discord.API.Abstractions.Rest;
-using Remora.Discord.API.Objects;
using Remora.Discord.Commands.Feedback.Services;
-using Remora.Discord.Commands.Responders;
-using Remora.Discord.Commands.Services;
-using Remora.Discord.Core;
using Remora.Results;
namespace Mara.Plugins.Core.Commands
@@ -25,13 +15,13 @@ namespace Mara.Plugins.Core.Commands
public sealed class AboutCommand : CommandGroup
{
private readonly FeedbackService _feedbackService;
- private readonly IMemoryCache _cache;
+ private readonly IdentityInformationConfiguration _identityInformation;
private readonly IDiscordRestUserAPI _userApi;
- public AboutCommand(FeedbackService feedbackService, IMemoryCache cache, IDiscordRestUserAPI userApi)
+ public AboutCommand(FeedbackService feedbackService, IdentityInformationConfiguration identityInformation, IDiscordRestUserAPI userApi)
{
_feedbackService = feedbackService;
- _cache = cache;
+ _identityInformation = identityInformation;
_userApi = userApi;
}
@@ -39,23 +29,43 @@ public AboutCommand(FeedbackService feedbackService, IMemoryCache cache, IDiscor
[Description("Provides information about the bot.")]
public async Task ShowBotInfoAsync()
{
- var identity = _cache.Get(nameof(IdentityInformationConfiguration));
- var user = await _userApi.GetUserAsync(identity.OwnerId);
-
var embedBuilder = new EmbedBuilder()
.WithTitle("Mara")
.WithUrl("https://mara.luzfaltex.com")
.WithColor(Color.Pink)
.WithDescription("A custom-tailored Discord moderation bot by LuzFaltex.");
- if (user.IsSuccess)
+ if (_identityInformation.Application.Team is { } team)
+ {
+ var avatarUrlResult = CDN.GetTeamIconUrl(team, imageSize: 256);
+
+ if (avatarUrlResult.IsSuccess)
+ {
+ var avatarUrl = avatarUrlResult.Entity;
+ embedBuilder = embedBuilder.WithAuthor(team.Name, iconUrl: avatarUrl!.AbsoluteUri);
+ }
+ else
+ {
+ var teamOwner = await _userApi.GetUserAsync(team.OwnerUserID, CancellationToken);
+ if (teamOwner.IsSuccess)
+ {
+ embedBuilder = embedBuilder.WithUserAsAuthor(teamOwner.Entity);
+ }
+ }
+ }
+ else
{
- embedBuilder = embedBuilder.WithUserAsAuthor(user.Entity);
+ var ownerId = _identityInformation.Application.Owner!.ID.Value;
+ var user = await _userApi.GetUserAsync(ownerId, CancellationToken);
+ if (user.IsSuccess)
+ {
+ embedBuilder = embedBuilder.WithUserAsAuthor(user.Entity);
+ }
}
embedBuilder.AddField("Version", typeof(CorePlugin).Assembly.GetName().Version?.ToString(3) ?? "1.0.0");
- return await _feedbackService.SendContextualEmbedAsync(embedBuilder.Build(), this.CancellationToken);
+ return await _feedbackService.SendContextualEmbedAsync(embedBuilder.Build(), ct: CancellationToken);
}
}
}
diff --git a/Plugins/Mara.Plugins.Core/CorePlugin.cs b/Plugins/Mara.Plugins.Core/CorePlugin.cs
index d9d9315..934e0bb 100644
--- a/Plugins/Mara.Plugins.Core/CorePlugin.cs
+++ b/Plugins/Mara.Plugins.Core/CorePlugin.cs
@@ -1,20 +1,20 @@
using System;
using System.Reflection;
+using System.Threading;
using System.Threading.Tasks;
-using Mara.Plugins.Core;
using Mara.Plugins.Core.Commands;
using Mara.Plugins.Core.Responders;
using Microsoft.Extensions.DependencyInjection;
using Remora.Commands.Extensions;
using Remora.Discord.Gateway.Extensions;
using Remora.Plugins.Abstractions;
-using Remora.Plugins.Abstractions.Attributes;
using Remora.Results;
-[assembly:RemoraPlugin(typeof(CorePlugin))]
-
namespace Mara.Plugins.Core
{
+ ///
+ /// Represents core functionality.
+ ///
public class CorePlugin : PluginDescriptor
{
///
@@ -25,20 +25,22 @@ public class CorePlugin : PluginDescriptor
public override string Description => "Provides core functionality for the bot.";
///
- public override void ConfigureServices(IServiceCollection serviceCollection)
+ public override Result ConfigureServices(IServiceCollection serviceCollection)
{
serviceCollection.AddResponder();
serviceCollection.AddResponder();
serviceCollection.AddResponder();
- serviceCollection.AddResponder();
+ serviceCollection.AddResponder();
serviceCollection.AddCommandGroup();
+
+ return Result.FromSuccess();
}
///
- public override ValueTask InitializeAsync(IServiceProvider serviceProvider)
+ public override ValueTask InitializeAsync(IServiceProvider serviceProvider, CancellationToken ct = default)
{
- return base.InitializeAsync(serviceProvider);
+ return base.InitializeAsync(serviceProvider, ct);
}
}
}
diff --git a/Plugins/Mara.Plugins.Core/Mara.Plugins.Core.csproj b/Plugins/Mara.Plugins.Core/Mara.Plugins.Core.csproj
index c94d703..d309b2a 100644
--- a/Plugins/Mara.Plugins.Core/Mara.Plugins.Core.csproj
+++ b/Plugins/Mara.Plugins.Core/Mara.Plugins.Core.csproj
@@ -1,16 +1,16 @@
- net5.0
+ net6.0
1.0.0
$(Version)
$(Version)
-
-
-
+
+
+
diff --git a/Plugins/Mara.Plugins.Core/Responders/MessageDeleteResponder.cs b/Plugins/Mara.Plugins.Core/Responders/DeleteRequestResponder.cs
similarity index 88%
rename from Plugins/Mara.Plugins.Core/Responders/MessageDeleteResponder.cs
rename to Plugins/Mara.Plugins.Core/Responders/DeleteRequestResponder.cs
index 9cc24a1..8d85e32 100644
--- a/Plugins/Mara.Plugins.Core/Responders/MessageDeleteResponder.cs
+++ b/Plugins/Mara.Plugins.Core/Responders/DeleteRequestResponder.cs
@@ -12,16 +12,16 @@
namespace Mara.Plugins.Core.Responders
{
- public sealed class MessageDeleteResponder : IResponder
+ public sealed class DeleteRequestResponder : IResponder
{
private readonly DiscordRestChannelAPI _channelApi;
- public MessageDeleteResponder(DiscordRestChannelAPI channelApi)
+ public DeleteRequestResponder(DiscordRestChannelAPI channelApi)
{
_channelApi = channelApi;
}
- public async Task RespondAsync(MessageReactionAdd gatewayEvent, CancellationToken cancellationToken = new CancellationToken())
+ public async Task RespondAsync(MessageReactionAdd gatewayEvent, CancellationToken cancellationToken = default)
{
// If the reaction wasn't ❌, skip.
if (!gatewayEvent.Emoji.Name.Equals("❌"))
diff --git a/Plugins/Mara.Plugins.Core/Responders/ReadyResponder.cs b/Plugins/Mara.Plugins.Core/Responders/ReadyResponder.cs
index b51e45d..17327a0 100644
--- a/Plugins/Mara.Plugins.Core/Responders/ReadyResponder.cs
+++ b/Plugins/Mara.Plugins.Core/Responders/ReadyResponder.cs
@@ -1,22 +1,14 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Runtime.InteropServices;
-using System.Text;
-using System.Threading;
+using System.Threading;
using System.Threading.Tasks;
using Mara.Common.Discord.Feedback;
using Mara.Common.Events;
-using Microsoft.Extensions.Caching.Memory;
using Microsoft.Extensions.Logging;
-using Microsoft.Extensions.Options;
using Remora.Discord.API.Abstractions.Gateway.Events;
using Remora.Discord.API.Abstractions.Objects;
using Remora.Discord.API.Abstractions.Rest;
using Remora.Discord.API.Gateway.Commands;
using Remora.Discord.API.Objects;
using Remora.Discord.Commands.Services;
-using Remora.Discord.Core;
using Remora.Discord.Gateway;
using Remora.Results;
@@ -25,7 +17,7 @@ namespace Mara.Plugins.Core.Responders
public sealed class ReadyResponder : LoggingEventResponderBase
{
private readonly IDiscordRestOAuth2API _oauthApi;
- private readonly IMemoryCache _cache;
+ private readonly IdentityInformationConfiguration _identityInfo;
private readonly DiscordGatewayClient _gatewayClient;
private readonly SlashService _slashService;
private readonly ILogger _logger;
@@ -34,24 +26,21 @@ public ReadyResponder
(
ILogger logger,
IDiscordRestOAuth2API oauth,
- IMemoryCache cache,
+ IdentityInformationConfiguration identityInfo,
DiscordGatewayClient gatewayClient,
SlashService slashService
) : base(logger)
{
_logger = logger;
_oauthApi = oauth;
- _cache = cache;
+ _identityInfo = identityInfo;
_gatewayClient = gatewayClient;
_slashService = slashService;
}
public override async Task HandleAsync(IReady gatewayEvent, CancellationToken cancellationToken = default)
{
- var identityInfo = new IdentityInformationConfiguration
- {
- Id = gatewayEvent.User.ID
- };
+ _identityInfo.Id = gatewayEvent.User.ID;
var getApplication = await _oauthApi.GetCurrentBotApplicationInformationAsync(cancellationToken);
if (!getApplication.IsSuccess)
@@ -59,37 +48,14 @@ public override async Task HandleAsync(IReady gatewayEvent, Cancellation
return Result.FromError(getApplication);
}
- var application = getApplication.Entity;
-
- identityInfo.ApplicationId = application.ID;
- identityInfo.OwnerId = application.Owner.ID.Value;
+ _identityInfo.Application = getApplication.Entity;
- // Update memory cache
- _cache.Set(nameof(IdentityInformationConfiguration), identityInfo);
+ var application = getApplication.Entity;
// Set status
var updatePresence = new UpdatePresence(ClientStatus.Online, false, null,
new[] {new Activity("anime", ActivityType.Watching)});
- _gatewayClient.SubmitCommandAsync(updatePresence);
-
- // Load slash commands
- /*
- var checkSlashService = _slashService.SupportsSlashCommands();
-
- if (checkSlashService.IsSuccess)
- {
- var updateSlash = await _slashService.UpdateSlashCommandsAsync(ct: cancellationToken);
- if (!updateSlash.IsSuccess)
- {
- _logger.LogWarning("Failed to update slash commands: {Reason}",
- updateSlash.Error.Message);
- }
- }
- else
- {
- _logger.LogWarning("The registered commands of the bot don't support slash commands: {Reason}", checkSlashService.Error.Message);
- }
- */
+ _gatewayClient.SubmitCommand(updatePresence);
return Result.FromSuccess();
}
diff --git a/Plugins/Mara.Plugins.Core/Responders/SlashCommandRegistrationResponder.cs b/Plugins/Mara.Plugins.Core/Responders/SlashCommandRegistrationResponder.cs
index da8f9a2..b96ab3e 100644
--- a/Plugins/Mara.Plugins.Core/Responders/SlashCommandRegistrationResponder.cs
+++ b/Plugins/Mara.Plugins.Core/Responders/SlashCommandRegistrationResponder.cs
@@ -3,10 +3,12 @@
using Microsoft.Extensions.Logging;
using Remora.Discord.API.Abstractions.Gateway.Events;
using Remora.Discord.Commands.Services;
-using Remora.Discord.Core;
using Remora.Discord.Gateway.Responders;
+using Remora.Rest.Core;
using Remora.Results;
+using DiscordConstants = Remora.Discord.API.Constants;
+
namespace Mara.Plugins.Core.Responders
{
public sealed class SlashCommandRegistrationResponder : IResponder
@@ -24,7 +26,7 @@ public SlashCommandRegistrationResponder(SlashService slashService, ILogger RespondAsync(IGuildCreate gatewayEvent, CancellationToken cancellationToken = new CancellationToken())
{
// For debug only
- var guildId = new Snowflake(861515006067998731);
+ var guildId = new Snowflake(861515006067998731, DiscordConstants.DiscordEpoch);
if (gatewayEvent.ID != guildId)
{
diff --git a/Plugins/Mara.Plugins.MediatR/Mara.Plugins.Mediator.csproj b/Plugins/Mara.Plugins.MediatR/Mara.Plugins.Mediator.csproj
index c693899..b14ae30 100644
--- a/Plugins/Mara.Plugins.MediatR/Mara.Plugins.Mediator.csproj
+++ b/Plugins/Mara.Plugins.MediatR/Mara.Plugins.Mediator.csproj
@@ -2,18 +2,18 @@
Library
- net5.0
+ net6.0
1.0.0
$(Version)
$(Version)
-
+
-
-
+
+
diff --git a/Plugins/Mara.Plugins.Mediator.Tests/Mara.Plugins.Mediator.Tests.csproj b/Plugins/Mara.Plugins.Mediator.Tests/Mara.Plugins.Mediator.Tests.csproj
new file mode 100644
index 0000000..617180a
--- /dev/null
+++ b/Plugins/Mara.Plugins.Mediator.Tests/Mara.Plugins.Mediator.Tests.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net6.0
+
+ false
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
diff --git a/Plugins/Mara.Plugins.Mediator.Tests/UnitTest1.cs b/Plugins/Mara.Plugins.Mediator.Tests/UnitTest1.cs
new file mode 100644
index 0000000..f9dc234
--- /dev/null
+++ b/Plugins/Mara.Plugins.Mediator.Tests/UnitTest1.cs
@@ -0,0 +1,14 @@
+using System;
+using Xunit;
+
+namespace Mara.Plugins.Mediator.Tests
+{
+ public class UnitTest1
+ {
+ [Fact]
+ public void Test1()
+ {
+
+ }
+ }
+}
diff --git a/Plugins/Mara.Plugins.Moderation.Tests/Mara.Plugins.Moderation.Tests.csproj b/Plugins/Mara.Plugins.Moderation.Tests/Mara.Plugins.Moderation.Tests.csproj
new file mode 100644
index 0000000..617180a
--- /dev/null
+++ b/Plugins/Mara.Plugins.Moderation.Tests/Mara.Plugins.Moderation.Tests.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net6.0
+
+ false
+
+
+
+
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+ runtime; build; native; contentfiles; analyzers; buildtransitive
+ all
+
+
+
+
diff --git a/Plugins/Mara.Plugins.Moderation.Tests/UnitTest1.cs b/Plugins/Mara.Plugins.Moderation.Tests/UnitTest1.cs
new file mode 100644
index 0000000..7c8e98a
--- /dev/null
+++ b/Plugins/Mara.Plugins.Moderation.Tests/UnitTest1.cs
@@ -0,0 +1,14 @@
+using System;
+using Xunit;
+
+namespace Mara.Plugins.Moderation.Tests
+{
+ public class UnitTest1
+ {
+ [Fact]
+ public void Test1()
+ {
+
+ }
+ }
+}
diff --git a/Plugins/Mara.Plugins.Moderation/Commands/InfoCommand.cs b/Plugins/Mara.Plugins.Moderation/Commands/InfoCommand.cs
new file mode 100644
index 0000000..d79315f
--- /dev/null
+++ b/Plugins/Mara.Plugins.Moderation/Commands/InfoCommand.cs
@@ -0,0 +1,146 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Linq;
+using System.Reflection.Metadata.Ecma335;
+using System.Text;
+using System.Threading.Tasks;
+using Mara.Common.Discord;
+using Mara.Plugins.Moderation.Services;
+using Remora.Commands.Attributes;
+using Remora.Commands.Groups;
+using Remora.Discord.API;
+using Remora.Discord.API.Abstractions.Objects;
+using Remora.Discord.API.Abstractions.Rest;
+using Remora.Discord.API.Objects;
+using Remora.Discord.Commands.Attributes;
+using Remora.Discord.Commands.Contexts;
+using Remora.Discord.Commands.Feedback.Services;
+using Remora.Discord.Core;
+using Remora.Results;
+
+namespace Mara.Plugins.Moderation.Commands
+{
+ public sealed class InfoCommand : CommandGroup
+ {
+ private readonly FeedbackService _feedbackService;
+ private readonly IDiscordRestUserAPI _userApi;
+ private readonly IDiscordRestGuildAPI _guildApi;
+ private readonly UserService _userService;
+ private readonly ICommandContext _context;
+
+ public InfoCommand
+ (
+ FeedbackService feedbackService,
+ IDiscordRestUserAPI userApi,
+ IDiscordRestGuildAPI guildApi,
+ UserService userService,
+ ICommandContext context
+ )
+ {
+ _feedbackService = feedbackService;
+ _userApi = userApi;
+ _guildApi = guildApi;
+ _userService = userService;
+ _context = context;
+ }
+
+ [Command("info")]
+ [Description("Returns information about a user.")]
+ [CommandType(ApplicationCommandType.ChatInput)]
+ public async Task ShowUserInfoChatAsync(IUser user, Snowflake? guildId = null)
+ {
+ var buildEmbedResult = await BuildUserInfoEmbed(user, guildId);
+
+ if (!buildEmbedResult.IsSuccess)
+ {
+ return buildEmbedResult;
+ }
+
+ return await _feedbackService.SendContextualEmbedAsync(buildEmbedResult.Entity, ct: base.CancellationToken);
+ }
+
+ [Command("User Info")]
+ [Description("Returns information about a user.")]
+ [CommandType(ApplicationCommandType.User)]
+ public async Task ShowUserInfoMenuAsync()
+ {
+ var user = _context.User;
+
+ var buildEmbedResult = _context.GuildID.HasValue
+ ? await BuildUserInfoEmbed(user, _context.GuildID.Value)
+ : await BuildUserInfoEmbed(user, null);
+
+ if (!buildEmbedResult.IsSuccess)
+ {
+ return buildEmbedResult;
+ }
+
+ return await _feedbackService.SendContextualEmbedAsync(buildEmbedResult.Entity, ct: base.CancellationToken);
+ }
+
+ private async Task> BuildUserInfoEmbed(IUser user, Snowflake? guildId)
+ {
+ var userInfo = await _userService.GetUserInformation(user);
+
+ if (!userInfo.IsSuccess)
+ {
+ return Result