Added Ellie.Bot.Common
This commit is contained in:
parent
eb91d0f8de
commit
feb074a392
171 changed files with 7704 additions and 0 deletions
78
src/Ellie.Bot.Common/Abstractions/creds/IBotCredentials.cs
Normal file
78
src/Ellie.Bot.Common/Abstractions/creds/IBotCredentials.cs
Normal file
|
@ -0,0 +1,78 @@
|
|||
#nullable disable
|
||||
namespace Ellie;
|
||||
|
||||
public interface IBotCredentials
|
||||
{
|
||||
string Token { get; }
|
||||
string GoogleApiKey { get; }
|
||||
ICollection<ulong> OwnerIds { get; set; }
|
||||
bool UsePrivilegedIntents { get; }
|
||||
string RapidApiKey { get; }
|
||||
|
||||
Creds.DbOptions Db { get; }
|
||||
string OsuApiKey { get; }
|
||||
int TotalShards { get; }
|
||||
Creds.PatreonSettings Patreon { get; }
|
||||
string CleverbotApiKey { get; }
|
||||
string Gpt3ApiKey { get; }
|
||||
RestartConfig RestartCommand { get; }
|
||||
Creds.VotesSettings Votes { get; }
|
||||
string BotListToken { get; }
|
||||
string RedisOptions { get; }
|
||||
string LocationIqApiKey { get; }
|
||||
string TimezoneDbApiKey { get; }
|
||||
string CoinmarketcapApiKey { get; }
|
||||
string TrovoClientId { get; }
|
||||
string CoordinatorUrl { get; set; }
|
||||
string TwitchClientId { get; set; }
|
||||
string TwitchClientSecret { get; set; }
|
||||
GoogleApiConfig Google { get; set; }
|
||||
BotCacheImplemenation BotCache { get; set; }
|
||||
}
|
||||
|
||||
public interface IVotesSettings
|
||||
{
|
||||
string TopggServiceUrl { get; set; }
|
||||
string TopggKey { get; set; }
|
||||
string DiscordsServiceUrl { get; set; }
|
||||
string DiscordsKey { get; set; }
|
||||
}
|
||||
|
||||
public interface IPatreonSettings
|
||||
{
|
||||
public string ClientId { get; set; }
|
||||
public string AccessToken { get; set; }
|
||||
public string RefreshToken { get; set; }
|
||||
public string ClientSecret { get; set; }
|
||||
public string CampaignId { get; set; }
|
||||
}
|
||||
|
||||
public interface IRestartConfig
|
||||
{
|
||||
string Cmd { get; set; }
|
||||
string Args { get; set; }
|
||||
}
|
||||
|
||||
public class RestartConfig : IRestartConfig
|
||||
{
|
||||
public string Cmd { get; set; }
|
||||
public string Args { get; set; }
|
||||
}
|
||||
|
||||
public enum BotCacheImplemenation
|
||||
{
|
||||
Memory,
|
||||
Redis
|
||||
}
|
||||
|
||||
public interface IDbOptions
|
||||
{
|
||||
string Type { get; set; }
|
||||
string ConnectionString { get; set; }
|
||||
}
|
||||
|
||||
public interface IGoogleApiConfig
|
||||
{
|
||||
string SearchId { get; init; }
|
||||
string ImageSearchId { get; init; }
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
namespace Ellie;
|
||||
|
||||
public interface IBotCredsProvider
|
||||
{
|
||||
public void Reload();
|
||||
public IBotCredentials GetCreds();
|
||||
public void ModifyCredsFile(Action<IBotCredentials> func);
|
||||
}
|
13
src/Ellie.Bot.Common/Abstractions/strings/CommandStrings.cs
Normal file
13
src/Ellie.Bot.Common/Abstractions/strings/CommandStrings.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
#nullable disable
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace Ellie.Services;
|
||||
|
||||
public sealed class CommandStrings
|
||||
{
|
||||
[YamlMember(Alias = "desc")]
|
||||
public string Desc { get; set; }
|
||||
|
||||
[YamlMember(Alias = "args")]
|
||||
public string[] Args { get; set; }
|
||||
}
|
16
src/Ellie.Bot.Common/Abstractions/strings/IBotStrings.cs
Normal file
16
src/Ellie.Bot.Common/Abstractions/strings/IBotStrings.cs
Normal file
|
@ -0,0 +1,16 @@
|
|||
#nullable disable
|
||||
using System.Globalization;
|
||||
|
||||
namespace Ellie.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Defines methods to retrieve and reload bot strings
|
||||
/// </summary>
|
||||
public interface IBotStrings
|
||||
{
|
||||
string GetText(string key, ulong? guildId = null, params object[] data);
|
||||
string GetText(string key, CultureInfo locale, params object[] data);
|
||||
void Reload();
|
||||
CommandStrings GetCommandStrings(string commandName, ulong? guildId = null);
|
||||
CommandStrings GetCommandStrings(string commandName, CultureInfo cultureInfo);
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
#nullable disable
|
||||
using System.Globalization;
|
||||
|
||||
namespace Ellie.Common;
|
||||
|
||||
public static class BotStringsExtensions
|
||||
{
|
||||
// this one is for pipe fun, see PipeExtensions.cs
|
||||
public static string GetText(this IBotStrings strings, in LocStr str, in ulong guildId)
|
||||
=> strings.GetText(str.Key, guildId, str.Params);
|
||||
|
||||
public static string GetText(this IBotStrings strings, in LocStr str, ulong? guildId = null)
|
||||
=> strings.GetText(str.Key, guildId, str.Params);
|
||||
|
||||
public static string GetText(this IBotStrings strings, in LocStr str, CultureInfo culture)
|
||||
=> strings.GetText(str.Key, culture, str.Params);
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
#nullable disable
|
||||
namespace Ellie.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Implemented by classes which provide localized strings in their own ways
|
||||
/// </summary>
|
||||
public interface IBotStringsProvider
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets localized string
|
||||
/// </summary>
|
||||
/// <param name="localeName">Language name</param>
|
||||
/// <param name="key">String key</param>
|
||||
/// <returns>Localized string</returns>
|
||||
string GetText(string localeName, string key);
|
||||
|
||||
/// <summary>
|
||||
/// Reloads string cache
|
||||
/// </summary>
|
||||
void Reload();
|
||||
|
||||
/// <summary>
|
||||
/// Gets command arg examples and description
|
||||
/// </summary>
|
||||
/// <param name="localeName">Language name</param>
|
||||
/// <param name="commandName">Command name</param>
|
||||
CommandStrings GetCommandStrings(string localeName, string commandName);
|
||||
}
|
17
src/Ellie.Bot.Common/Abstractions/strings/IStringSource.cs
Normal file
17
src/Ellie.Bot.Common/Abstractions/strings/IStringSource.cs
Normal file
|
@ -0,0 +1,17 @@
|
|||
#nullable disable
|
||||
|
||||
namespace Ellie.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Basic interface used for classes implementing strings loading mechanism
|
||||
/// </summary>
|
||||
public interface IStringsSource
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets all response strings
|
||||
/// </summary>
|
||||
/// <returns>Dictionary(localename, Dictionary(key, response))</returns>
|
||||
Dictionary<string, Dictionary<string, string>> GetResponseStrings();
|
||||
|
||||
Dictionary<string, Dictionary<string, CommandStrings>> GetCommandStrings();
|
||||
}
|
13
src/Ellie.Bot.Common/Abstractions/strings/LocStr.cs
Normal file
13
src/Ellie.Bot.Common/Abstractions/strings/LocStr.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
namespace Ellie;
|
||||
|
||||
public readonly struct LocStr
|
||||
{
|
||||
public readonly string Key;
|
||||
public readonly object[] Params;
|
||||
|
||||
public LocStr(string key, params object[] data)
|
||||
{
|
||||
Key = key;
|
||||
Params = data;
|
||||
}
|
||||
}
|
12
src/Ellie.Bot.Common/Attributes/AliasesAttribute.cs
Normal file
12
src/Ellie.Bot.Common/Attributes/AliasesAttribute.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Ellie.Common.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class AliasesAttribute : AliasAttribute
|
||||
{
|
||||
public AliasesAttribute([CallerMemberName] string memberName = "")
|
||||
: base(CommandNameLoadHelper.GetAliasesFor(memberName))
|
||||
{
|
||||
}
|
||||
}
|
18
src/Ellie.Bot.Common/Attributes/CmdAttribute.cs
Normal file
18
src/Ellie.Bot.Common/Attributes/CmdAttribute.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Ellie.Common.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class CmdAttribute : CommandAttribute
|
||||
{
|
||||
public string MethodName { get; }
|
||||
|
||||
public CmdAttribute([CallerMemberName] string memberName = "")
|
||||
: base(CommandNameLoadHelper.GetCommandNameFor(memberName))
|
||||
{
|
||||
MethodName = memberName.ToLowerInvariant();
|
||||
Aliases = CommandNameLoadHelper.GetAliasesFor(memberName);
|
||||
Remarks = memberName.ToLowerInvariant();
|
||||
Summary = memberName.ToLowerInvariant();
|
||||
}
|
||||
}
|
11
src/Ellie.Bot.Common/Attributes/DIIgnoreAttribute.cs
Normal file
11
src/Ellie.Bot.Common/Attributes/DIIgnoreAttribute.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
#nullable disable
|
||||
namespace Ellie.Common;
|
||||
|
||||
/// <summary>
|
||||
/// Classed marked with this attribute will not be added to the service provider
|
||||
/// </summary>
|
||||
[AttributeUsage(AttributeTargets.Class)]
|
||||
public class DIIgnoreAttribute : Attribute
|
||||
{
|
||||
|
||||
}
|
7
src/Ellie.Bot.Common/Attributes/EllieOptionsAttribute.cs
Normal file
7
src/Ellie.Bot.Common/Attributes/EllieOptionsAttribute.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
namespace Ellie.Common.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class NadekoOptionsAttribute<TOption> : Attribute
|
||||
where TOption: IEllieCommandOptions
|
||||
{
|
||||
}
|
21
src/Ellie.Bot.Common/Attributes/NoPublicBotAttribute.cs
Normal file
21
src/Ellie.Bot.Common/Attributes/NoPublicBotAttribute.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
#nullable disable
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Ellie.Common;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
|
||||
[SuppressMessage("Style", "IDE0022:Use expression body for methods")]
|
||||
public sealed class NoPublicBotAttribute : PreconditionAttribute
|
||||
{
|
||||
public override Task<PreconditionResult> CheckPermissionsAsync(
|
||||
ICommandContext context,
|
||||
CommandInfo command,
|
||||
IServiceProvider services)
|
||||
{
|
||||
#if GLOBAL_ELLIE
|
||||
return Task.FromResult(PreconditionResult.FromError("Not available on the public bot. To learn how to selfhost a private bot, click [here](https://docs.elliebot.net)."));
|
||||
#else
|
||||
return Task.FromResult(PreconditionResult.FromSuccess());
|
||||
#endif
|
||||
}
|
||||
}
|
21
src/Ellie.Bot.Common/Attributes/OnlyPublicBotAttribute.cs
Normal file
21
src/Ellie.Bot.Common/Attributes/OnlyPublicBotAttribute.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
#nullable disable
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace Ellie.Common;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
|
||||
[SuppressMessage("Style", "IDE0022:Use expression body for methods")]
|
||||
public sealed class OnlyPublicBotAttribute : PreconditionAttribute
|
||||
{
|
||||
public override Task<PreconditionResult> CheckPermissionsAsync(
|
||||
ICommandContext context,
|
||||
CommandInfo command,
|
||||
IServiceProvider services)
|
||||
{
|
||||
#if GLOBAL_NADEKO || DEBUG
|
||||
return Task.FromResult(PreconditionResult.FromSuccess());
|
||||
#else
|
||||
return Task.FromResult(PreconditionResult.FromError("Only available on the public bot."));
|
||||
#endif
|
||||
}
|
||||
}
|
19
src/Ellie.Bot.Common/Attributes/OwnerOnlyAttribute.cs
Normal file
19
src/Ellie.Bot.Common/Attributes/OwnerOnlyAttribute.cs
Normal file
|
@ -0,0 +1,19 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Ellie.Common.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
|
||||
public sealed class OwnerOnlyAttribute : PreconditionAttribute
|
||||
{
|
||||
public override Task<PreconditionResult> CheckPermissionsAsync(
|
||||
ICommandContext context,
|
||||
CommandInfo command,
|
||||
IServiceProvider services)
|
||||
{
|
||||
var creds = services.GetRequiredService<IBotCredsProvider>().GetCreds();
|
||||
|
||||
return Task.FromResult(creds.IsOwner(context.User) || context.Client.CurrentUser.Id == context.User.Id
|
||||
? PreconditionResult.FromSuccess()
|
||||
: PreconditionResult.FromError("Not owner"));
|
||||
}
|
||||
}
|
38
src/Ellie.Bot.Common/Attributes/RatelimitAttribute.cs
Normal file
38
src/Ellie.Bot.Common/Attributes/RatelimitAttribute.cs
Normal file
|
@ -0,0 +1,38 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Ellie.Common.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class RatelimitAttribute : PreconditionAttribute
|
||||
{
|
||||
public int Seconds { get; }
|
||||
|
||||
public RatelimitAttribute(int seconds)
|
||||
{
|
||||
if (seconds <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(seconds));
|
||||
|
||||
Seconds = seconds;
|
||||
}
|
||||
|
||||
public override async Task<PreconditionResult> CheckPermissionsAsync(
|
||||
ICommandContext context,
|
||||
CommandInfo command,
|
||||
IServiceProvider services)
|
||||
{
|
||||
if (Seconds == 0)
|
||||
return PreconditionResult.FromSuccess();
|
||||
|
||||
var cache = services.GetRequiredService<IBotCache>();
|
||||
var rem = await cache.GetRatelimitAsync(
|
||||
new($"precondition:{context.User.Id}:{command.Name}"),
|
||||
Seconds.Seconds());
|
||||
|
||||
if (rem is null)
|
||||
return PreconditionResult.FromSuccess();
|
||||
|
||||
var msgContent = $"You can use this command again in {rem.Value.TotalSeconds:F1}s.";
|
||||
|
||||
return PreconditionResult.FromError(msgContent);
|
||||
}
|
||||
}
|
29
src/Ellie.Bot.Common/Attributes/UserPermAttribute.cs
Normal file
29
src/Ellie.Bot.Common/Attributes/UserPermAttribute.cs
Normal file
|
@ -0,0 +1,29 @@
|
|||
using Microsoft.Extensions.DependencyInjection;
|
||||
|
||||
namespace Discord;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true)]
|
||||
public class UserPermAttribute : RequireUserPermissionAttribute
|
||||
{
|
||||
public UserPermAttribute(GuildPerm permission)
|
||||
: base(permission)
|
||||
{
|
||||
}
|
||||
|
||||
public UserPermAttribute(ChannelPerm permission)
|
||||
: base(permission)
|
||||
{
|
||||
}
|
||||
|
||||
public override Task<PreconditionResult> CheckPermissionsAsync(
|
||||
ICommandContext context,
|
||||
CommandInfo command,
|
||||
IServiceProvider services)
|
||||
{
|
||||
var permService = services.GetRequiredService<IDiscordPermOverrideService>();
|
||||
if (permService.TryGetOverrides(context.Guild?.Id ?? 0, command.Name.ToUpperInvariant(), out _))
|
||||
return Task.FromResult(PreconditionResult.FromSuccess());
|
||||
|
||||
return base.CheckPermissionsAsync(context, command, services);
|
||||
}
|
||||
}
|
30
src/Ellie.Bot.Common/BotCommandTypeReader.cs
Normal file
30
src/Ellie.Bot.Common/BotCommandTypeReader.cs
Normal file
|
@ -0,0 +1,30 @@
|
|||
#nullable disable
|
||||
namespace Ellie.Common.TypeReaders;
|
||||
|
||||
public sealed class CommandTypeReader : EllieTypeReader<CommandInfo>
|
||||
{
|
||||
private readonly CommandService _cmds;
|
||||
private readonly ICommandHandler _handler;
|
||||
|
||||
public CommandTypeReader(ICommandHandler handler, CommandService cmds)
|
||||
{
|
||||
_handler = handler;
|
||||
_cmds = cmds;
|
||||
}
|
||||
|
||||
public override ValueTask<TypeReaderResult<CommandInfo>> ReadAsync(ICommandContext ctx, string input)
|
||||
{
|
||||
input = input.ToUpperInvariant();
|
||||
var prefix = _handler.GetPrefix(ctx.Guild);
|
||||
if (!input.StartsWith(prefix.ToUpperInvariant(), StringComparison.InvariantCulture))
|
||||
return new(TypeReaderResult.FromError<CommandInfo>(CommandError.ParseFailed, "No such command found."));
|
||||
|
||||
input = input[prefix.Length..];
|
||||
|
||||
var cmd = _cmds.Commands.FirstOrDefault(c => c.Aliases.Select(a => a.ToUpperInvariant()).Contains(input));
|
||||
if (cmd is null)
|
||||
return new(TypeReaderResult.FromError<CommandInfo>(CommandError.ParseFailed, "No such command found."));
|
||||
|
||||
return new(TypeReaderResult.FromSuccess(cmd));
|
||||
}
|
||||
}
|
10
src/Ellie.Bot.Common/CleaverBotResponseStr.cs
Normal file
10
src/Ellie.Bot.Common/CleaverBotResponseStr.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
#nullable disable
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace Ellie.Modules.Permissions;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Size = 1)]
|
||||
public readonly struct CleverBotResponseStr
|
||||
{
|
||||
public const string CLEVERBOT_RESPONSE = "cleverbot:response";
|
||||
}
|
31
src/Ellie.Bot.Common/CommandNameLoadHelper.cs
Normal file
31
src/Ellie.Bot.Common/CommandNameLoadHelper.cs
Normal file
|
@ -0,0 +1,31 @@
|
|||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace Ellie.Common.Attributes;
|
||||
|
||||
public static class CommandNameLoadHelper
|
||||
{
|
||||
private static readonly IDeserializer _deserializer = new Deserializer();
|
||||
|
||||
private static readonly Lazy<Dictionary<string, string[]>> _lazyCommandAliases
|
||||
= new(() => LoadAliases());
|
||||
|
||||
public static Dictionary<string, string[]> LoadAliases(string aliasesFilePath = "data/aliases.yml")
|
||||
{
|
||||
var text = File.ReadAllText(aliasesFilePath);
|
||||
return _deserializer.Deserialize<Dictionary<string, string[]>>(text);
|
||||
}
|
||||
|
||||
public static string[] GetAliasesFor(string methodName)
|
||||
=> _lazyCommandAliases.Value.TryGetValue(methodName.ToLowerInvariant(), out var aliases) && aliases.Length > 1
|
||||
? aliases.Skip(1).ToArray()
|
||||
: Array.Empty<string>();
|
||||
|
||||
public static string GetCommandNameFor(string methodName)
|
||||
{
|
||||
methodName = methodName.ToLowerInvariant();
|
||||
var toReturn = _lazyCommandAliases.Value.TryGetValue(methodName, out var aliases) && aliases.Length > 0
|
||||
? aliases[0]
|
||||
: methodName;
|
||||
return toReturn;
|
||||
}
|
||||
}
|
10
src/Ellie.Bot.Common/Common/AddRemove.cs
Normal file
10
src/Ellie.Bot.Common/Common/AddRemove.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
#nullable disable
|
||||
namespace Ellie.Common;
|
||||
|
||||
public enum AddRemove
|
||||
{
|
||||
Add = int.MinValue,
|
||||
Remove = int.MinValue + 1,
|
||||
Rem = int.MinValue + 1,
|
||||
Rm = int.MinValue + 1
|
||||
}
|
17
src/Ellie.Bot.Common/Common/CmdStrings.cs
Normal file
17
src/Ellie.Bot.Common/Common/CmdStrings.cs
Normal file
|
@ -0,0 +1,17 @@
|
|||
#nullable disable
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Ellie.Common;
|
||||
|
||||
public class CmdStrings
|
||||
{
|
||||
public string[] Usages { get; }
|
||||
public string Description { get; }
|
||||
|
||||
[JsonConstructor]
|
||||
public CmdStrings([JsonProperty("args")] string[] usages, [JsonProperty("desc")] string description)
|
||||
{
|
||||
Usages = usages;
|
||||
Description = description;
|
||||
}
|
||||
}
|
9
src/Ellie.Bot.Common/Common/CommandData.cs
Normal file
9
src/Ellie.Bot.Common/Common/CommandData.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
#nullable disable
|
||||
namespace Ellie.Common;
|
||||
|
||||
public class CommandData
|
||||
{
|
||||
public string Cmd { get; set; }
|
||||
public string Desc { get; set; }
|
||||
public string[] Usage { get; set; }
|
||||
}
|
38
src/Ellie.Bot.Common/Common/DownloadTracker.cs
Normal file
38
src/Ellie.Bot.Common/Common/DownloadTracker.cs
Normal file
|
@ -0,0 +1,38 @@
|
|||
#nullable disable
|
||||
namespace Ellie.Common;
|
||||
|
||||
public class DownloadTracker : IEService
|
||||
{
|
||||
private ConcurrentDictionary<ulong, DateTime> LastDownloads { get; } = new();
|
||||
private readonly SemaphoreSlim _downloadUsersSemaphore = new(1, 1);
|
||||
|
||||
/// <summary>
|
||||
/// Ensures all users on the specified guild were downloaded within the last hour.
|
||||
/// </summary>
|
||||
/// <param name="guild">Guild to check and potentially download users from</param>
|
||||
/// <returns>Task representing download state</returns>
|
||||
public async Task EnsureUsersDownloadedAsync(IGuild guild)
|
||||
{
|
||||
#if GLOBAL_NADEKO
|
||||
return;
|
||||
#endif
|
||||
await _downloadUsersSemaphore.WaitAsync();
|
||||
try
|
||||
{
|
||||
var now = DateTime.UtcNow;
|
||||
|
||||
// download once per hour at most
|
||||
var added = LastDownloads.AddOrUpdate(guild.Id,
|
||||
now,
|
||||
(_, old) => now - old > TimeSpan.FromHours(1) ? now : old);
|
||||
|
||||
// means that this entry was just added - download the users
|
||||
if (added == now)
|
||||
await guild.DownloadUsersAsync();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_downloadUsersSemaphore.Release();
|
||||
}
|
||||
}
|
||||
}
|
13
src/Ellie.Bot.Common/Common/Helpers.cs
Normal file
13
src/Ellie.Bot.Common/Common/Helpers.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
#nullable disable
|
||||
namespace Ellie.Common;
|
||||
|
||||
public static class Helpers
|
||||
{
|
||||
public static void ReadErrorAndExit(int exitCode)
|
||||
{
|
||||
if (!Console.IsInputRedirected)
|
||||
Console.ReadKey();
|
||||
|
||||
Environment.Exit(exitCode);
|
||||
}
|
||||
}
|
51
src/Ellie.Bot.Common/Common/ImageUrls.cs
Normal file
51
src/Ellie.Bot.Common/Common/ImageUrls.cs
Normal file
|
@ -0,0 +1,51 @@
|
|||
#nullable disable
|
||||
using Ellie.Common.Yml;
|
||||
using Cloneable;
|
||||
|
||||
namespace Ellie.Common;
|
||||
|
||||
[Cloneable]
|
||||
public partial class ImageUrls : ICloneable<ImageUrls>
|
||||
{
|
||||
[Comment("DO NOT CHANGE")]
|
||||
public int Version { get; set; } = 3;
|
||||
|
||||
public CoinData Coins { get; set; }
|
||||
public Uri[] Currency { get; set; }
|
||||
public Uri[] Dice { get; set; }
|
||||
public RategirlData Rategirl { get; set; }
|
||||
public XpData Xp { get; set; }
|
||||
|
||||
//new
|
||||
public RipData Rip { get; set; }
|
||||
public SlotData Slots { get; set; }
|
||||
|
||||
public class RipData
|
||||
{
|
||||
public Uri Bg { get; set; }
|
||||
public Uri Overlay { get; set; }
|
||||
}
|
||||
|
||||
public class SlotData
|
||||
{
|
||||
public Uri[] Emojis { get; set; }
|
||||
public Uri Bg { get; set; }
|
||||
}
|
||||
|
||||
public class CoinData
|
||||
{
|
||||
public Uri[] Heads { get; set; }
|
||||
public Uri[] Tails { get; set; }
|
||||
}
|
||||
|
||||
public class RategirlData
|
||||
{
|
||||
public Uri Matrix { get; set; }
|
||||
public Uri Dot { get; set; }
|
||||
}
|
||||
|
||||
public class XpData
|
||||
{
|
||||
public Uri Bg { get; set; }
|
||||
}
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
using System.Globalization;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ellie.Common.JsonConverters;
|
||||
|
||||
public class CultureInfoConverter : JsonConverter<CultureInfo>
|
||||
{
|
||||
public override CultureInfo Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
=> new(reader.GetString() ?? "en-US");
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, CultureInfo value, JsonSerializerOptions options)
|
||||
=> writer.WriteStringValue(value.Name);
|
||||
}
|
|
@ -0,0 +1,14 @@
|
|||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ellie.Common.JsonConverters;
|
||||
|
||||
public class Rgba32Converter : JsonConverter<Rgba32>
|
||||
{
|
||||
public override Rgba32 Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
|
||||
=> Rgba32.ParseHex(reader.GetString());
|
||||
|
||||
public override void Write(Utf8JsonWriter writer, Rgba32 value, JsonSerializerOptions options)
|
||||
=> writer.WriteStringValue(value.ToHex());
|
||||
}
|
14
src/Ellie.Bot.Common/Common/LbOpts.cs
Normal file
14
src/Ellie.Bot.Common/Common/LbOpts.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
#nullable disable
|
||||
using CommandLine;
|
||||
|
||||
namespace Ellie.Common;
|
||||
|
||||
public class LbOpts : IEllieCommandOptions
|
||||
{
|
||||
[Option('c', "clean", Default = false, HelpText = "Only show users who are on the server.")]
|
||||
public bool Clean { get; set; }
|
||||
|
||||
public void NormalizeOptions()
|
||||
{
|
||||
}
|
||||
}
|
16
src/Ellie.Bot.Common/Common/Linq2DbExpressions.cs
Normal file
16
src/Ellie.Bot.Common/Common/Linq2DbExpressions.cs
Normal file
|
@ -0,0 +1,16 @@
|
|||
#nullable disable
|
||||
using LinqToDB;
|
||||
using System.Linq.Expressions;
|
||||
|
||||
namespace Ellie.Common;
|
||||
|
||||
public static class Linq2DbExpressions
|
||||
{
|
||||
[ExpressionMethod(nameof(GuildOnShardExpression))]
|
||||
public static bool GuildOnShard(ulong guildId, int totalShards, int shardId)
|
||||
=> throw new NotSupportedException();
|
||||
|
||||
private static Expression<Func<ulong, int, int, bool>> GuildOnShardExpression()
|
||||
=> (guildId, totalShards, shardId)
|
||||
=> guildId / 4194304 % (ulong)totalShards == (ulong)shardId;
|
||||
}
|
52
src/Ellie.Bot.Common/Common/LoginErrorHandler.cs
Normal file
52
src/Ellie.Bot.Common/Common/LoginErrorHandler.cs
Normal file
|
@ -0,0 +1,52 @@
|
|||
#nullable disable
|
||||
using System.Net;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace Ellie.Common;
|
||||
|
||||
public class LoginErrorHandler
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Handle(Exception ex)
|
||||
=> Log.Fatal(ex, "A fatal error has occurred while attempting to connect to Discord");
|
||||
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static void Handle(HttpException ex)
|
||||
{
|
||||
switch (ex.HttpCode)
|
||||
{
|
||||
case HttpStatusCode.Unauthorized:
|
||||
Log.Error("Your bot token is wrong.\n"
|
||||
+ "You can find the bot token under the Bot tab in the developer page.\n"
|
||||
+ "Fix your token in the credentials file and restart the bot");
|
||||
break;
|
||||
|
||||
case HttpStatusCode.BadRequest:
|
||||
Log.Error("Something has been incorrectly formatted in your credentials file.\n"
|
||||
+ "Use the JSON Guide as reference to fix it and restart the bot");
|
||||
Log.Error("If you are on Linux, make sure Redis is installed and running");
|
||||
break;
|
||||
|
||||
case HttpStatusCode.RequestTimeout:
|
||||
Log.Error("The request timed out. Make sure you have no external program blocking the bot "
|
||||
+ "from connecting to the internet");
|
||||
break;
|
||||
|
||||
case HttpStatusCode.ServiceUnavailable:
|
||||
case HttpStatusCode.InternalServerError:
|
||||
Log.Error("Discord is having internal issues. Please, try again later");
|
||||
break;
|
||||
|
||||
case HttpStatusCode.TooManyRequests:
|
||||
Log.Error("Your bot has been ratelimited by Discord. Please, try again later.\n"
|
||||
+ "Global ratelimits usually last for an hour");
|
||||
break;
|
||||
|
||||
default:
|
||||
Log.Warning("An error occurred while attempting to connect to Discord");
|
||||
break;
|
||||
}
|
||||
|
||||
Log.Fatal(ex, "Fatal error occurred while loading credentials");
|
||||
}
|
||||
}
|
46
src/Ellie.Bot.Common/Common/OldCreds.cs
Normal file
46
src/Ellie.Bot.Common/Common/OldCreds.cs
Normal file
|
@ -0,0 +1,46 @@
|
|||
#nullable disable
|
||||
namespace Ellie.Common;
|
||||
|
||||
public class OldCreds
|
||||
{
|
||||
public string Token { get; set; } = string.Empty;
|
||||
public ulong[] OwnerIds { get; set; } = new ulong[1];
|
||||
public string LoLApiKey { get; set; } = string.Empty;
|
||||
public string GoogleApiKey { get; set; } = string.Empty;
|
||||
public string MashapeKey { get; set; } = string.Empty;
|
||||
public string OsuApiKey { get; set; } = string.Empty;
|
||||
public string SoundCloudClientId { get; set; } = string.Empty;
|
||||
public string CleverbotApiKey { get; set; } = string.Empty;
|
||||
public string CarbonKey { get; set; } = string.Empty;
|
||||
public int TotalShards { get; set; } = 1;
|
||||
public string PatreonAccessToken { get; set; } = string.Empty;
|
||||
public string PatreonCampaignId { get; set; } = "334038";
|
||||
public RestartConfig RestartCommand { get; set; }
|
||||
|
||||
public string ShardRunCommand { get; set; } = string.Empty;
|
||||
public string ShardRunArguments { get; set; } = string.Empty;
|
||||
public int? ShardRunPort { get; set; }
|
||||
public string MiningProxyUrl { get; set; } = string.Empty;
|
||||
public string MiningProxyCreds { get; set; } = string.Empty;
|
||||
|
||||
public string BotListToken { get; set; } = string.Empty;
|
||||
public string TwitchClientId { get; set; } = string.Empty;
|
||||
public string VotesToken { get; set; } = string.Empty;
|
||||
public string VotesUrl { get; set; } = string.Empty;
|
||||
public string RedisOptions { get; set; } = string.Empty;
|
||||
public string LocationIqApiKey { get; set; } = string.Empty;
|
||||
public string TimezoneDbApiKey { get; set; } = string.Empty;
|
||||
public string CoinmarketcapApiKey { get; set; } = string.Empty;
|
||||
|
||||
public class RestartConfig
|
||||
{
|
||||
public string Cmd { get; set; }
|
||||
public string Args { get; set; }
|
||||
|
||||
public RestartConfig(string cmd, string args)
|
||||
{
|
||||
Cmd = cmd;
|
||||
Args = args;
|
||||
}
|
||||
}
|
||||
}
|
23
src/Ellie.Bot.Common/Common/OptionsParser.cs
Normal file
23
src/Ellie.Bot.Common/Common/OptionsParser.cs
Normal file
|
@ -0,0 +1,23 @@
|
|||
using CommandLine;
|
||||
|
||||
namespace Ellie.Common;
|
||||
|
||||
public static class OptionsParser
|
||||
{
|
||||
public static T ParseFrom<T>(string[]? args)
|
||||
where T : IEllieCommandOptions, new()
|
||||
=> ParseFrom(new T(), args).Item1;
|
||||
|
||||
public static (T, bool) ParseFrom<T>(T options, string[]? args)
|
||||
where T : IEllieCommandOptions
|
||||
{
|
||||
using var p = new Parser(x =>
|
||||
{
|
||||
x.HelpWriter = null;
|
||||
});
|
||||
var res = p.ParseArguments<T>(args);
|
||||
var output = res.MapResult(x => x, _ => options);
|
||||
output.NormalizeOptions();
|
||||
return (output, res.Tag == ParserResultType.Parsed);
|
||||
}
|
||||
}
|
9
src/Ellie.Bot.Common/Common/OsuMapData.cs
Normal file
9
src/Ellie.Bot.Common/Common/OsuMapData.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
#nullable disable
|
||||
namespace Ellie.Common;
|
||||
|
||||
public class OsuMapData
|
||||
{
|
||||
public string Title { get; set; }
|
||||
public string Artist { get; set; }
|
||||
public string Version { get; set; }
|
||||
}
|
58
src/Ellie.Bot.Common/Common/OsuUserBets.cs
Normal file
58
src/Ellie.Bot.Common/Common/OsuUserBets.cs
Normal file
|
@ -0,0 +1,58 @@
|
|||
#nullable disable
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace Ellie.Common;
|
||||
|
||||
public class OsuUserBests
|
||||
{
|
||||
[JsonProperty("beatmap_id")]
|
||||
public string BeatmapId { get; set; }
|
||||
|
||||
[JsonProperty("score_id")]
|
||||
public string ScoreId { get; set; }
|
||||
|
||||
[JsonProperty("score")]
|
||||
public string Score { get; set; }
|
||||
|
||||
[JsonProperty("maxcombo")]
|
||||
public string Maxcombo { get; set; }
|
||||
|
||||
[JsonProperty("count50")]
|
||||
public double Count50 { get; set; }
|
||||
|
||||
[JsonProperty("count100")]
|
||||
public double Count100 { get; set; }
|
||||
|
||||
[JsonProperty("count300")]
|
||||
public double Count300 { get; set; }
|
||||
|
||||
[JsonProperty("countmiss")]
|
||||
public int Countmiss { get; set; }
|
||||
|
||||
[JsonProperty("countkatu")]
|
||||
public double Countkatu { get; set; }
|
||||
|
||||
[JsonProperty("countgeki")]
|
||||
public double Countgeki { get; set; }
|
||||
|
||||
[JsonProperty("perfect")]
|
||||
public string Perfect { get; set; }
|
||||
|
||||
[JsonProperty("enabled_mods")]
|
||||
public int EnabledMods { get; set; }
|
||||
|
||||
[JsonProperty("user_id")]
|
||||
public string UserId { get; set; }
|
||||
|
||||
[JsonProperty("date")]
|
||||
public string Date { get; set; }
|
||||
|
||||
[JsonProperty("rank")]
|
||||
public string Rank { get; set; }
|
||||
|
||||
[JsonProperty("pp")]
|
||||
public double Pp { get; set; }
|
||||
|
||||
[JsonProperty("replay_available")]
|
||||
public string ReplayAvailable { get; set; }
|
||||
}
|
8
src/Ellie.Bot.Common/Common/Pokemon/PokemonNameId.cs
Normal file
8
src/Ellie.Bot.Common/Common/Pokemon/PokemonNameId.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
#nullable disable
|
||||
namespace Ellie.Common.Pokemon;
|
||||
|
||||
public class PokemonNameId
|
||||
{
|
||||
public int Id { get; set; }
|
||||
public string Name { get; set; }
|
||||
}
|
42
src/Ellie.Bot.Common/Common/Pokemon/SearchPokemon.cs
Normal file
42
src/Ellie.Bot.Common/Common/Pokemon/SearchPokemon.cs
Normal file
|
@ -0,0 +1,42 @@
|
|||
#nullable disable
|
||||
|
||||
using System.Text.Json.Serialization;
|
||||
|
||||
namespace Ellie.Common.Pokemon;
|
||||
|
||||
public class SearchPokemon
|
||||
{
|
||||
[JsonPropertyName("num")]
|
||||
public int Id { get; set; }
|
||||
|
||||
public string Species { get; set; }
|
||||
public string[] Types { get; set; }
|
||||
public GenderRatioClass GenderRatio { get; set; }
|
||||
public BaseStatsClass BaseStats { get; set; }
|
||||
public Dictionary<string, string> Abilities { get; set; }
|
||||
public float HeightM { get; set; }
|
||||
public float WeightKg { get; set; }
|
||||
public string Color { get; set; }
|
||||
public string[] Evos { get; set; }
|
||||
public string[] EggGroups { get; set; }
|
||||
|
||||
public class GenderRatioClass
|
||||
{
|
||||
public float M { get; set; }
|
||||
public float F { get; set; }
|
||||
}
|
||||
|
||||
public class BaseStatsClass
|
||||
{
|
||||
public int Hp { get; set; }
|
||||
public int Atk { get; set; }
|
||||
public int Def { get; set; }
|
||||
public int Spa { get; set; }
|
||||
public int Spd { get; set; }
|
||||
public int Spe { get; set; }
|
||||
|
||||
public override string ToString()
|
||||
=> $@"💚**HP:** {Hp,-4} ⚔**ATK:** {Atk,-4} 🛡**DEF:** {Def,-4}
|
||||
✨**SPA:** {Spa,-4} 🎇**SPD:** {Spd,-4} 💨**SPE:** {Spe,-4}";
|
||||
}
|
||||
}
|
10
src/Ellie.Bot.Common/Common/Pokemon/SearchPokemonAbility.cs
Normal file
10
src/Ellie.Bot.Common/Common/Pokemon/SearchPokemonAbility.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
#nullable disable
|
||||
namespace Ellie.Common.Pokemon;
|
||||
|
||||
public class SearchPokemonAbility
|
||||
{
|
||||
public string Desc { get; set; }
|
||||
public string ShortDesc { get; set; }
|
||||
public string Name { get; set; }
|
||||
public float Rating { get; set; }
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
#nullable disable
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
|
||||
namespace Ellie.Common;
|
||||
|
||||
public class RequireObjectPropertiesContractResolver : DefaultContractResolver
|
||||
{
|
||||
protected override JsonObjectContract CreateObjectContract(Type objectType)
|
||||
{
|
||||
var contract = base.CreateObjectContract(objectType);
|
||||
contract.ItemRequired = Required.DisallowNull;
|
||||
return contract;
|
||||
}
|
||||
}
|
11
src/Ellie.Bot.Common/Common/TriviaQuestionModel.cs
Normal file
11
src/Ellie.Bot.Common/Common/TriviaQuestionModel.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
#nullable disable
|
||||
namespace Ellie.Modules.Games.Common.Trivia;
|
||||
|
||||
public sealed class TriviaQuestionModel
|
||||
{
|
||||
public string Category { get; init; }
|
||||
public string Question { get; init; }
|
||||
public string ImageUrl { get; init; }
|
||||
public string AnswerImageUrl { get; init; }
|
||||
public string Answer { get; init; }
|
||||
}
|
13
src/Ellie.Bot.Common/Common/TypeReaders/EmoteTypeReader.cs
Normal file
13
src/Ellie.Bot.Common/Common/TypeReaders/EmoteTypeReader.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
#nullable disable
|
||||
namespace Ellie.Common.TypeReaders;
|
||||
|
||||
public sealed class EmoteTypeReader : EllieTypeReader<Emote>
|
||||
{
|
||||
public override ValueTask<TypeReaderResult<Emote>> ReadAsync(ICommandContext ctx, string input)
|
||||
{
|
||||
if (!Emote.TryParse(input, out var emote))
|
||||
return new(TypeReaderResult.FromError<Emote>(CommandError.ParseFailed, "Input is not a valid emote"));
|
||||
|
||||
return new(TypeReaderResult.FromSuccess(emote));
|
||||
}
|
||||
}
|
24
src/Ellie.Bot.Common/Common/TypeReaders/GuildTypeReader.cs
Normal file
24
src/Ellie.Bot.Common/Common/TypeReaders/GuildTypeReader.cs
Normal file
|
@ -0,0 +1,24 @@
|
|||
#nullable disable
|
||||
namespace Ellie.Common.TypeReaders;
|
||||
|
||||
public sealed class GuildTypeReader : EllieTypeReader<IGuild>
|
||||
{
|
||||
private readonly DiscordSocketClient _client;
|
||||
|
||||
public GuildTypeReader(DiscordSocketClient client)
|
||||
=> _client = client;
|
||||
|
||||
public override ValueTask<TypeReaderResult<IGuild>> ReadAsync(ICommandContext context, string input)
|
||||
{
|
||||
input = input.Trim().ToUpperInvariant();
|
||||
var guilds = _client.Guilds;
|
||||
IGuild guild = guilds.FirstOrDefault(g => g.Id.ToString().Trim().ToUpperInvariant() == input) //by id
|
||||
?? guilds.FirstOrDefault(g => g.Name.Trim().ToUpperInvariant() == input); //by name
|
||||
|
||||
if (guild is not null)
|
||||
return new(TypeReaderResult.FromSuccess(guild));
|
||||
|
||||
return new(
|
||||
TypeReaderResult.FromError<IGuild>(CommandError.ParseFailed, "No guild by that name or Id found"));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
namespace Ellie.Common.TypeReaders;
|
||||
|
||||
public sealed class GuildUserTypeReader : EllieTypeReader<IGuildUser>
|
||||
{
|
||||
public override async ValueTask<TypeReaderResult<IGuildUser>> ReadAsync(ICommandContext ctx, string input)
|
||||
{
|
||||
if (ctx.Guild is null)
|
||||
return TypeReaderResult.FromError<IGuildUser>(CommandError.Unsuccessful, "Must be in a guild.");
|
||||
|
||||
input = input.Trim();
|
||||
IGuildUser? user = null;
|
||||
if (MentionUtils.TryParseUser(input, out var id))
|
||||
user = await ctx.Guild.GetUserAsync(id, CacheMode.AllowDownload);
|
||||
|
||||
if (ulong.TryParse(input, out id))
|
||||
user = await ctx.Guild.GetUserAsync(id, CacheMode.AllowDownload);
|
||||
|
||||
if (user is null)
|
||||
{
|
||||
var users = await ctx.Guild.GetUsersAsync(CacheMode.CacheOnly);
|
||||
user = users.FirstOrDefault(x => x.Username == input)
|
||||
?? users.FirstOrDefault(x =>
|
||||
string.Equals(x.ToString(), input, StringComparison.InvariantCultureIgnoreCase))
|
||||
?? users.FirstOrDefault(x =>
|
||||
string.Equals(x.Username, input, StringComparison.InvariantCultureIgnoreCase));
|
||||
}
|
||||
|
||||
if (user is null)
|
||||
return TypeReaderResult.FromError<IGuildUser>(CommandError.ObjectNotFound, "User not found.");
|
||||
|
||||
return TypeReaderResult.FromSuccess(user);
|
||||
}
|
||||
}
|
19
src/Ellie.Bot.Common/Common/TypeReaders/KwumTypeReader.cs
Normal file
19
src/Ellie.Bot.Common/Common/TypeReaders/KwumTypeReader.cs
Normal file
|
@ -0,0 +1,19 @@
|
|||
#nullable disable
|
||||
namespace Ellie.Common.TypeReaders;
|
||||
|
||||
public sealed class KwumTypeReader : EllieTypeReader<kwum>
|
||||
{
|
||||
public override ValueTask<TypeReaderResult<kwum>> ReadAsync(ICommandContext context, string input)
|
||||
{
|
||||
if (kwum.TryParse(input, out var val))
|
||||
return new(TypeReaderResult.FromSuccess(val));
|
||||
|
||||
return new(TypeReaderResult.FromError<kwum>(CommandError.ParseFailed, "Input is not a valid kwum"));
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class SmartTextTypeReader : EllieTypeReader<SmartText>
|
||||
{
|
||||
public override ValueTask<TypeReaderResult<SmartText>> ReadAsync(ICommandContext ctx, string input)
|
||||
=> new(TypeReaderResult.FromSuccess(SmartText.CreateFrom(input)));
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
#nullable disable
|
||||
namespace Ellie.Common.TypeReaders.Models;
|
||||
|
||||
public class PermissionAction
|
||||
{
|
||||
public static PermissionAction Enable
|
||||
=> new(true);
|
||||
|
||||
public static PermissionAction Disable
|
||||
=> new(false);
|
||||
|
||||
public bool Value { get; }
|
||||
|
||||
public PermissionAction(bool value)
|
||||
=> Value = value;
|
||||
|
||||
public override bool Equals(object obj)
|
||||
{
|
||||
if (obj is null || GetType() != obj.GetType())
|
||||
return false;
|
||||
|
||||
return Value == ((PermissionAction)obj).Value;
|
||||
}
|
||||
|
||||
public override int GetHashCode()
|
||||
=> Value.GetHashCode();
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
#nullable disable
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Ellie.Common.TypeReaders.Models;
|
||||
|
||||
public class StoopidTime
|
||||
{
|
||||
private static readonly Regex _regex = new(
|
||||
@"^(?:(?<months>\d)mo)?(?:(?<weeks>\d{1,2})w)?(?:(?<days>\d{1,2})d)?(?:(?<hours>\d{1,4})h)?(?:(?<minutes>\d{1,5})m)?(?:(?<seconds>\d{1,6})s)?$",
|
||||
RegexOptions.Compiled | RegexOptions.Multiline);
|
||||
|
||||
public string Input { get; set; }
|
||||
public TimeSpan Time { get; set; }
|
||||
|
||||
private StoopidTime() { }
|
||||
|
||||
public static StoopidTime FromInput(string input)
|
||||
{
|
||||
var m = _regex.Match(input);
|
||||
|
||||
if (m.Length == 0)
|
||||
throw new ArgumentException("Invalid string input format.");
|
||||
|
||||
var namesAndValues = new Dictionary<string, int>();
|
||||
|
||||
foreach (var groupName in _regex.GetGroupNames())
|
||||
{
|
||||
if (groupName == "0")
|
||||
continue;
|
||||
if (!int.TryParse(m.Groups[groupName].Value, out var value))
|
||||
{
|
||||
namesAndValues[groupName] = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value < 1)
|
||||
throw new ArgumentException($"Invalid {groupName} value.");
|
||||
|
||||
namesAndValues[groupName] = value;
|
||||
}
|
||||
|
||||
var ts = new TimeSpan((30 * namesAndValues["months"]) + (7 * namesAndValues["weeks"]) + namesAndValues["days"],
|
||||
namesAndValues["hours"],
|
||||
namesAndValues["minutes"],
|
||||
namesAndValues["seconds"]);
|
||||
if (ts > TimeSpan.FromDays(90))
|
||||
throw new ArgumentException("Time is too long.");
|
||||
|
||||
return new()
|
||||
{
|
||||
Input = input,
|
||||
Time = ts
|
||||
};
|
||||
}
|
||||
}
|
50
src/Ellie.Bot.Common/Common/TypeReaders/ModuleTypeReader.cs
Normal file
50
src/Ellie.Bot.Common/Common/TypeReaders/ModuleTypeReader.cs
Normal file
|
@ -0,0 +1,50 @@
|
|||
#nullable disable
|
||||
namespace Ellie.Common.TypeReaders;
|
||||
|
||||
public sealed class ModuleTypeReader : EllieTypeReader<ModuleInfo>
|
||||
{
|
||||
private readonly CommandService _cmds;
|
||||
|
||||
public ModuleTypeReader(CommandService cmds)
|
||||
=> _cmds = cmds;
|
||||
|
||||
public override ValueTask<TypeReaderResult<ModuleInfo>> ReadAsync(ICommandContext context, string input)
|
||||
{
|
||||
input = input.ToUpperInvariant();
|
||||
var module = _cmds.Modules.GroupBy(m => m.GetTopLevelModule())
|
||||
.FirstOrDefault(m => m.Key.Name.ToUpperInvariant() == input)
|
||||
?.Key;
|
||||
if (module is null)
|
||||
return new(TypeReaderResult.FromError<ModuleInfo>(CommandError.ParseFailed, "No such module found."));
|
||||
|
||||
return new(TypeReaderResult.FromSuccess(module));
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ModuleOrCrTypeReader : EllieTypeReader<ModuleOrCrInfo>
|
||||
{
|
||||
private readonly CommandService _cmds;
|
||||
|
||||
public ModuleOrCrTypeReader(CommandService cmds)
|
||||
=> _cmds = cmds;
|
||||
|
||||
public override ValueTask<TypeReaderResult<ModuleOrCrInfo>> ReadAsync(ICommandContext context, string input)
|
||||
{
|
||||
input = input.ToUpperInvariant();
|
||||
var module = _cmds.Modules.GroupBy(m => m.GetTopLevelModule())
|
||||
.FirstOrDefault(m => m.Key.Name.ToUpperInvariant() == input)
|
||||
?.Key;
|
||||
if (module is null && input != "ACTUALEXPRESSIONS")
|
||||
return new(TypeReaderResult.FromError<ModuleOrCrInfo>(CommandError.ParseFailed, "No such module found."));
|
||||
|
||||
return new(TypeReaderResult.FromSuccess(new ModuleOrCrInfo
|
||||
{
|
||||
Name = input
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
public sealed class ModuleOrCrInfo
|
||||
{
|
||||
public string Name { get; set; }
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
#nullable disable
|
||||
using Ellie.Common.TypeReaders.Models;
|
||||
|
||||
namespace Ellie.Common.TypeReaders;
|
||||
|
||||
/// <summary>
|
||||
/// Used instead of bool for more flexible keywords for true/false only in the permission module
|
||||
/// </summary>
|
||||
public sealed class PermissionActionTypeReader : EllieTypeReader<PermissionAction>
|
||||
{
|
||||
public override ValueTask<TypeReaderResult<PermissionAction>> ReadAsync(ICommandContext context, string input)
|
||||
{
|
||||
input = input.ToUpperInvariant();
|
||||
switch (input)
|
||||
{
|
||||
case "1":
|
||||
case "T":
|
||||
case "TRUE":
|
||||
case "ENABLE":
|
||||
case "ENABLED":
|
||||
case "ALLOW":
|
||||
case "PERMIT":
|
||||
case "UNBAN":
|
||||
return new(TypeReaderResult.FromSuccess(PermissionAction.Enable));
|
||||
case "0":
|
||||
case "F":
|
||||
case "FALSE":
|
||||
case "DENY":
|
||||
case "DISABLE":
|
||||
case "DISABLED":
|
||||
case "DISALLOW":
|
||||
case "BAN":
|
||||
return new(TypeReaderResult.FromSuccess(PermissionAction.Disable));
|
||||
default:
|
||||
return new(TypeReaderResult.FromError<PermissionAction>(CommandError.ParseFailed,
|
||||
"Did not receive a valid boolean value"));
|
||||
}
|
||||
}
|
||||
}
|
20
src/Ellie.Bot.Common/Common/TypeReaders/Rgba32TypeReader.cs
Normal file
20
src/Ellie.Bot.Common/Common/TypeReaders/Rgba32TypeReader.cs
Normal file
|
@ -0,0 +1,20 @@
|
|||
using Color = SixLabors.ImageSharp.Color;
|
||||
|
||||
#nullable disable
|
||||
namespace Ellie.Common.TypeReaders;
|
||||
|
||||
public sealed class Rgba32TypeReader : EllieTypeReader<Color>
|
||||
{
|
||||
public override ValueTask<TypeReaderResult<Color>> ReadAsync(ICommandContext context, string input)
|
||||
{
|
||||
input = input.Replace("#", "", StringComparison.InvariantCulture);
|
||||
try
|
||||
{
|
||||
return ValueTask.FromResult(TypeReaderResult.FromSuccess(Color.ParseHex(input)));
|
||||
}
|
||||
catch
|
||||
{
|
||||
return ValueTask.FromResult(TypeReaderResult.FromError<Color>(CommandError.ParseFailed, "Parameter is not a valid color hex."));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
#nullable disable
|
||||
using Ellie.Common.TypeReaders.Models;
|
||||
|
||||
namespace Ellie.Common.TypeReaders;
|
||||
|
||||
public sealed class StoopidTimeTypeReader : EllieTypeReader<StoopidTime>
|
||||
{
|
||||
public override ValueTask<TypeReaderResult<StoopidTime>> ReadAsync(ICommandContext context, string input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
return new(TypeReaderResult.FromError<StoopidTime>(CommandError.Unsuccessful, "Input is empty."));
|
||||
try
|
||||
{
|
||||
var time = StoopidTime.FromInput(input);
|
||||
return new(TypeReaderResult.FromSuccess(time));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return new(TypeReaderResult.FromError<StoopidTime>(CommandError.Exception, ex.Message));
|
||||
}
|
||||
}
|
||||
}
|
203
src/Ellie.Bot.Common/Configs/BotConfig.cs
Normal file
203
src/Ellie.Bot.Common/Configs/BotConfig.cs
Normal file
|
@ -0,0 +1,203 @@
|
|||
#nullable disable
|
||||
|
||||
using Cloneable;
|
||||
using Ellie.Common.Yml;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using System.Globalization;
|
||||
using YamlDotNet.Core;
|
||||
using YamlDotNet.Serialization;
|
||||
|
||||
namespace Ellie.Common.Configs;
|
||||
|
||||
[Cloneable]
|
||||
public sealed partial class BotConfig : ICloneable<BotConfig>
|
||||
{
|
||||
[Comment("""DO NOT CHANGE""")]
|
||||
public int Version { get; set; } = 5;
|
||||
|
||||
[Comment("""
|
||||
Most commands, when executed, have a small colored line
|
||||
next to the response. The color depends whether the command
|
||||
is completed, errored or in progress (pending)
|
||||
Color settings below are for the color of those lines.
|
||||
To get color's hex, you can go here https://htmlcolorcodes.com/
|
||||
and copy the hex code fo your selected color (marked as #)
|
||||
""")]
|
||||
public ColorConfig Color { get; set; }
|
||||
|
||||
[Comment("Default bot language. It has to be in the list of supported languages (.langli)")]
|
||||
public CultureInfo DefaultLocale { get; set; }
|
||||
|
||||
[Comment("""
|
||||
Style in which executed commands will show up in the console.
|
||||
Allowed values: Simple, Normal, None
|
||||
""")]
|
||||
public ConsoleOutputType ConsoleOutputType { get; set; }
|
||||
|
||||
[Comment("""Whether the bot will check for new releases every hour""")]
|
||||
public bool CheckForUpdates { get; set; } = true;
|
||||
|
||||
[Comment("""Do you want any messages sent by users in Bot's DM to be forwarded to the owner(s)?""")]
|
||||
public bool ForwardMessages { get; set; }
|
||||
|
||||
[Comment("""
|
||||
Do you want the message to be forwarded only to the first owner specified in the list of owners (in creds.yml),
|
||||
or all owners? (this might cause the bot to lag if there's a lot of owners specified)
|
||||
""")]
|
||||
public bool ForwardToAllOwners { get; set; }
|
||||
|
||||
[Comment("""
|
||||
Any messages sent by users in Bot's DM to be forwarded to the specified channel.
|
||||
This option will only work when ForwardToAllOwners is set to false
|
||||
""")]
|
||||
public ulong? ForwardToChannel { get; set; }
|
||||
|
||||
[Comment("""
|
||||
When a user DMs the bot with a message which is not a command
|
||||
they will receive this message. Leave empty for no response. The string which will be sent whenever someone DMs the bot.
|
||||
Supports embeds. How it looks: https://puu.sh/B0BLV.png
|
||||
""")]
|
||||
[YamlMember(ScalarStyle = ScalarStyle.Literal)]
|
||||
public string DmHelpText { get; set; }
|
||||
|
||||
[Comment("""
|
||||
Only users who send a DM to the bot containing one of the specified words will get a DmHelpText response.
|
||||
Case insensitive.
|
||||
Leave empty to reply with DmHelpText to every DM.
|
||||
""")]
|
||||
public List<string> DmHelpTextKeywords { get; set; }
|
||||
|
||||
[Comment("""This is the response for the .h command""")]
|
||||
[YamlMember(ScalarStyle = ScalarStyle.Literal)]
|
||||
public string HelpText { get; set; }
|
||||
|
||||
[Comment("""List of modules and commands completely blocked on the bot""")]
|
||||
public BlockedConfig Blocked { get; set; }
|
||||
|
||||
[Comment("""Which string will be used to recognize the commands""")]
|
||||
public string Prefix { get; set; }
|
||||
|
||||
[Comment("""
|
||||
Toggles whether your bot will group greet/bye messages into a single message every 5 seconds.
|
||||
1st user who joins will get greeted immediately
|
||||
If more users join within the next 5 seconds, they will be greeted in groups of 5.
|
||||
This will cause %user.mention% and other placeholders to be replaced with multiple users.
|
||||
Keep in mind this might break some of your embeds - for example if you have %user.avatar% in the thumbnail,
|
||||
it will become invalid, as it will resolve to a list of avatars of grouped users.
|
||||
note: This setting is primarily used if you're afraid of raids, or you're running medium/large bots where some
|
||||
servers might get hundreds of people join at once. This is used to prevent the bot from getting ratelimited,
|
||||
and (slightly) reduce the greet spam in those servers.
|
||||
""")]
|
||||
public bool GroupGreets { get; set; }
|
||||
|
||||
[Comment("""
|
||||
Whether the bot will rotate through all specified statuses.
|
||||
This setting can be changed via .ropl command.
|
||||
See RotatingStatuses submodule in Administration.
|
||||
""")]
|
||||
public bool RotateStatuses { get; set; }
|
||||
|
||||
public BotConfig()
|
||||
{
|
||||
var color = new ColorConfig();
|
||||
Color = color;
|
||||
DefaultLocale = new("en-US");
|
||||
ConsoleOutputType = ConsoleOutputType.Normal;
|
||||
ForwardMessages = false;
|
||||
ForwardToAllOwners = false;
|
||||
DmHelpText = """{"description": "Type `%prefix%h` for help."}""";
|
||||
HelpText = """
|
||||
{
|
||||
"title": "To invite me to your server, use this link",
|
||||
"description": "https://discordapp.com/oauth2/authorize?client_id={0}&scope=bot&permissions=66186303",
|
||||
"color": 53380,
|
||||
"thumbnail": "https://i.imgur.com/nKYyqMK.png",
|
||||
"fields": [
|
||||
{
|
||||
"name": "Useful help commands",
|
||||
"value": "`%bot.prefix%modules` Lists all bot modules.
|
||||
`%prefix%h CommandName` Shows some help about a specific command.
|
||||
`%prefix%commands ModuleName` Lists all commands in a module.",
|
||||
"inline": false
|
||||
},
|
||||
{
|
||||
"name": "List of all Commands",
|
||||
"value": "https://commands.elliebot.net",
|
||||
"inline": false
|
||||
},
|
||||
{
|
||||
"name": "Ellie Support Server",
|
||||
"value": "https://discord.elliebot.net/ ",
|
||||
"inline": true
|
||||
}
|
||||
]
|
||||
}
|
||||
""";
|
||||
var blocked = new BlockedConfig();
|
||||
Blocked = blocked;
|
||||
Prefix = ".";
|
||||
RotateStatuses = false;
|
||||
GroupGreets = false;
|
||||
DmHelpTextKeywords = new()
|
||||
{
|
||||
"help",
|
||||
"commands",
|
||||
"cmds",
|
||||
"module",
|
||||
"can you do"
|
||||
};
|
||||
}
|
||||
|
||||
// [Comment(@"Whether the prefix will be a suffix, or prefix.
|
||||
// For example, if your prefix is ! you will run a command called 'cash' by typing either
|
||||
// '!cash @Someone' if your prefixIsSuffix: false or
|
||||
// 'cash @Someone!' if your prefixIsSuffix: true")]
|
||||
// public bool PrefixIsSuffix { get; set; }
|
||||
|
||||
// public string Prefixed(string text) => PrefixIsSuffix
|
||||
// ? text + Prefix
|
||||
// : Prefix + text;
|
||||
|
||||
public string Prefixed(string text)
|
||||
=> Prefix + text;
|
||||
}
|
||||
|
||||
[Cloneable]
|
||||
public sealed partial class BlockedConfig
|
||||
{
|
||||
public HashSet<string> Commands { get; set; }
|
||||
public HashSet<string> Modules { get; set; }
|
||||
|
||||
public BlockedConfig()
|
||||
{
|
||||
Modules = new();
|
||||
Commands = new();
|
||||
}
|
||||
}
|
||||
|
||||
[Cloneable]
|
||||
public partial class ColorConfig
|
||||
{
|
||||
[Comment("""Color used for embed responses when command successfully executes""")]
|
||||
public Rgba32 Ok { get; set; }
|
||||
|
||||
[Comment("""Color used for embed responses when command has an error""")]
|
||||
public Rgba32 Error { get; set; }
|
||||
|
||||
[Comment("""Color used for embed responses while command is doing work or is in progress""")]
|
||||
public Rgba32 Pending { get; set; }
|
||||
|
||||
public ColorConfig()
|
||||
{
|
||||
Ok = Rgba32.ParseHex("00e584");
|
||||
Error = Rgba32.ParseHex("ee281f");
|
||||
Pending = Rgba32.ParseHex("faa61a");
|
||||
}
|
||||
}
|
||||
|
||||
public enum ConsoleOutputType
|
||||
{
|
||||
Normal = 0,
|
||||
Simple = 1,
|
||||
None = 2
|
||||
}
|
18
src/Ellie.Bot.Common/Configs/IConfigSeria.cs
Normal file
18
src/Ellie.Bot.Common/Configs/IConfigSeria.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
namespace Ellie.Common.Configs;
|
||||
|
||||
/// <summary>
|
||||
/// Base interface for available config serializers
|
||||
/// </summary>
|
||||
public interface IConfigSeria
|
||||
{
|
||||
/// <summary>
|
||||
/// Serialize the object to string
|
||||
/// </summary>
|
||||
public string Serialize<T>(T obj)
|
||||
where T : notnull;
|
||||
|
||||
/// <summary>
|
||||
/// Deserialize string data into an object of the specified type
|
||||
/// </summary>
|
||||
public T Deserialize<T>(string data);
|
||||
}
|
273
src/Ellie.Bot.Common/Creds.cs
Normal file
273
src/Ellie.Bot.Common/Creds.cs
Normal file
|
@ -0,0 +1,273 @@
|
|||
#nullable disable
|
||||
using Ellie.Common.Yml;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace Ellie.Common;
|
||||
|
||||
public sealed class Creds : IBotCredentials
|
||||
{
|
||||
[Comment("""DO NOT CHANGE""")]
|
||||
public int Version { get; set; }
|
||||
|
||||
[Comment("""Bot token. Do not share with anyone ever -> https://discordapp.com/developers/applications/""")]
|
||||
public string Token { get; set; }
|
||||
|
||||
[Comment("""
|
||||
List of Ids of the users who have bot owner permissions
|
||||
**DO NOT ADD PEOPLE YOU DON'T TRUST**
|
||||
""")]
|
||||
public ICollection<ulong> OwnerIds { get; set; }
|
||||
|
||||
[Comment("Keep this on 'true' unless you're sure your bot shouldn't use privileged intents or you're waiting to be accepted")]
|
||||
public bool UsePrivilegedIntents { get; set; }
|
||||
|
||||
[Comment("""
|
||||
The number of shards that the bot will be running on.
|
||||
Leave at 1 if you don't know what you're doing.
|
||||
|
||||
note: If you are planning to have more than one shard, then you must change botCache to 'redis'.
|
||||
Also, in that case you should be using Ellie.Coordinator to start the bot, and it will correctly override this value.
|
||||
""")]
|
||||
public int TotalShards { get; set; }
|
||||
|
||||
[Comment(
|
||||
"""
|
||||
Login to https://console.cloud.google.com, create a new project, go to APIs & Services -> Library -> YouTube Data API and enable it.
|
||||
Then, go to APIs and Services -> Credentials and click Create credentials -> API key.
|
||||
Used only for Youtube Data Api (at the moment).
|
||||
""")]
|
||||
public string GoogleApiKey { get; set; }
|
||||
|
||||
[Comment(
|
||||
"""
|
||||
Create a new custom search here https://programmablesearchengine.google.com/cse/create/new
|
||||
Enable SafeSearch
|
||||
Remove all Sites to Search
|
||||
Enable Search the entire web
|
||||
Copy the 'Search Engine ID' to the SearchId field
|
||||
|
||||
Do all steps again but enable image search for the ImageSearchId
|
||||
""")]
|
||||
public GoogleApiConfig Google { get; set; }
|
||||
|
||||
[Comment("""Settings for voting system for discordbots. Meant for use on global Ellie.""")]
|
||||
public VotesSettings Votes { get; set; }
|
||||
|
||||
[Comment("""
|
||||
Patreon auto reward system settings.
|
||||
go to https://www.patreon.com/portal -> my clients -> create client
|
||||
""")]
|
||||
public PatreonSettings Patreon { get; set; }
|
||||
|
||||
[Comment("""Api key for sending stats to DiscordBotList.""")]
|
||||
public string BotListToken { get; set; }
|
||||
|
||||
[Comment("""Official cleverbot api key.""")]
|
||||
public string CleverbotApiKey { get; set; }
|
||||
|
||||
[Comment(@"Official GPT-3 api key.")]
|
||||
public string Gpt3ApiKey { get; set; }
|
||||
|
||||
[Comment("""
|
||||
Which cache implementation should bot use.
|
||||
'memory' - Cache will be in memory of the bot's process itself. Only use this on bots with a single shard. When the bot is restarted the cache is reset.
|
||||
'redis' - Uses redis (which needs to be separately downloaded and installed). The cache will persist through bot restarts. You can configure connection string in creds.yml
|
||||
""")]
|
||||
public BotCacheImplemenation BotCache { get; set; }
|
||||
|
||||
[Comment("""
|
||||
Redis connection string. Don't change if you don't know what you're doing.
|
||||
Only used if botCache is set to 'redis'
|
||||
""")]
|
||||
public string RedisOptions { get; set; }
|
||||
|
||||
[Comment("""Database options. Don't change if you don't know what you're doing. Leave null for default values""")]
|
||||
public DbOptions Db { get; set; }
|
||||
|
||||
[Comment("""
|
||||
Address and port of the coordinator endpoint. Leave empty for default.
|
||||
Change only if you've changed the coordinator address or port.
|
||||
""")]
|
||||
public string CoordinatorUrl { get; set; }
|
||||
|
||||
[Comment(
|
||||
"""Api key obtained on https://rapidapi.com (go to MyApps -> Add New App -> Enter Name -> Application key)""")]
|
||||
public string RapidApiKey { get; set; }
|
||||
|
||||
[Comment("""
|
||||
https://locationiq.com api key (register and you will receive the token in the email).
|
||||
Used only for .time command.
|
||||
""")]
|
||||
public string LocationIqApiKey { get; set; }
|
||||
|
||||
[Comment("""
|
||||
https://timezonedb.com api key (register and you will receive the token in the email).
|
||||
Used only for .time command
|
||||
""")]
|
||||
public string TimezoneDbApiKey { get; set; }
|
||||
|
||||
[Comment("""
|
||||
https://pro.coinmarketcap.com/account/ api key. There is a free plan for personal use.
|
||||
Used for cryptocurrency related commands.
|
||||
""")]
|
||||
public string CoinmarketcapApiKey { get; set; }
|
||||
|
||||
// [Comment(@"https://polygon.io/dashboard/api-keys api key. Free plan allows for 5 queries per minute.
|
||||
// Used for stocks related commands.")]
|
||||
// public string PolygonIoApiKey { get; set; }
|
||||
|
||||
[Comment("""Api key used for Osu related commands. Obtain this key at https://osu.ppy.sh/p/api""")]
|
||||
public string OsuApiKey { get; set; }
|
||||
|
||||
[Comment("""
|
||||
Optional Trovo client id.
|
||||
You should use this if Trovo stream notifications stopped working or you're getting ratelimit errors.
|
||||
""")]
|
||||
public string TrovoClientId { get; set; }
|
||||
|
||||
[Comment("""Obtain by creating an application at https://dev.twitch.tv/console/apps""")]
|
||||
public string TwitchClientId { get; set; }
|
||||
|
||||
[Comment("""Obtain by creating an application at https://dev.twitch.tv/console/apps""")]
|
||||
public string TwitchClientSecret { get; set; }
|
||||
|
||||
[Comment("""
|
||||
Command and args which will be used to restart the bot.
|
||||
Only used if bot is executed directly (NOT through the coordinator)
|
||||
placeholders:
|
||||
{0} -> shard id
|
||||
{1} -> total shards
|
||||
Linux default
|
||||
cmd: dotnet
|
||||
args: "Ellie.dll -- {0}"
|
||||
Windows default
|
||||
cmd: Ellie.exe
|
||||
args: "{0}"
|
||||
""")]
|
||||
public RestartConfig RestartCommand { get; set; }
|
||||
|
||||
public Creds()
|
||||
{
|
||||
Version = 7;
|
||||
Token = string.Empty;
|
||||
UsePrivilegedIntents = true;
|
||||
OwnerIds = new List<ulong>();
|
||||
TotalShards = 1;
|
||||
GoogleApiKey = string.Empty;
|
||||
Votes = new VotesSettings(string.Empty, string.Empty, string.Empty, string.Empty);
|
||||
Patreon = new PatreonSettings(string.Empty, string.Empty, string.Empty, string.Empty);
|
||||
BotListToken = string.Empty;
|
||||
CleverbotApiKey = string.Empty;
|
||||
Gpt3ApiKey = string.Empty;
|
||||
BotCache = BotCacheImplemenation.Memory;
|
||||
RedisOptions = "localhost:6379,syncTimeout=30000,responseTimeout=30000,allowAdmin=true,password=";
|
||||
Db = new DbOptions()
|
||||
{
|
||||
Type = "sqlite",
|
||||
ConnectionString = "Data Source=data/Ellie.db"
|
||||
};
|
||||
|
||||
CoordinatorUrl = "http://localhost:3442";
|
||||
|
||||
RestartCommand = new RestartConfig();
|
||||
Google = new GoogleApiConfig();
|
||||
}
|
||||
|
||||
public class DbOptions
|
||||
: IDbOptions
|
||||
{
|
||||
[Comment("""
|
||||
Database type. "sqlite", "mysql" and "postgresql" are supported.
|
||||
Default is "sqlite"
|
||||
""")]
|
||||
public string Type { get; set; }
|
||||
|
||||
[Comment("""
|
||||
Database connection string.
|
||||
You MUST change this if you're not using "sqlite" type.
|
||||
Default is "Data Source=data/Ellie.db"
|
||||
Example for mysql: "Server=localhost;Port=3306;Uid=root;Pwd=my_super_secret_mysql_password;Database=ellie"
|
||||
Example for postgresql: "Server=localhost;Port=5432;User Id=postgres;Password=my_super_secret_postgres_password;Database=ellie;"
|
||||
""")]
|
||||
public string ConnectionString { get; set; }
|
||||
}
|
||||
|
||||
public sealed record PatreonSettings : IPatreonSettings
|
||||
{
|
||||
public string ClientId { get; set; }
|
||||
public string AccessToken { get; set; }
|
||||
public string RefreshToken { get; set; }
|
||||
public string ClientSecret { get; set; }
|
||||
|
||||
[Comment(
|
||||
"""Campaign ID of your patreon page. Go to your patreon page (make sure you're logged in) and type "prompt('Campaign ID', window.patreon.bootstrap.creator.data.id);" in the console. (ctrl + shift + i)""")]
|
||||
public string CampaignId { get; set; }
|
||||
|
||||
public PatreonSettings(
|
||||
string accessToken,
|
||||
string refreshToken,
|
||||
string clientSecret,
|
||||
string campaignId)
|
||||
{
|
||||
AccessToken = accessToken;
|
||||
RefreshToken = refreshToken;
|
||||
ClientSecret = clientSecret;
|
||||
CampaignId = campaignId;
|
||||
}
|
||||
|
||||
public PatreonSettings()
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
public sealed record VotesSettings : IVotesSettings
|
||||
{
|
||||
[Comment("""
|
||||
top.gg votes service url
|
||||
This is the url of your instance of the Ellie.Votes api
|
||||
Example: https://votes.my.cool.bot.com
|
||||
""")]
|
||||
public string TopggServiceUrl { get; set; }
|
||||
|
||||
[Comment("""
|
||||
Authorization header value sent to the TopGG service url with each request
|
||||
This should be equivalent to the TopggKey in your Ellie.Votes api appsettings.json file
|
||||
""")]
|
||||
public string TopggKey { get; set; }
|
||||
|
||||
[Comment("""
|
||||
discords.com votes service url
|
||||
This is the url of your instance of the Ellie.Votes api
|
||||
Example: https://votes.my.cool.bot.com
|
||||
""")]
|
||||
public string DiscordsServiceUrl { get; set; }
|
||||
|
||||
[Comment("""
|
||||
Authorization header value sent to the Discords service url with each request
|
||||
This should be equivalent to the DiscordsKey in your Ellie.Votes api appsettings.json file
|
||||
""")]
|
||||
public string DiscordsKey { get; set; }
|
||||
|
||||
public VotesSettings()
|
||||
{
|
||||
}
|
||||
|
||||
public VotesSettings(
|
||||
string topggServiceUrl,
|
||||
string topggKey,
|
||||
string discordsServiceUrl,
|
||||
string discordsKey)
|
||||
{
|
||||
TopggServiceUrl = topggServiceUrl;
|
||||
TopggKey = topggKey;
|
||||
DiscordsServiceUrl = discordsServiceUrl;
|
||||
DiscordsKey = discordsKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class GoogleApiConfig : IGoogleApiConfig
|
||||
{
|
||||
public string SearchId { get; init; }
|
||||
public string ImageSearchId { get; init; }
|
||||
}
|
5
src/Ellie.Bot.Common/Currency/CurrencyType.cs
Normal file
5
src/Ellie.Bot.Common/Currency/CurrencyType.cs
Normal file
|
@ -0,0 +1,5 @@
|
|||
namespace Ellie.Services.Currency;
|
||||
|
||||
public enum CurrencyType{
|
||||
Default
|
||||
}
|
10
src/Ellie.Bot.Common/Currency/IBankService.cs
Normal file
10
src/Ellie.Bot.Common/Currency/IBankService.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
namespace Ellie.Modules.Gambling.Bank;
|
||||
|
||||
public interface IBankService
|
||||
{
|
||||
Task<bool> DepositAsync(ulong userId, long amount);
|
||||
Task<bool> WithdrawAsync(ulong userId, long amount);
|
||||
Task<long> GetBalanceAsync(ulong userId);
|
||||
Task<bool> AwardAsync(ulong userId, long amount);
|
||||
Task<bool> TakeAsync(ulong userId, long amount);
|
||||
}
|
40
src/Ellie.Bot.Common/Currency/ICurrencyService.cs
Normal file
40
src/Ellie.Bot.Common/Currency/ICurrencyService.cs
Normal file
|
@ -0,0 +1,40 @@
|
|||
using Ellie.Services.Currency;
|
||||
|
||||
namespace Ellie.Services;
|
||||
|
||||
public interface ICurrencyService
|
||||
{
|
||||
Task<IWallet> GetWalletAsync(ulong userId, CurrencyType type = CurrencyType.Default);
|
||||
|
||||
Task AddBulkAsync(
|
||||
IReadOnlyCollection<ulong> userIds,
|
||||
long amount,
|
||||
TxData? txData,
|
||||
CurrencyType type = CurrencyType.Default);
|
||||
|
||||
Task RemoveBulkAsync(
|
||||
IReadOnlyCollection<ulong> userIds,
|
||||
long amount,
|
||||
TxData? txData,
|
||||
CurrencyType type = CurrencyType.Default);
|
||||
|
||||
Task AddAsync(
|
||||
ulong userId,
|
||||
long amount,
|
||||
TxData? txData);
|
||||
|
||||
Task AddAsync(
|
||||
IUser user,
|
||||
long amount,
|
||||
TxData? txData);
|
||||
|
||||
Task<bool> RemoveAsync(
|
||||
ulong userId,
|
||||
long amount,
|
||||
TxData? txData);
|
||||
|
||||
Task<bool> RemoveAsync(
|
||||
IUser user,
|
||||
long amount,
|
||||
TxData? txData);
|
||||
}
|
9
src/Ellie.Bot.Common/Currency/ITxTracker.cs
Normal file
9
src/Ellie.Bot.Common/Currency/ITxTracker.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
using Ellie.Services.Currency;
|
||||
|
||||
namespace Ellie.Services;
|
||||
|
||||
public interface ITxTracker
|
||||
{
|
||||
Task TrackAdd(long amount, TxData? txData);
|
||||
Task TrackRemove(long amount, TxData? txData);
|
||||
}
|
40
src/Ellie.Bot.Common/Currency/IWallet.cs
Normal file
40
src/Ellie.Bot.Common/Currency/IWallet.cs
Normal file
|
@ -0,0 +1,40 @@
|
|||
namespace Ellie.Services.Currency;
|
||||
|
||||
public interface IWallet
|
||||
{
|
||||
public ulong UserId { get; }
|
||||
|
||||
public Task<long> GetBalance();
|
||||
public Task<bool> Take(long amount, TxData? txData);
|
||||
public Task Add(long amount, TxData? txData);
|
||||
|
||||
public async Task<bool> Transfer(
|
||||
long amount,
|
||||
IWallet to,
|
||||
TxData? txData)
|
||||
{
|
||||
if (amount <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(amount), "Amount must be greater than 0.");
|
||||
|
||||
if (txData is not null)
|
||||
txData = txData with
|
||||
{
|
||||
OtherId = to.UserId
|
||||
};
|
||||
|
||||
var succ = await Take(amount, txData);
|
||||
|
||||
if (!succ)
|
||||
return false;
|
||||
|
||||
if (txData is not null)
|
||||
txData = txData with
|
||||
{
|
||||
OtherId = UserId
|
||||
};
|
||||
|
||||
await to.Add(amount, txData);
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
7
src/Ellie.Bot.Common/Currency/TxData.cs
Normal file
7
src/Ellie.Bot.Common/Currency/TxData.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
namespace Ellie.Services.Currency;
|
||||
|
||||
public record class TxData(
|
||||
string Type,
|
||||
string Extra,
|
||||
string? Note = "",
|
||||
ulong? OtherId = null);
|
18
src/Ellie.Bot.Common/DbService.cs
Normal file
18
src/Ellie.Bot.Common/DbService.cs
Normal file
|
@ -0,0 +1,18 @@
|
|||
#nullable disable
|
||||
using LinqToDB.Common;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Ellie.Services.Database;
|
||||
|
||||
namespace Ellie.Services;
|
||||
|
||||
public abstract class DbService
|
||||
{
|
||||
/// <summary>
|
||||
/// Call this to apply all migrations
|
||||
/// </summary>
|
||||
public abstract Task SetupAsync();
|
||||
|
||||
public abstract DbContext CreateRawDbContext(string dbType, string connString);
|
||||
public abstract DbContext GetDbContext();
|
||||
}
|
138
src/Ellie.Bot.Common/DoAsUserMessage.cs
Normal file
138
src/Ellie.Bot.Common/DoAsUserMessage.cs
Normal file
|
@ -0,0 +1,138 @@
|
|||
using MessageType = Discord.MessageType;
|
||||
|
||||
namespace Ellie.Modules.Administration;
|
||||
|
||||
public sealed class DoAsUserMessage : IUserMessage
|
||||
{
|
||||
private readonly string _message;
|
||||
private IUserMessage _msg;
|
||||
private readonly IUser _user;
|
||||
|
||||
public DoAsUserMessage(SocketUserMessage msg, IUser user, string message)
|
||||
{
|
||||
_msg = msg;
|
||||
_user = user;
|
||||
_message = message;
|
||||
}
|
||||
|
||||
public ulong Id => _msg.Id;
|
||||
|
||||
public DateTimeOffset CreatedAt => _msg.CreatedAt;
|
||||
|
||||
public Task DeleteAsync(RequestOptions? options = null)
|
||||
{
|
||||
return _msg.DeleteAsync(options);
|
||||
}
|
||||
|
||||
public Task AddReactionAsync(IEmote emote, RequestOptions? options = null)
|
||||
{
|
||||
return _msg.AddReactionAsync(emote, options);
|
||||
}
|
||||
|
||||
public Task RemoveReactionAsync(IEmote emote, IUser user, RequestOptions? options = null)
|
||||
{
|
||||
return _msg.RemoveReactionAsync(emote, user, options);
|
||||
}
|
||||
|
||||
public Task RemoveReactionAsync(IEmote emote, ulong userId, RequestOptions? options = null)
|
||||
{
|
||||
return _msg.RemoveReactionAsync(emote, userId, options);
|
||||
}
|
||||
|
||||
public Task RemoveAllReactionsAsync(RequestOptions? options = null)
|
||||
{
|
||||
return _msg.RemoveAllReactionsAsync(options);
|
||||
}
|
||||
|
||||
public Task RemoveAllReactionsForEmoteAsync(IEmote emote, RequestOptions? options = null)
|
||||
{
|
||||
return _msg.RemoveAllReactionsForEmoteAsync(emote, options);
|
||||
}
|
||||
|
||||
public IAsyncEnumerable<IReadOnlyCollection<IUser>> GetReactionUsersAsync(IEmote emoji, int limit,
|
||||
RequestOptions? options = null)
|
||||
{
|
||||
return _msg.GetReactionUsersAsync(emoji, limit, options);
|
||||
}
|
||||
|
||||
public MessageType Type => _msg.Type;
|
||||
|
||||
public MessageSource Source => _msg.Source;
|
||||
|
||||
public bool IsTTS => _msg.IsTTS;
|
||||
|
||||
public bool IsPinned => _msg.IsPinned;
|
||||
|
||||
public bool IsSuppressed => _msg.IsSuppressed;
|
||||
|
||||
public bool MentionedEveryone => _msg.MentionedEveryone;
|
||||
|
||||
public string Content => _message;
|
||||
|
||||
public string CleanContent => _msg.CleanContent;
|
||||
|
||||
public DateTimeOffset Timestamp => _msg.Timestamp;
|
||||
|
||||
public DateTimeOffset? EditedTimestamp => _msg.EditedTimestamp;
|
||||
|
||||
public IMessageChannel Channel => _msg.Channel;
|
||||
|
||||
public IUser Author => _user;
|
||||
|
||||
public IReadOnlyCollection<IAttachment> Attachments => _msg.Attachments;
|
||||
|
||||
public IReadOnlyCollection<IEmbed> Embeds => _msg.Embeds;
|
||||
|
||||
public IReadOnlyCollection<ITag> Tags => _msg.Tags;
|
||||
|
||||
public IReadOnlyCollection<ulong> MentionedChannelIds => _msg.MentionedChannelIds;
|
||||
|
||||
public IReadOnlyCollection<ulong> MentionedRoleIds => _msg.MentionedRoleIds;
|
||||
|
||||
public IReadOnlyCollection<ulong> MentionedUserIds => _msg.MentionedUserIds;
|
||||
|
||||
public MessageActivity Activity => _msg.Activity;
|
||||
|
||||
public MessageApplication Application => _msg.Application;
|
||||
|
||||
public MessageReference Reference => _msg.Reference;
|
||||
|
||||
public IReadOnlyDictionary<IEmote, ReactionMetadata> Reactions => _msg.Reactions;
|
||||
|
||||
public IReadOnlyCollection<IMessageComponent> Components => _msg.Components;
|
||||
|
||||
public IReadOnlyCollection<IStickerItem> Stickers => _msg.Stickers;
|
||||
|
||||
public MessageFlags? Flags => _msg.Flags;
|
||||
|
||||
public IMessageInteraction Interaction => _msg.Interaction;
|
||||
|
||||
public Task ModifyAsync(Action<MessageProperties> func, RequestOptions? options = null)
|
||||
{
|
||||
return _msg.ModifyAsync(func, options);
|
||||
}
|
||||
|
||||
public Task PinAsync(RequestOptions? options = null)
|
||||
{
|
||||
return _msg.PinAsync(options);
|
||||
}
|
||||
|
||||
public Task UnpinAsync(RequestOptions? options = null)
|
||||
{
|
||||
return _msg.UnpinAsync(options);
|
||||
}
|
||||
|
||||
public Task CrosspostAsync(RequestOptions? options = null)
|
||||
{
|
||||
return _msg.CrosspostAsync(options);
|
||||
}
|
||||
|
||||
public string Resolve(TagHandling userHandling = TagHandling.Name, TagHandling channelHandling = TagHandling.Name,
|
||||
TagHandling roleHandling = TagHandling.Name,
|
||||
TagHandling everyoneHandling = TagHandling.Ignore, TagHandling emojiHandling = TagHandling.Name)
|
||||
{
|
||||
return _msg.Resolve(userHandling, channelHandling, roleHandling, everyoneHandling, emojiHandling);
|
||||
}
|
||||
|
||||
public IUserMessage ReferencedMessage => _msg.ReferencedMessage;
|
||||
}
|
40
src/Ellie.Bot.Common/Ellie.Bot.Common.csproj
Normal file
40
src/Ellie.Bot.Common/Ellie.Bot.Common.csproj
Normal file
|
@ -0,0 +1,40 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Discord.Net" Version="3.104.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Ellie.Bot.Db\Ellie.Bot.Db.csproj" />
|
||||
<ProjectReference Include="..\Ellise.Common\Ellise.Common.csproj" />
|
||||
<PackageReference Include="JetBrains.Annotations" Version="2022.3.1" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="7.0.0" />
|
||||
<PackageReference Include="SixLabors.Fonts" Version="1.0.0-beta17" />
|
||||
<PackageReference Include="SixLabors.ImageSharp" Version="2.1.3" />
|
||||
<PackageReference Include="SixLabors.ImageSharp.Drawing" Version="1.0.0-beta14" />
|
||||
<PackageReference Include="SixLabors.Shapes" Version="1.0.0-beta0009" />
|
||||
<PackageReference Include="CommandLineParser" Version="2.9.1" />
|
||||
<PackageReference Include="Humanizer" Version="2.14.1">
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
<Publish>True</Publish>
|
||||
</PackageReference>
|
||||
|
||||
<PackageReference Include="Microsoft.Extensions.Http" Version="7.0.0" />
|
||||
|
||||
<ProjectReference Include="..\Ellie.Marmalade\Ellie.Marmalade.csproj" />
|
||||
|
||||
<ProjectReference Include="..\Ellie.Bot.Generators.Strings\Ellie.Bot.Generators.Strings.csproj" OutputItemType="Analyzer" />
|
||||
<ProjectReference Include="..\Ellie.Bot.Generators.Cloneable\Ellie.Bot.Generators.Cloneable.csproj" OutputItemType="Analyzer" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="..\Ellie\data\strings\responses\responses.en-US.json" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
141
src/Ellie.Bot.Common/EllieModule.cs
Normal file
141
src/Ellie.Bot.Common/EllieModule.cs
Normal file
|
@ -0,0 +1,141 @@
|
|||
#nullable disable
|
||||
using System.Globalization;
|
||||
|
||||
// ReSharper disable InconsistentNaming
|
||||
|
||||
namespace Ellie.Common;
|
||||
|
||||
[UsedImplicitly(ImplicitUseTargetFlags.Default
|
||||
| ImplicitUseTargetFlags.WithInheritors
|
||||
| ImplicitUseTargetFlags.WithMembers)]
|
||||
public abstract class EllieModule : ModuleBase
|
||||
{
|
||||
protected CultureInfo Culture { get; set; }
|
||||
|
||||
// Injected by Discord.net
|
||||
public IBotStrings Strings { get; set; }
|
||||
public ICommandHandler _cmdHandler { get; set; }
|
||||
public ILocalization _localization { get; set; }
|
||||
public IEmbedBuilderService _eb { get; set; }
|
||||
public IEllieInteractionService _inter { get; set; }
|
||||
|
||||
protected string prefix
|
||||
=> _cmdHandler.GetPrefix(ctx.Guild);
|
||||
|
||||
protected ICommandContext ctx
|
||||
=> Context;
|
||||
|
||||
protected override void BeforeExecute(CommandInfo command)
|
||||
=> Culture = _localization.GetCultureInfo(ctx.Guild?.Id);
|
||||
|
||||
protected string GetText(in LocStr data)
|
||||
=> Strings.GetText(data, Culture);
|
||||
|
||||
public Task<IUserMessage> SendErrorAsync(
|
||||
string title,
|
||||
string error,
|
||||
string url = null,
|
||||
string footer = null,
|
||||
EllieInteraction inter = null)
|
||||
=> ctx.Channel.SendErrorAsync(_eb, title, error, url, footer);
|
||||
|
||||
public Task<IUserMessage> SendConfirmAsync(
|
||||
string title,
|
||||
string text,
|
||||
string url = null,
|
||||
string footer = null)
|
||||
=> ctx.Channel.SendConfirmAsync(_eb, title, text, url, footer);
|
||||
|
||||
//
|
||||
public Task<IUserMessage> SendErrorAsync(string text, EllieInteraction inter = null)
|
||||
=> ctx.Channel.SendAsync(_eb, text, MsgType.Error, inter);
|
||||
public Task<IUserMessage> SendConfirmAsync(string text, EllieInteraction inter = null)
|
||||
=> ctx.Channel.SendAsync(_eb, text, MsgType.Ok, inter);
|
||||
public Task<IUserMessage> SendPendingAsync(string text, EllieInteraction inter = null)
|
||||
=> ctx.Channel.SendAsync(_eb, text, MsgType.Pending, inter);
|
||||
|
||||
|
||||
// localized normal
|
||||
public Task<IUserMessage> ErrorLocalizedAsync(LocStr str, EllieInteraction inter = null)
|
||||
=> SendErrorAsync(GetText(str), inter);
|
||||
|
||||
public Task<IUserMessage> PendingLocalizedAsync(LocStr str, EllieInteraction inter = null)
|
||||
=> SendPendingAsync(GetText(str), inter);
|
||||
|
||||
public Task<IUserMessage> ConfirmLocalizedAsync(LocStr str, EllieInteraction inter = null)
|
||||
=> SendConfirmAsync(GetText(str), inter);
|
||||
|
||||
// localized replies
|
||||
public Task<IUserMessage> ReplyErrorLocalizedAsync(LocStr str, EllieInteraction inter = null)
|
||||
=> SendErrorAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}", inter);
|
||||
|
||||
public Task<IUserMessage> ReplyPendingLocalizedAsync(LocStr str, EllieInteraction inter = null)
|
||||
=> SendPendingAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}", inter);
|
||||
|
||||
public Task<IUserMessage> ReplyConfirmLocalizedAsync(LocStr str, EllieInteraction inter = null)
|
||||
=> SendConfirmAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}", inter);
|
||||
|
||||
public async Task<bool> PromptUserConfirmAsync(IEmbedBuilder embed)
|
||||
{
|
||||
embed.WithPendingColor().WithFooter("yes/no");
|
||||
|
||||
var msg = await ctx.Channel.EmbedAsync(embed);
|
||||
try
|
||||
{
|
||||
var input = await GetUserInputAsync(ctx.User.Id, ctx.Channel.Id);
|
||||
input = input?.ToUpperInvariant();
|
||||
|
||||
if (input != "YES" && input != "Y")
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_ = Task.Run(() => msg.DeleteAsync());
|
||||
}
|
||||
}
|
||||
|
||||
// TypeConverter typeConverter = TypeDescriptor.GetConverter(propType); ?
|
||||
public async Task<string> GetUserInputAsync(ulong userId, ulong channelId)
|
||||
{
|
||||
var userInputTask = new TaskCompletionSource<string>();
|
||||
var dsc = (DiscordSocketClient)ctx.Client;
|
||||
try
|
||||
{
|
||||
dsc.MessageReceived += MessageReceived;
|
||||
|
||||
if (await Task.WhenAny(userInputTask.Task, Task.Delay(10000)) != userInputTask.Task)
|
||||
return null;
|
||||
|
||||
return await userInputTask.Task;
|
||||
}
|
||||
finally
|
||||
{
|
||||
dsc.MessageReceived -= MessageReceived;
|
||||
}
|
||||
|
||||
Task MessageReceived(SocketMessage arg)
|
||||
{
|
||||
_ = Task.Run(() =>
|
||||
{
|
||||
if (arg is not SocketUserMessage userMsg
|
||||
|| userMsg.Channel is not ITextChannel
|
||||
|| userMsg.Author.Id != userId
|
||||
|| userMsg.Channel.Id != channelId)
|
||||
return Task.CompletedTask;
|
||||
|
||||
if (userInputTask.TrySetResult(arg.Content))
|
||||
userMsg.DeleteAfter(1);
|
||||
|
||||
return Task.CompletedTask;
|
||||
});
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public abstract class EllieModule<TService> : EllieModule
|
||||
{
|
||||
public TService _service { get; set; }
|
||||
}
|
15
src/Ellie.Bot.Common/EllieTypeReader.cs
Normal file
15
src/Ellie.Bot.Common/EllieTypeReader.cs
Normal file
|
@ -0,0 +1,15 @@
|
|||
#nullable disable
|
||||
|
||||
namespace Ellie.Common.TypeReaders;
|
||||
|
||||
[MeansImplicitUse(ImplicitUseTargetFlags.Default | ImplicitUseTargetFlags.WithInheritors)]
|
||||
public abstract class EllieTypeReader<T> : TypeReader
|
||||
{
|
||||
public abstract ValueTask<TypeReaderResult<T>> ReadAsync(ICommandContext ctx, string input);
|
||||
|
||||
public override async Task<Discord.Commands.TypeReaderResult> ReadAsync(
|
||||
ICommandContext ctx,
|
||||
string input,
|
||||
IServiceProvider services)
|
||||
=> await ReadAsync(ctx, input);
|
||||
}
|
12
src/Ellie.Bot.Common/Extensions/DbExtensions.cs
Normal file
12
src/Ellie.Bot.Common/Extensions/DbExtensions.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
using Ellie.Db;
|
||||
using Ellie.Db.Models;
|
||||
// todo fix these namespaces. It should only be Nadeko.Bot.Db
|
||||
using Ellie.Services.Database;
|
||||
|
||||
namespace Ellie.Extensions;
|
||||
|
||||
public static class DbExtensions
|
||||
{
|
||||
public static DiscordUser GetOrCreateUser(this EllieBaseContext ctx, IUser original, Func<IQueryable<DiscordUser>, IQueryable<DiscordUser>>? includes = null)
|
||||
=> ctx.GetOrCreateUser(original.Id, original.Username, original.Discriminator, original.AvatarId, includes);
|
||||
}
|
97
src/Ellie.Bot.Common/Extensions/ImagesharpExtensions.cs
Normal file
97
src/Ellie.Bot.Common/Extensions/ImagesharpExtensions.cs
Normal file
|
@ -0,0 +1,97 @@
|
|||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Drawing;
|
||||
using SixLabors.ImageSharp.Drawing.Processing;
|
||||
using SixLabors.ImageSharp.Formats;
|
||||
using SixLabors.ImageSharp.Formats.Png;
|
||||
using SixLabors.ImageSharp.PixelFormats;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using Color = Discord.Color;
|
||||
|
||||
namespace Ellie.Extensions;
|
||||
|
||||
public static class ImagesharpExtensions
|
||||
{
|
||||
// https://github.com/SixLabors/Samples/blob/master/ImageSharp/AvatarWithRoundedCorner/Program.cs
|
||||
public static IImageProcessingContext ApplyRoundedCorners(this IImageProcessingContext ctx, float cornerRadius)
|
||||
{
|
||||
var size = ctx.GetCurrentSize();
|
||||
var corners = BuildCorners(size.Width, size.Height, cornerRadius);
|
||||
|
||||
ctx.SetGraphicsOptions(new GraphicsOptions
|
||||
{
|
||||
Antialias = true,
|
||||
// enforces that any part of this shape that has color is punched out of the background
|
||||
AlphaCompositionMode = PixelAlphaCompositionMode.DestOut
|
||||
});
|
||||
|
||||
foreach (var c in corners)
|
||||
ctx = ctx.Fill(SixLabors.ImageSharp.Color.Red, c);
|
||||
|
||||
return ctx;
|
||||
}
|
||||
|
||||
private static IPathCollection BuildCorners(int imageWidth, int imageHeight, float cornerRadius)
|
||||
{
|
||||
// first create a square
|
||||
var rect = new RectangularPolygon(-0.5f, -0.5f, cornerRadius, cornerRadius);
|
||||
|
||||
// then cut out of the square a circle so we are left with a corner
|
||||
var cornerTopLeft = rect.Clip(new EllipsePolygon(cornerRadius - 0.5f, cornerRadius - 0.5f, cornerRadius));
|
||||
|
||||
// corner is now a corner shape positions top left
|
||||
//lets make 3 more positioned correctly, we can do that by translating the original around the center of the image
|
||||
|
||||
var rightPos = imageWidth - cornerTopLeft.Bounds.Width + 1;
|
||||
var bottomPos = imageHeight - cornerTopLeft.Bounds.Height + 1;
|
||||
|
||||
// move it across the width of the image - the width of the shape
|
||||
var cornerTopRight = cornerTopLeft.RotateDegree(90).Translate(rightPos, 0);
|
||||
var cornerBottomLeft = cornerTopLeft.RotateDegree(-90).Translate(0, bottomPos);
|
||||
var cornerBottomRight = cornerTopLeft.RotateDegree(180).Translate(rightPos, bottomPos);
|
||||
|
||||
return new PathCollection(cornerTopLeft, cornerBottomLeft, cornerTopRight, cornerBottomRight);
|
||||
}
|
||||
|
||||
public static Color ToDiscordColor(this Rgba32 color)
|
||||
=> new(color.R, color.G, color.B);
|
||||
|
||||
public static MemoryStream ToStream(this Image<Rgba32> img, IImageFormat? format = null)
|
||||
{
|
||||
var imageStream = new MemoryStream();
|
||||
if (format?.Name == "GIF")
|
||||
img.SaveAsGif(imageStream);
|
||||
else
|
||||
{
|
||||
img.SaveAsPng(imageStream,
|
||||
new()
|
||||
{
|
||||
ColorType = PngColorType.RgbWithAlpha,
|
||||
CompressionLevel = PngCompressionLevel.DefaultCompression
|
||||
});
|
||||
}
|
||||
|
||||
imageStream.Position = 0;
|
||||
return imageStream;
|
||||
}
|
||||
|
||||
public static async Task<MemoryStream> ToStreamAsync(this Image<Rgba32> img, IImageFormat? format = null)
|
||||
{
|
||||
var imageStream = new MemoryStream();
|
||||
if (format?.Name == "GIF")
|
||||
{
|
||||
await img.SaveAsGifAsync(imageStream);
|
||||
}
|
||||
else
|
||||
{
|
||||
await img.SaveAsPngAsync(imageStream,
|
||||
new PngEncoder()
|
||||
{
|
||||
ColorType = PngColorType.RgbWithAlpha,
|
||||
CompressionLevel = PngCompressionLevel.DefaultCompression
|
||||
});
|
||||
}
|
||||
|
||||
imageStream.Position = 0;
|
||||
return imageStream;
|
||||
}
|
||||
}
|
31
src/Ellie.Bot.Common/GlobalUsings.cs
Normal file
31
src/Ellie.Bot.Common/GlobalUsings.cs
Normal file
|
@ -0,0 +1,31 @@
|
|||
// // global using System.Collections.Concurrent;
|
||||
global using NonBlocking;
|
||||
//
|
||||
// // packages
|
||||
global using Humanizer;
|
||||
//
|
||||
// // ellie
|
||||
global using Ellie;
|
||||
global using Ellie.Services;
|
||||
global using Ellise.Common; // new project
|
||||
global using Ellie.Common; // old + ellie specific things
|
||||
global using Ellie.Common.Attributes;
|
||||
global using Ellie.Extensions;
|
||||
|
||||
// discord
|
||||
global using Discord;
|
||||
global using Discord.Commands;
|
||||
global using Discord.Net;
|
||||
global using Discord.WebSocket;
|
||||
|
||||
// aliases
|
||||
global using GuildPerm = Discord.GuildPermission;
|
||||
global using ChannelPerm = Discord.ChannelPermission;
|
||||
global using BotPermAttribute = Discord.Commands.RequireBotPermissionAttribute;
|
||||
global using LeftoverAttribute = Discord.Commands.RemainderAttribute;
|
||||
|
||||
// non-essential
|
||||
global using JetBrains.Annotations;
|
||||
|
||||
|
||||
global using Serilog;
|
12
src/Ellie.Bot.Common/IBot.cs
Normal file
12
src/Ellie.Bot.Common/IBot.cs
Normal file
|
@ -0,0 +1,12 @@
|
|||
#nullable disable
|
||||
using Ellie.Services.Database.Models;
|
||||
|
||||
namespace Ellie;
|
||||
|
||||
public interface IBot
|
||||
{
|
||||
IReadOnlyList<ulong> GetCurrentGuildIds();
|
||||
event Func<GuildConfig, Task> JoinedGuild;
|
||||
IReadOnlyCollection<GuildConfig> AllGuildConfigs { get; }
|
||||
bool IsReady { get; }
|
||||
}
|
8
src/Ellie.Bot.Common/ICloneable.cs
Normal file
8
src/Ellie.Bot.Common/ICloneable.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
#nullable disable
|
||||
namespace Ellie.Common;
|
||||
|
||||
public interface ICloneable<T>
|
||||
where T : new()
|
||||
{
|
||||
public T Clone();
|
||||
}
|
29
src/Ellie.Bot.Common/ICurrencyProvider.cs
Normal file
29
src/Ellie.Bot.Common/ICurrencyProvider.cs
Normal file
|
@ -0,0 +1,29 @@
|
|||
using System.Globalization;
|
||||
using System.Numerics;
|
||||
|
||||
namespace Ellie.Bot.Common;
|
||||
|
||||
public interface ICurrencyProvider
|
||||
{
|
||||
string GetCurrencySign();
|
||||
}
|
||||
|
||||
public static class CurrencyHelper
|
||||
{
|
||||
public static string N<T>(T cur, IFormatProvider format)
|
||||
where T : INumber<T>
|
||||
=> cur.ToString("C0", format);
|
||||
|
||||
public static string N<T>(T cur, CultureInfo culture, string currencySign)
|
||||
where T : INumber<T>
|
||||
=> N(cur, GetCurrencyFormat(culture, currencySign));
|
||||
|
||||
private static IFormatProvider GetCurrencyFormat(CultureInfo culture, string currencySign)
|
||||
{
|
||||
var flowersCurrencyCulture = (CultureInfo)culture.Clone();
|
||||
flowersCurrencyCulture.NumberFormat.CurrencySymbol = currencySign;
|
||||
flowersCurrencyCulture.NumberFormat.CurrencyNegativePattern = 5;
|
||||
|
||||
return flowersCurrencyCulture;
|
||||
}
|
||||
}
|
7
src/Ellie.Bot.Common/IDiscordPermOverrideService.cs
Normal file
7
src/Ellie.Bot.Common/IDiscordPermOverrideService.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
#nullable disable
|
||||
namespace Ellise.Common;
|
||||
|
||||
public interface IDiscordPermOverrideService
|
||||
{
|
||||
bool TryGetOverrides(ulong guildId, string commandName, out Ellie.Bot.Db.GuildPerm? perm);
|
||||
}
|
7
src/Ellie.Bot.Common/IEllieCommandOptions.cs
Normal file
7
src/Ellie.Bot.Common/IEllieCommandOptions.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
#nullable disable
|
||||
namespace Ellie.Common;
|
||||
|
||||
public interface IEllieCommandOptions
|
||||
{
|
||||
void NormalizeOptions();
|
||||
}
|
35
src/Ellie.Bot.Common/ILogCommandService.cs
Normal file
35
src/Ellie.Bot.Common/ILogCommandService.cs
Normal file
|
@ -0,0 +1,35 @@
|
|||
using Ellie.Services.Database.Models;
|
||||
|
||||
namespace Ellie.Common;
|
||||
|
||||
public interface ILogCommandService
|
||||
{
|
||||
void AddDeleteIgnore(ulong xId);
|
||||
Task LogServer(ulong guildId, ulong channelId, bool actionValue);
|
||||
bool LogIgnore(ulong guildId, ulong itemId, IgnoredItemType itemType);
|
||||
LogSetting? GetGuildLogSettings(ulong guildId);
|
||||
bool Log(ulong guildId, ulong? channelId, LogType type);
|
||||
}
|
||||
|
||||
public enum LogType
|
||||
{
|
||||
Other,
|
||||
MessageUpdated,
|
||||
MessageDeleted,
|
||||
UserJoined,
|
||||
UserLeft,
|
||||
UserBanned,
|
||||
UserUnbanned,
|
||||
UserUpdated,
|
||||
ChannelCreated,
|
||||
ChannelDestroyed,
|
||||
ChannelUpdated,
|
||||
UserPresence,
|
||||
VoicePresence,
|
||||
VoicePresenceTts,
|
||||
UserMuted,
|
||||
UserWarned,
|
||||
|
||||
ThreadDeleted,
|
||||
ThreadCreated
|
||||
}
|
13
src/Ellie.Bot.Common/IPermissionChecker.cs
Normal file
13
src/Ellie.Bot.Common/IPermissionChecker.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
using OneOf;
|
||||
using OneOf.Types;
|
||||
|
||||
namespace Ellie.Bot.Common;
|
||||
|
||||
public interface IPermissionChecker
|
||||
{
|
||||
Task<OneOf<Success, Error<LocStr>>> CheckAsync(IGuild guild,
|
||||
IMessageChannel channel,
|
||||
IUser author,
|
||||
string module,
|
||||
string? cmd);
|
||||
}
|
7
src/Ellie.Bot.Common/IPlaceholderProvider.cs
Normal file
7
src/Ellie.Bot.Common/IPlaceholderProvider.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
#nullable disable
|
||||
namespace Ellie.Common;
|
||||
|
||||
public interface IPlaceholderProvider
|
||||
{
|
||||
public IEnumerable<(string Name, Func<string> Func)> GetPlaceholders();
|
||||
}
|
82
src/Ellie.Bot.Common/Interaction/EllieInteraction.cs
Normal file
82
src/Ellie.Bot.Common/Interaction/EllieInteraction.cs
Normal file
|
@ -0,0 +1,82 @@
|
|||
namespace Ellie;
|
||||
|
||||
public sealed class EllieInteraction
|
||||
{
|
||||
private readonly ulong _authorId;
|
||||
private readonly ButtonBuilder _button;
|
||||
private readonly Func<SocketMessageComponent, Task> _onClick;
|
||||
private readonly bool _onlyAuthor;
|
||||
public DiscordSocketClient Client { get; }
|
||||
|
||||
private readonly TaskCompletionSource<bool> _interactionCompletedSource;
|
||||
|
||||
private IUserMessage message = null!;
|
||||
|
||||
public EllieInteraction(DiscordSocketClient client,
|
||||
ulong authorId,
|
||||
ButtonBuilder button,
|
||||
Func<SocketMessageComponent, Task> onClick,
|
||||
bool onlyAuthor)
|
||||
{
|
||||
_authorId = authorId;
|
||||
_button = button;
|
||||
_onClick = onClick;
|
||||
_onlyAuthor = onlyAuthor;
|
||||
_interactionCompletedSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
|
||||
|
||||
Client = client;
|
||||
}
|
||||
|
||||
public async Task RunAsync(IUserMessage msg)
|
||||
{
|
||||
message = msg;
|
||||
|
||||
Client.InteractionCreated += OnInteraction;
|
||||
await Task.WhenAny(Task.Delay(15_000), _interactionCompletedSource.Task);
|
||||
Client.InteractionCreated -= OnInteraction;
|
||||
|
||||
await msg.ModifyAsync(m => m.Components = new ComponentBuilder().Build());
|
||||
}
|
||||
|
||||
private Task OnInteraction(SocketInteraction arg)
|
||||
{
|
||||
if (arg is not SocketMessageComponent smc)
|
||||
return Task.CompletedTask;
|
||||
|
||||
if (smc.Message.Id != message.Id)
|
||||
return Task.CompletedTask;
|
||||
|
||||
if (_onlyAuthor && smc.User.Id != _authorId)
|
||||
return Task.CompletedTask;
|
||||
|
||||
if (smc.Data.CustomId != _button.CustomId)
|
||||
return Task.CompletedTask;
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
await ExecuteOnActionAsync(smc);
|
||||
|
||||
// this should only be a thing on single-response buttons
|
||||
_interactionCompletedSource.TrySetResult(true);
|
||||
|
||||
if (!smc.HasResponded)
|
||||
{
|
||||
await smc.DeferAsync();
|
||||
}
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
public MessageComponent CreateComponent()
|
||||
{
|
||||
var comp = new ComponentBuilder()
|
||||
.WithButton(_button);
|
||||
|
||||
return comp.Build();
|
||||
}
|
||||
|
||||
public Task ExecuteOnActionAsync(SocketMessageComponent smc)
|
||||
=> _onClick(smc);
|
||||
}
|
8
src/Ellie.Bot.Common/Interaction/EllieInteractionData.cs
Normal file
8
src/Ellie.Bot.Common/Interaction/EllieInteractionData.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
namespace Ellie;
|
||||
|
||||
/// <summary>
|
||||
/// Represents essential interacation data
|
||||
/// </summary>
|
||||
/// <param name="Emote">Emote which will show on a button</param>
|
||||
/// <param name="CustomId">Custom interaction id</param>
|
||||
public record EllieInteractionData(IEmote Emote, string CustomId, string? Text = null);
|
20
src/Ellie.Bot.Common/Interaction/EllieInteractionService.cs
Normal file
20
src/Ellie.Bot.Common/Interaction/EllieInteractionService.cs
Normal file
|
@ -0,0 +1,20 @@
|
|||
namespace Ellie;
|
||||
|
||||
public class EllieInteractionService : IEllieInteractionService, IEService
|
||||
{
|
||||
private readonly DiscordSocketClient _client;
|
||||
|
||||
public EllieInteractionService(DiscordSocketClient client)
|
||||
{
|
||||
_client = client;
|
||||
}
|
||||
|
||||
public EllieInteraction Create<T>(
|
||||
ulong userId,
|
||||
SimpleInteraction<T> inter)
|
||||
=> new EllieInteraction(_client,
|
||||
userId,
|
||||
inter.Button,
|
||||
inter.TriggerAsync,
|
||||
onlyAuthor: true);
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
namespace Ellie;
|
||||
|
||||
public interface IEllieInteractionService
|
||||
{
|
||||
public EllieInteraction Create<T>(
|
||||
ulong userId,
|
||||
SimpleInteraction<T> inter);
|
||||
}
|
20
src/Ellie.Bot.Common/Interaction/SimpleInteraction.cs
Normal file
20
src/Ellie.Bot.Common/Interaction/SimpleInteraction.cs
Normal file
|
@ -0,0 +1,20 @@
|
|||
namespace Ellie;
|
||||
|
||||
public class SimpleInteraction<T>
|
||||
{
|
||||
public ButtonBuilder Button { get; }
|
||||
private readonly Func<SocketMessageComponent, T, Task> _onClick;
|
||||
private readonly T? _state;
|
||||
|
||||
public SimpleInteraction(ButtonBuilder button, Func<SocketMessageComponent, T?, Task> onClick, T? state = default)
|
||||
{
|
||||
Button = button;
|
||||
_onClick = onClick;
|
||||
_state = state;
|
||||
}
|
||||
|
||||
public async Task TriggerAsync(SocketMessageComponent smc)
|
||||
{
|
||||
await _onClick(smc, _state!);
|
||||
}
|
||||
}
|
24
src/Ellie.Bot.Common/Marmalade/IMarmaladeLoaderService.cs
Normal file
24
src/Ellie.Bot.Common/Marmalade/IMarmaladeLoaderService.cs
Normal file
|
@ -0,0 +1,24 @@
|
|||
using System.Globalization;
|
||||
|
||||
namespace Ellie.Marmalade;
|
||||
|
||||
public interface IMarmaladeLoaderSevice
|
||||
{
|
||||
Task<MarmaladeLoadResult> LoadMarmaladeAsync(string marmaladeName);
|
||||
Task<MarmaladeUnloadResult> UnloadMarmaladeAsync(string marmaladeName);
|
||||
string GetCommandDescription(string marmaladeName, string commandName, CultureInfo culture);
|
||||
string[] GetCommandExampleArgs(string marmaladeName, string commandName, CultureInfo culture);
|
||||
Task ReloadStrings();
|
||||
IReadOnlyCollection<string> GetAllMarmalades();
|
||||
IReadOnlyCollection<MarmaladeStats> GetLoadedMarmalades(CultureInfo? cultureInfo = null);
|
||||
}
|
||||
|
||||
public sealed record MarmaladeStats(string Name,
|
||||
string? Description,
|
||||
IReadOnlyCollection<CanaryStats> Canaries);
|
||||
|
||||
public sealed record CanaryStats(string Name,
|
||||
string? Prefix,
|
||||
IReadOnlyCollection<CanaryCommandStats> Commands);
|
||||
|
||||
public sealed record CanaryCommandStats(string Name);
|
10
src/Ellie.Bot.Common/Marmalade/MarmaladeLoadResult.cs
Normal file
10
src/Ellie.Bot.Common/Marmalade/MarmaladeLoadResult.cs
Normal file
|
@ -0,0 +1,10 @@
|
|||
namespace Ellie.Marmalade;
|
||||
|
||||
public enum MarmaladeLoadResult
|
||||
{
|
||||
Success,
|
||||
NotFound,
|
||||
AlreadyLoaded,
|
||||
Empty,
|
||||
UnknownError,
|
||||
}
|
9
src/Ellie.Bot.Common/Marmalade/MarmaladeUnloadResult.cs
Normal file
9
src/Ellie.Bot.Common/Marmalade/MarmaladeUnloadResult.cs
Normal file
|
@ -0,0 +1,9 @@
|
|||
namespace Ellie.Marmalade;
|
||||
|
||||
public enum MarmaladeUnloadResult
|
||||
{
|
||||
Success,
|
||||
NotLoaded,
|
||||
PossiblyUnable,
|
||||
NotFound,
|
||||
}
|
8
src/Ellie.Bot.Common/MessageType.cs
Normal file
8
src/Ellie.Bot.Common/MessageType.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
namespace Ellie.Common;
|
||||
|
||||
public enum MsgType
|
||||
{
|
||||
Ok,
|
||||
Pending,
|
||||
Error
|
||||
}
|
6
src/Ellie.Bot.Common/ModuleBehaviors/IBehavior.cs
Normal file
6
src/Ellie.Bot.Common/ModuleBehaviors/IBehavior.cs
Normal file
|
@ -0,0 +1,6 @@
|
|||
namespace Ellie.Common.ModuleBehaviors;
|
||||
|
||||
public interface IBehavior
|
||||
{
|
||||
public virtual string Name => this.GetType().Name;
|
||||
}
|
19
src/Ellie.Bot.Common/ModuleBehaviors/IExecNoCommand.cs
Normal file
19
src/Ellie.Bot.Common/ModuleBehaviors/IExecNoCommand.cs
Normal file
|
@ -0,0 +1,19 @@
|
|||
namespace Ellie.Common.ModuleBehaviors;
|
||||
|
||||
/// <summary>
|
||||
/// Executed if no command was found for this message
|
||||
/// </summary>
|
||||
public interface IExecNoCommand : IBehavior
|
||||
{
|
||||
/// <summary>
|
||||
/// Executed at the end of the lifecycle if no command was found
|
||||
/// <see cref="IExecOnMessage"/> →
|
||||
/// <see cref="IInputTransformer"/> →
|
||||
/// <see cref="IExecPreCommand"/> →
|
||||
/// [<see cref="IExecPostCommand"/> | *<see cref="IExecNoCommand"/>*]
|
||||
/// </summary>
|
||||
/// <param name="guild"></param>
|
||||
/// <param name="msg"></param>
|
||||
/// <returns>A task representing completion</returns>
|
||||
Task ExecOnNoCommandAsync(IGuild guild, IUserMessage msg);
|
||||
}
|
21
src/Ellie.Bot.Common/ModuleBehaviors/IExecOnMessage.cs
Normal file
21
src/Ellie.Bot.Common/ModuleBehaviors/IExecOnMessage.cs
Normal file
|
@ -0,0 +1,21 @@
|
|||
namespace Ellie.Common.ModuleBehaviors;
|
||||
|
||||
/// <summary>
|
||||
/// Implemented by modules to handle non-bot messages received
|
||||
/// </summary>
|
||||
public interface IExecOnMessage : IBehavior
|
||||
{
|
||||
int Priority { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Ran after a non-bot message was received
|
||||
/// *<see cref="IExecOnMessage"/>* →
|
||||
/// <see cref="IInputTransformer"/> →
|
||||
/// <see cref="IExecPreCommand"/> →
|
||||
/// [<see cref="IExecPostCommand"/> | <see cref="IExecNoCommand"/>]
|
||||
/// </summary>
|
||||
/// <param name="guild">Guild where the message was sent</param>
|
||||
/// <param name="msg">The message that was received</param>
|
||||
/// <returns>Whether further processing of this message should be blocked</returns>
|
||||
Task<bool> ExecOnMessageAsync(IGuild guild, IUserMessage msg);
|
||||
}
|
22
src/Ellie.Bot.Common/ModuleBehaviors/IExecPostCommand.cs
Normal file
22
src/Ellie.Bot.Common/ModuleBehaviors/IExecPostCommand.cs
Normal file
|
@ -0,0 +1,22 @@
|
|||
namespace Ellie.Common.ModuleBehaviors;
|
||||
|
||||
/// <summary>
|
||||
/// This interface's method is executed after the command successfully finished execution.
|
||||
/// ***There is no support for this method in NadekoBot services.***
|
||||
/// It is only meant to be used in medusa system
|
||||
/// </summary>
|
||||
public interface IExecPostCommand : IBehavior
|
||||
{
|
||||
/// <summary>
|
||||
/// Executed after a command was successfully executed
|
||||
/// <see cref="IExecOnMessage"/> →
|
||||
/// <see cref="IInputTransformer"/> →
|
||||
/// <see cref="IExecPreCommand"/> →
|
||||
/// [*<see cref="IExecPostCommand"/>* | <see cref="IExecNoCommand"/>]
|
||||
/// </summary>
|
||||
/// <param name="ctx">Command context</param>
|
||||
/// <param name="moduleName">Module name</param>
|
||||
/// <param name="commandName">Command name</param>
|
||||
/// <returns>A task representing completion</returns>
|
||||
ValueTask ExecPostCommandAsync(ICommandContext ctx, string moduleName, string commandName);
|
||||
}
|
25
src/Ellie.Bot.Common/ModuleBehaviors/IExecPreCommand.cs
Normal file
25
src/Ellie.Bot.Common/ModuleBehaviors/IExecPreCommand.cs
Normal file
|
@ -0,0 +1,25 @@
|
|||
namespace Ellie.Common.ModuleBehaviors;
|
||||
|
||||
/// <summary>
|
||||
/// This interface's method is executed after a command was found but before it was executed.
|
||||
/// Able to block further processing of a command
|
||||
/// </summary>
|
||||
public interface IExecPreCommand : IBehavior
|
||||
{
|
||||
public int Priority { get; }
|
||||
|
||||
/// <summary>
|
||||
/// <para>
|
||||
/// Ran after a command was found but before execution.
|
||||
/// </para>
|
||||
/// <see cref="IExecOnMessage"/> →
|
||||
/// <see cref="IInputTransformer"/> →
|
||||
/// *<see cref="IExecPreCommand"/>* →
|
||||
/// [<see cref="IExecPostCommand"/> | <see cref="IExecNoCommand"/>]
|
||||
/// </summary>
|
||||
/// <param name="context">Command context</param>
|
||||
/// <param name="moduleName">Name of the module</param>
|
||||
/// <param name="command">Command info</param>
|
||||
/// <returns>Whether further processing of the command is blocked</returns>
|
||||
Task<bool> ExecPreCommandAsync(ICommandContext context, string moduleName, CommandInfo command);
|
||||
}
|
25
src/Ellie.Bot.Common/ModuleBehaviors/IInputTransformer.cs
Normal file
25
src/Ellie.Bot.Common/ModuleBehaviors/IInputTransformer.cs
Normal file
|
@ -0,0 +1,25 @@
|
|||
namespace Ellie.Common.ModuleBehaviors;
|
||||
|
||||
/// <summary>
|
||||
/// Implemented by services which may transform input before a command is searched for
|
||||
/// </summary>
|
||||
public interface IInputTransformer : IBehavior
|
||||
{
|
||||
/// <summary>
|
||||
/// Ran after a non-bot message was received
|
||||
/// <see cref="IExecOnMessage"/> ->
|
||||
/// *<see cref="IInputTransformer"/>* ->
|
||||
/// <see cref="IExecPreCommand"/> ->
|
||||
/// [<see cref="IExecPostCommand"/> OR <see cref="IExecNoCommand"/>]
|
||||
/// </summary>
|
||||
/// <param name="guild">Guild</param>
|
||||
/// <param name="channel">Channel in which the message was sent</param>
|
||||
/// <param name="user">User who sent the message</param>
|
||||
/// <param name="input">Content of the message</param>
|
||||
/// <returns>New input, if any, otherwise null</returns>
|
||||
Task<string?> TransformInput(
|
||||
IGuild guild,
|
||||
IMessageChannel channel,
|
||||
IUser user,
|
||||
string input);
|
||||
}
|
13
src/Ellie.Bot.Common/ModuleBehaviors/IReadyExecutor.cs
Normal file
13
src/Ellie.Bot.Common/ModuleBehaviors/IReadyExecutor.cs
Normal file
|
@ -0,0 +1,13 @@
|
|||
namespace Ellie.Common.ModuleBehaviors;
|
||||
|
||||
/// <summary>
|
||||
/// All services which need to execute something after
|
||||
/// the bot is ready should implement this interface
|
||||
/// </summary>
|
||||
public interface IReadyExecutor : IBehavior
|
||||
{
|
||||
/// <summary>
|
||||
/// Executed when bot is ready
|
||||
/// </summary>
|
||||
public Task OnReadyAsync();
|
||||
}
|
7
src/Ellie.Bot.Common/Patronage/FeatureLimitKey.cs
Normal file
7
src/Ellie.Bot.Common/Patronage/FeatureLimitKey.cs
Normal file
|
@ -0,0 +1,7 @@
|
|||
namespace Ellie.Modules.Patronage;
|
||||
|
||||
public readonly struct FeatureLimitKey
|
||||
{
|
||||
public string PrettyName { get; init; }
|
||||
public string Key { get; init; }
|
||||
}
|
8
src/Ellie.Bot.Common/Patronage/FeatureQuotaStats.cs
Normal file
8
src/Ellie.Bot.Common/Patronage/FeatureQuotaStats.cs
Normal file
|
@ -0,0 +1,8 @@
|
|||
namespace Ellie.Modules.Patronage;
|
||||
|
||||
public readonly struct FeatureQuotaStats
|
||||
{
|
||||
public (uint Cur, uint Max) Hourly { get; init; }
|
||||
public (uint Cur, uint Max) Daily { get; init; }
|
||||
public (uint Cur, uint Max) Monthly { get; init; }
|
||||
}
|
11
src/Ellie.Bot.Common/Patronage/IPatronData.cs
Normal file
11
src/Ellie.Bot.Common/Patronage/IPatronData.cs
Normal file
|
@ -0,0 +1,11 @@
|
|||
namespace Ellie.Modules.Patronage;
|
||||
|
||||
public interface ISubscriberData
|
||||
{
|
||||
public string UniquePlatformUserId { get; }
|
||||
public ulong UserId { get; }
|
||||
public int Cents { get; }
|
||||
|
||||
public DateTime? LastCharge { get; }
|
||||
public SubscriptionChargeStatus ChargeStatus { get; }
|
||||
}
|
56
src/Ellie.Bot.Common/Patronage/IPatronageService.cs
Normal file
56
src/Ellie.Bot.Common/Patronage/IPatronageService.cs
Normal file
|
@ -0,0 +1,56 @@
|
|||
using Ellie.Db.Models;
|
||||
using OneOf;
|
||||
|
||||
namespace Ellie.Modules.Patronage;
|
||||
|
||||
/// <summary>
|
||||
/// Manages patrons and provides access to their data
|
||||
/// </summary>
|
||||
public interface IPatronageService
|
||||
{
|
||||
/// <summary>
|
||||
/// Called when the payment is made.
|
||||
/// Either as a single payment for that patron,
|
||||
/// or as a recurring monthly donation.
|
||||
/// </summary>
|
||||
public event Func<Patron, Task> OnNewPatronPayment;
|
||||
|
||||
/// <summary>
|
||||
/// Called when the patron changes the pledge amount
|
||||
/// (Patron old, Patron new) => Task
|
||||
/// </summary>
|
||||
public event Func<Patron, Patron, Task> OnPatronUpdated;
|
||||
|
||||
/// <summary>
|
||||
/// Called when the patron refunds the purchase or it's marked as fraud
|
||||
/// </summary>
|
||||
public event Func<Patron, Task> OnPatronRefunded;
|
||||
|
||||
/// <summary>
|
||||
/// Gets a Patron with the specified userId
|
||||
/// </summary>
|
||||
/// <param name="userId">UserId for which to get the patron data for.</param>
|
||||
/// <returns>A patron with the specifeid userId</returns>
|
||||
public Task<Patron> GetPatronAsync(ulong userId);
|
||||
|
||||
/// <summary>
|
||||
/// Gets the quota statistic for the user/patron specified by the userId
|
||||
/// </summary>
|
||||
/// <param name="userId">UserId of the user for which to get the quota statistic for</param>
|
||||
/// <returns>Quota stats for the specified user</returns>
|
||||
Task<UserQuotaStats> GetUserQuotaStatistic(ulong userId);
|
||||
|
||||
|
||||
Task<FeatureLimit> TryGetFeatureLimitAsync(FeatureLimitKey key, ulong userId, int? defaultValue);
|
||||
|
||||
ValueTask<OneOf<(uint Hourly, uint Daily, uint Monthly), QuotaLimit>> TryIncrementQuotaCounterAsync(
|
||||
ulong userId,
|
||||
bool isSelf,
|
||||
FeatureType featureType,
|
||||
string featureName,
|
||||
uint? maybeHourly,
|
||||
uint? maybeDaily,
|
||||
uint? maybeMonthly);
|
||||
|
||||
PatronConfigData GetConfig();
|
||||
}
|
16
src/Ellie.Bot.Common/Patronage/ISubscriptionHandler.cs
Normal file
16
src/Ellie.Bot.Common/Patronage/ISubscriptionHandler.cs
Normal file
|
@ -0,0 +1,16 @@
|
|||
#nullable disable
|
||||
namespace Ellie.Modules.Patronage;
|
||||
|
||||
/// <summary>
|
||||
/// Services implementing this interface are handling pledges/subscriptions/payments coming
|
||||
/// from a payment platform.
|
||||
/// </summary>
|
||||
public interface ISubscriptionHandler
|
||||
{
|
||||
/// <summary>
|
||||
/// Get Current patrons in batches.
|
||||
/// This will only return patrons who have their discord account connected
|
||||
/// </summary>
|
||||
/// <returns>Batched patrons</returns>
|
||||
public IAsyncEnumerable<IReadOnlyCollection<ISubscriberData>> GetPatronsAsync();
|
||||
}
|
38
src/Ellie.Bot.Common/Patronage/Patron.cs
Normal file
38
src/Ellie.Bot.Common/Patronage/Patron.cs
Normal file
|
@ -0,0 +1,38 @@
|
|||
namespace Ellie.Modules.Patronage;
|
||||
|
||||
public readonly struct Patron
|
||||
{
|
||||
/// <summary>
|
||||
/// Unique id assigned to this patron by the payment platform
|
||||
/// </summary>
|
||||
public string UniquePlatformUserId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Discord UserId to which this <see cref="UniquePlatformUserId"/> is connected to
|
||||
/// </summary>
|
||||
public ulong UserId { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Amount the Patron is currently pledging or paid
|
||||
/// </summary>
|
||||
public int Amount { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Current Tier of the patron
|
||||
/// (do not question it in consumer classes, as the calculation should be always internal and may change)
|
||||
/// </summary>
|
||||
public PatronTier Tier { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When was the last time this <see cref="Amount"/> was paid
|
||||
/// </summary>
|
||||
public DateTime PaidAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// After which date does the user's Patronage benefit end
|
||||
/// </summary>
|
||||
public DateTime ValidThru { get; init; }
|
||||
|
||||
public bool IsActive
|
||||
=> !ValidThru.IsBeforeToday();
|
||||
}
|
37
src/Ellie.Bot.Common/Patronage/PatronConfigData.cs
Normal file
37
src/Ellie.Bot.Common/Patronage/PatronConfigData.cs
Normal file
|
@ -0,0 +1,37 @@
|
|||
using Ellie.Common.Yml;
|
||||
using Cloneable;
|
||||
|
||||
namespace Ellie.Modules.Patronage;
|
||||
|
||||
[Cloneable]
|
||||
public partial class PatronConfigData : ICloneable<PatronConfigData>
|
||||
{
|
||||
[Comment("DO NOT CHANGE")]
|
||||
public int Version { get; set; } = 2;
|
||||
|
||||
[Comment("Whether the patronage feature is enabled")]
|
||||
public bool IsEnabled { get; set; }
|
||||
|
||||
[Comment("List of patron only features and relevant quota data")]
|
||||
public FeatureQuotas Quotas { get; set; }
|
||||
|
||||
public PatronConfigData()
|
||||
{
|
||||
Quotas = new();
|
||||
}
|
||||
|
||||
public class FeatureQuotas
|
||||
{
|
||||
[Comment("Dictionary of feature names with their respective limits. Set to null for unlimited")]
|
||||
public Dictionary<string, Dictionary<PatronTier, int?>> Features { get; set; } = new();
|
||||
|
||||
[Comment("Dictionary of commands with their respective quota data")]
|
||||
public Dictionary<string, Dictionary<PatronTier, Dictionary<QuotaPer, uint>?>> Commands { get; set; } = new();
|
||||
|
||||
[Comment("Dictionary of groups with their respective quota data")]
|
||||
public Dictionary<string, Dictionary<PatronTier, Dictionary<QuotaPer, uint>?>> Groups { get; set; } = new();
|
||||
|
||||
[Comment("Dictionary of modules with their respective quota data")]
|
||||
public Dictionary<string, Dictionary<PatronTier, Dictionary<QuotaPer, uint>?>> Modules { get; set; } = new();
|
||||
}
|
||||
}
|
33
src/Ellie.Bot.Common/Patronage/PatronExtensions.cs
Normal file
33
src/Ellie.Bot.Common/Patronage/PatronExtensions.cs
Normal file
|
@ -0,0 +1,33 @@
|
|||
namespace Ellie.Modules.Patronage;
|
||||
|
||||
public static class PatronExtensions
|
||||
{
|
||||
public static string ToFullName(this PatronTier tier)
|
||||
=> tier switch
|
||||
{
|
||||
_ => $"Patron Tier {tier}",
|
||||
};
|
||||
|
||||
public static string ToFullName(this QuotaPer per)
|
||||
=> per.Humanize(LetterCasing.LowerCase);
|
||||
|
||||
public static DateTime DayOfNextMonth(this DateTime date, int day)
|
||||
{
|
||||
var nextMonth = date.AddMonths(1);
|
||||
var dt = DateTime.SpecifyKind(new(nextMonth.Year, nextMonth.Month, day), DateTimeKind.Utc);
|
||||
return dt;
|
||||
}
|
||||
|
||||
public static DateTime FirstOfNextMonth(this DateTime date)
|
||||
=> date.DayOfNextMonth(1);
|
||||
|
||||
public static DateTime SecondOfNextMonth(this DateTime date)
|
||||
=> date.DayOfNextMonth(2);
|
||||
|
||||
public static string ToShortAndRelativeTimestampTag(this DateTime date)
|
||||
{
|
||||
var fullResetStr = TimestampTag.FromDateTime(date, TimestampTagStyles.ShortDateTime);
|
||||
var relativeResetStr = TimestampTag.FromDateTime(date, TimestampTagStyles.Relative);
|
||||
return $"{fullResetStr}\n{relativeResetStr}";
|
||||
}
|
||||
}
|
14
src/Ellie.Bot.Common/Patronage/PatronTier.cs
Normal file
14
src/Ellie.Bot.Common/Patronage/PatronTier.cs
Normal file
|
@ -0,0 +1,14 @@
|
|||
// ReSharper disable InconsistentNaming
|
||||
namespace Ellie.Modules.Patronage;
|
||||
|
||||
public enum PatronTier
|
||||
{
|
||||
None,
|
||||
I,
|
||||
V,
|
||||
X,
|
||||
XX,
|
||||
L,
|
||||
C,
|
||||
ComingSoon
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue