Full system rewrite incoming
This commit is contained in:
parent
0fecee1265
commit
4cde58b3e2
756 changed files with 26 additions and 66237 deletions
|
@ -1,78 +0,0 @@
|
|||
#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; }
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
namespace Ellie;
|
||||
|
||||
public interface IBotCredsProvider
|
||||
{
|
||||
public void Reload();
|
||||
public IBotCredentials GetCreds();
|
||||
public void ModifyCredsFile(Action<IBotCredentials> func);
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
#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; }
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
#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);
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
#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);
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
#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);
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
#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();
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
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))
|
||||
{
|
||||
}
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
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();
|
||||
}
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
#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
|
||||
{
|
||||
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
namespace Ellie.Common.Attributes;
|
||||
|
||||
[AttributeUsage(AttributeTargets.Method)]
|
||||
public sealed class EllieOptionsAttribute<TOption> : Attribute
|
||||
where TOption: IEllieCommandOptions
|
||||
{
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
#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
|
||||
}
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
#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_ELLIE || DEBUG
|
||||
return Task.FromResult(PreconditionResult.FromSuccess());
|
||||
#else
|
||||
return Task.FromResult(PreconditionResult.FromError("Only available on the public bot."));
|
||||
#endif
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
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"));
|
||||
}
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
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);
|
||||
}
|
||||
}
|
|
@ -1,30 +0,0 @@
|
|||
#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));
|
||||
}
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
#nullable disable
|
||||
namespace Ellie.Common;
|
||||
|
||||
public abstract class CleanupModuleBase : EllieModule
|
||||
{
|
||||
protected async Task ConfirmActionInternalAsync(string name, Func<Task> action)
|
||||
{
|
||||
try
|
||||
{
|
||||
var embed = _eb.Create()
|
||||
.WithTitle(GetText(strs.sql_confirm_exec))
|
||||
.WithDescription(name);
|
||||
|
||||
if (!await PromptUserConfirmAsync(embed))
|
||||
return;
|
||||
|
||||
await action();
|
||||
await ctx.OkAsync();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
await SendErrorAsync(ex.ToString());
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
#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";
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -1,203 +0,0 @@
|
|||
#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
|
||||
}
|
|
@ -1,18 +0,0 @@
|
|||
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);
|
||||
}
|
|
@ -1,272 +0,0 @@
|
|||
#nullable disable
|
||||
using Ellie.Common.Yml;
|
||||
|
||||
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; }
|
||||
}
|
|
@ -1,5 +0,0 @@
|
|||
namespace Ellie.Services.Currency;
|
||||
|
||||
public enum CurrencyType{
|
||||
Default
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
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);
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
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);
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
using Ellie.Services.Currency;
|
||||
|
||||
namespace Ellie.Services;
|
||||
|
||||
public interface ITxTracker
|
||||
{
|
||||
Task TrackAdd(long amount, TxData? txData);
|
||||
Task TrackRemove(long amount, TxData? txData);
|
||||
}
|
|
@ -1,40 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
namespace Ellie.Services.Currency;
|
||||
|
||||
public record class TxData(
|
||||
string Type,
|
||||
string Extra,
|
||||
string? Note = "",
|
||||
ulong? OtherId = null);
|
|
@ -1,18 +0,0 @@
|
|||
#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();
|
||||
}
|
|
@ -1,141 +0,0 @@
|
|||
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 IThreadChannel Thread => _msg.Thread;
|
||||
|
||||
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 MessageRoleSubscriptionData RoleSubscriptionData => _msg.RoleSubscriptionData;
|
||||
|
||||
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;
|
||||
}
|
|
@ -34,7 +34,8 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AdditionalFiles Include="..\Ellie\data\strings\responses\responses.en-US.json" />
|
||||
<AdditionalFiles Include="..\Ellie\data\strings\responses\responses.en-US.json">
|
||||
<Link>responses.en-US.json</Link>
|
||||
</AdditionalFiles>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -1,143 +0,0 @@
|
|||
#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; }
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
#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);
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
using Microsoft.EntityFrameworkCore;
|
||||
using Ellie.Db;
|
||||
using Ellie.Db.Models;
|
||||
// todo fix these namespaces. It should only be Ellie.Bot.Db
|
||||
using Ellie.Services.Database;
|
||||
|
||||
namespace Ellie.Extensions;
|
||||
|
||||
public static class DbExtensions
|
||||
{
|
||||
public static DiscordUser GetOrCreateUser(this DbContext ctx, IUser original, Func<IQueryable<DiscordUser>, IQueryable<DiscordUser>>? includes = null)
|
||||
=> ctx.GetOrCreateUser(original.Id, original.Username, original.Discriminator, original.AvatarId, includes);
|
||||
}
|
|
@ -1,97 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -1,31 +0,0 @@
|
|||
// // 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;
|
|
@ -1,12 +0,0 @@
|
|||
#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; }
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
#nullable disable
|
||||
namespace Ellie.Common;
|
||||
|
||||
public interface ICloneable<T>
|
||||
where T : new()
|
||||
{
|
||||
public T Clone();
|
||||
}
|
|
@ -1,29 +0,0 @@
|
|||
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;
|
||||
}
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
#nullable disable
|
||||
namespace Ellise.Common;
|
||||
|
||||
public interface IDiscordPermOverrideService
|
||||
{
|
||||
bool TryGetOverrides(ulong guildId, string commandName, out Ellie.Bot.Db.GuildPerm? perm);
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
#nullable disable
|
||||
namespace Ellie.Common;
|
||||
|
||||
public interface IEllieCommandOptions
|
||||
{
|
||||
void NormalizeOptions();
|
||||
}
|
|
@ -1,35 +0,0 @@
|
|||
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
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
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);
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
#nullable disable
|
||||
namespace Ellie.Common;
|
||||
|
||||
public interface IPlaceholderProvider
|
||||
{
|
||||
public IEnumerable<(string Name, Func<string> Func)> GetPlaceholders();
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
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);
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
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);
|
|
@ -1,20 +0,0 @@
|
|||
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);
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
namespace Ellie;
|
||||
|
||||
public interface IEllieInteractionService
|
||||
{
|
||||
public EllieInteraction Create<T>(
|
||||
ulong userId,
|
||||
SimpleInteraction<T> inter);
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
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!);
|
||||
}
|
||||
}
|
|
@ -1,24 +0,0 @@
|
|||
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);
|
|
@ -1,10 +0,0 @@
|
|||
namespace Ellie.Marmalade;
|
||||
|
||||
public enum MarmaladeLoadResult
|
||||
{
|
||||
Success,
|
||||
NotFound,
|
||||
AlreadyLoaded,
|
||||
Empty,
|
||||
UnknownError,
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
namespace Ellie.Marmalade;
|
||||
|
||||
public enum MarmaladeUnloadResult
|
||||
{
|
||||
Success,
|
||||
NotLoaded,
|
||||
PossiblyUnable,
|
||||
NotFound,
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
namespace Ellie.Common;
|
||||
|
||||
public enum MsgType
|
||||
{
|
||||
Ok,
|
||||
Pending,
|
||||
Error
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
namespace Ellie.Common.ModuleBehaviors;
|
||||
|
||||
public interface IBehavior
|
||||
{
|
||||
public virtual string Name => this.GetType().Name;
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
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);
|
||||
}
|
|
@ -1,21 +0,0 @@
|
|||
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);
|
||||
}
|
|
@ -1,22 +0,0 @@
|
|||
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 Ellie 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);
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
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);
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
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);
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
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();
|
||||
}
|
|
@ -1,7 +0,0 @@
|
|||
namespace Ellie.Modules.Patronage;
|
||||
|
||||
public readonly struct FeatureLimitKey
|
||||
{
|
||||
public string PrettyName { get; init; }
|
||||
public string Key { get; init; }
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
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; }
|
||||
}
|
|
@ -1,11 +0,0 @@
|
|||
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; }
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
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();
|
||||
}
|
|
@ -1,16 +0,0 @@
|
|||
#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();
|
||||
}
|
|
@ -1,38 +0,0 @@
|
|||
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();
|
||||
}
|
|
@ -1,37 +0,0 @@
|
|||
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();
|
||||
}
|
||||
}
|
|
@ -1,33 +0,0 @@
|
|||
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}";
|
||||
}
|
||||
}
|
|
@ -1,14 +0,0 @@
|
|||
// ReSharper disable InconsistentNaming
|
||||
namespace Ellie.Modules.Patronage;
|
||||
|
||||
public enum PatronTier
|
||||
{
|
||||
None,
|
||||
I,
|
||||
V,
|
||||
X,
|
||||
XX,
|
||||
L,
|
||||
C,
|
||||
ComingSoon
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
using Ellie.Db.Models;
|
||||
|
||||
namespace Ellie.Modules.Patronage;
|
||||
|
||||
/// <summary>
|
||||
/// Represents information about why the user has triggered a quota limit
|
||||
/// </summary>
|
||||
public readonly struct QuotaLimit
|
||||
{
|
||||
/// <summary>
|
||||
/// Amount of usages reached, which is the limit
|
||||
/// </summary>
|
||||
public uint Quota { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Which period is this quota limit for (hourly, daily, monthly, etc...)
|
||||
/// </summary>
|
||||
public QuotaPer QuotaPeriod { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// When does this quota limit reset
|
||||
/// </summary>
|
||||
public DateTime ResetsAt { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Type of the feature this quota limit is for
|
||||
/// </summary>
|
||||
public FeatureType FeatureType { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Name of the feature this quota limit is for
|
||||
/// </summary>
|
||||
public string Feature { get; init; }
|
||||
|
||||
/// <summary>
|
||||
/// Whether it is the user's own quota (true), or server owners (false)
|
||||
/// </summary>
|
||||
public bool IsOwnQuota { get; init; }
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Respresent information about the feature limit
|
||||
/// </summary>
|
||||
public readonly struct FeatureLimit
|
||||
{
|
||||
|
||||
/// <summary>
|
||||
/// Whether this limit comes from the patronage system
|
||||
/// </summary>
|
||||
public bool IsPatronLimit { get; init; } = false;
|
||||
|
||||
/// <summary>
|
||||
/// Maximum limit allowed
|
||||
/// </summary>
|
||||
public int? Quota { get; init; } = null;
|
||||
|
||||
/// <summary>
|
||||
/// Name of the limit
|
||||
/// </summary>
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
public FeatureLimit()
|
||||
{
|
||||
}
|
||||
}
|
|
@ -1,8 +0,0 @@
|
|||
namespace Ellie.Modules.Patronage;
|
||||
|
||||
public enum QuotaPer
|
||||
{
|
||||
PerHour,
|
||||
PerDay,
|
||||
PerMonth,
|
||||
}
|
|
@ -1,10 +0,0 @@
|
|||
#nullable disable
|
||||
namespace Ellie.Modules.Patronage;
|
||||
|
||||
public enum SubscriptionChargeStatus
|
||||
{
|
||||
Paid,
|
||||
Refunded,
|
||||
Unpaid,
|
||||
Other,
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
namespace Ellie.Modules.Patronage;
|
||||
|
||||
public readonly struct UserQuotaStats
|
||||
{
|
||||
private static readonly IReadOnlyDictionary<string, FeatureQuotaStats> _emptyDictionary
|
||||
= new Dictionary<string, FeatureQuotaStats>();
|
||||
public PatronTier Tier { get; init; }
|
||||
= PatronTier.None;
|
||||
|
||||
public IReadOnlyDictionary<string, FeatureQuotaStats> Features { get; init; }
|
||||
= _emptyDictionary;
|
||||
|
||||
public IReadOnlyDictionary<string, FeatureQuotaStats> Commands { get; init; }
|
||||
= _emptyDictionary;
|
||||
|
||||
public IReadOnlyDictionary<string, FeatureQuotaStats> Groups { get; init; }
|
||||
= _emptyDictionary;
|
||||
|
||||
public IReadOnlyDictionary<string, FeatureQuotaStats> Modules { get; init; }
|
||||
= _emptyDictionary;
|
||||
|
||||
public UserQuotaStats()
|
||||
{
|
||||
}
|
||||
}
|
|
@ -1,164 +0,0 @@
|
|||
#nullable disable
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Ellie.Common;
|
||||
|
||||
public class ReplacementBuilder
|
||||
{
|
||||
private static readonly Regex _rngRegex = new("%rng(?:(?<from>(?:-)?\\d+)-(?<to>(?:-)?\\d+))?%",
|
||||
RegexOptions.Compiled);
|
||||
|
||||
private readonly ConcurrentDictionary<Regex, Func<Match, string>> _regex = new();
|
||||
|
||||
private readonly ConcurrentDictionary<string, Func<string>> _reps = new();
|
||||
|
||||
public ReplacementBuilder()
|
||||
=> WithRngRegex();
|
||||
|
||||
public ReplacementBuilder WithDefault(
|
||||
IUser usr,
|
||||
IMessageChannel ch,
|
||||
SocketGuild g,
|
||||
DiscordSocketClient client)
|
||||
=> WithUser(usr).WithChannel(ch).WithServer(client, g).WithClient(client);
|
||||
|
||||
public ReplacementBuilder WithDefault(ICommandContext ctx)
|
||||
=> WithDefault(ctx.User, ctx.Channel, ctx.Guild as SocketGuild, (DiscordSocketClient)ctx.Client);
|
||||
|
||||
public ReplacementBuilder WithMention(DiscordSocketClient client)
|
||||
{
|
||||
_reps.TryAdd("%bot.mention%", () => client.CurrentUser.Mention);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReplacementBuilder WithClient(DiscordSocketClient client)
|
||||
{
|
||||
WithMention(client);
|
||||
|
||||
_reps.TryAdd("%bot.status%", () => client.Status.ToString());
|
||||
_reps.TryAdd("%bot.latency%", () => client.Latency.ToString());
|
||||
_reps.TryAdd("%bot.name%", () => client.CurrentUser.Username);
|
||||
_reps.TryAdd("%bot.fullname%", () => client.CurrentUser.ToString());
|
||||
_reps.TryAdd("%bot.time%",
|
||||
() => DateTime.Now.ToString("HH:mm " + TimeZoneInfo.Local.StandardName.GetInitials()));
|
||||
_reps.TryAdd("%bot.discrim%", () => client.CurrentUser.Discriminator);
|
||||
_reps.TryAdd("%bot.id%", () => client.CurrentUser.Id.ToString());
|
||||
_reps.TryAdd("%bot.avatar%", () => client.CurrentUser.RealAvatarUrl().ToString());
|
||||
|
||||
WithStats(client);
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReplacementBuilder WithServer(DiscordSocketClient client, SocketGuild g)
|
||||
{
|
||||
_reps.TryAdd("%server%", () => g is null ? "DM" : g.Name);
|
||||
_reps.TryAdd("%server.id%", () => g is null ? "DM" : g.Id.ToString());
|
||||
_reps.TryAdd("%server.name%", () => g is null ? "DM" : g.Name);
|
||||
_reps.TryAdd("%server.icon%", () => g is null ? null : g.IconUrl);
|
||||
_reps.TryAdd("%server.members%", () => g is { } sg ? sg.MemberCount.ToString() : "?");
|
||||
_reps.TryAdd("%server.boosters%", () => g.PremiumSubscriptionCount.ToString());
|
||||
_reps.TryAdd("%server.boost_level%", () => ((int)g.PremiumTier).ToString());
|
||||
// todo fix
|
||||
// _reps.TryAdd("%server.time%",
|
||||
// () =>
|
||||
// {
|
||||
// var to = TimeZoneInfo.Local;
|
||||
// if (g is not null)
|
||||
// {
|
||||
// if (GuildTimezoneService.AllServices.TryGetValue(client.CurrentUser.Id, out var tz))
|
||||
// to = tz.GetTimeZoneOrDefault(g.Id) ?? TimeZoneInfo.Local;
|
||||
// }
|
||||
//
|
||||
// return TimeZoneInfo.ConvertTime(DateTime.UtcNow, TimeZoneInfo.Utc, to).ToString("HH:mm ")
|
||||
// + to.StandardName.GetInitials();
|
||||
// });
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReplacementBuilder WithChannel(IMessageChannel ch)
|
||||
{
|
||||
_reps.TryAdd("%channel%", () => ch.Name);
|
||||
_reps.TryAdd("%channel.mention%", () => (ch as ITextChannel)?.Mention ?? "#" + ch.Name);
|
||||
_reps.TryAdd("%channel.name%", () => ch.Name);
|
||||
_reps.TryAdd("%channel.id%", () => ch.Id.ToString());
|
||||
_reps.TryAdd("%channel.created%", () => ch.CreatedAt.ToString("HH:mm dd.MM.yyyy"));
|
||||
_reps.TryAdd("%channel.nsfw%", () => (ch as ITextChannel)?.IsNsfw.ToString() ?? "-");
|
||||
_reps.TryAdd("%channel.topic%", () => (ch as ITextChannel)?.Topic ?? "-");
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReplacementBuilder WithUser(IUser user)
|
||||
{
|
||||
WithManyUsers(new[] { user });
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReplacementBuilder WithManyUsers(IEnumerable<IUser> users)
|
||||
{
|
||||
_reps.TryAdd("%user%", () => string.Join(" ", users.Select(user => user.Mention)));
|
||||
_reps.TryAdd("%user.mention%", () => string.Join(" ", users.Select(user => user.Mention)));
|
||||
_reps.TryAdd("%user.fullname%", () => string.Join(" ", users.Select(user => user.ToString())));
|
||||
_reps.TryAdd("%user.name%", () => string.Join(" ", users.Select(user => user.Username)));
|
||||
_reps.TryAdd("%user.discrim%", () => string.Join(" ", users.Select(user => user.Discriminator)));
|
||||
_reps.TryAdd("%user.avatar%", () => string.Join(" ", users.Select(user => user.RealAvatarUrl().ToString())));
|
||||
_reps.TryAdd("%user.id%", () => string.Join(" ", users.Select(user => user.Id.ToString())));
|
||||
_reps.TryAdd("%user.created_time%",
|
||||
() => string.Join(" ", users.Select(user => user.CreatedAt.ToString("HH:mm"))));
|
||||
_reps.TryAdd("%user.created_date%",
|
||||
() => string.Join(" ", users.Select(user => user.CreatedAt.ToString("dd.MM.yyyy"))));
|
||||
_reps.TryAdd("%user.joined_time%",
|
||||
() => string.Join(" ", users.Select(user => (user as IGuildUser)?.JoinedAt?.ToString("HH:mm") ?? "-")));
|
||||
_reps.TryAdd("%user.joined_date%",
|
||||
() => string.Join(" ",
|
||||
users.Select(user => (user as IGuildUser)?.JoinedAt?.ToString("dd.MM.yyyy") ?? "-")));
|
||||
return this;
|
||||
}
|
||||
|
||||
private ReplacementBuilder WithStats(DiscordSocketClient c)
|
||||
{
|
||||
_reps.TryAdd("%shard.servercount%", () => c.Guilds.Count.ToString());
|
||||
_reps.TryAdd("%shard.usercount%", () => c.Guilds.Sum(g => g.MemberCount).ToString());
|
||||
_reps.TryAdd("%shard.id%", () => c.ShardId.ToString());
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReplacementBuilder WithRngRegex()
|
||||
{
|
||||
var rng = new EllieRandom();
|
||||
_regex.TryAdd(_rngRegex,
|
||||
match =>
|
||||
{
|
||||
if (!int.TryParse(match.Groups["from"].ToString(), out var from))
|
||||
from = 0;
|
||||
if (!int.TryParse(match.Groups["to"].ToString(), out var to))
|
||||
to = 0;
|
||||
|
||||
if (from == 0 && to == 0)
|
||||
return rng.Next(0, 11).ToString();
|
||||
|
||||
if (from >= to)
|
||||
return string.Empty;
|
||||
|
||||
return rng.Next(from, to + 1).ToString();
|
||||
});
|
||||
return this;
|
||||
}
|
||||
|
||||
public ReplacementBuilder WithOverride(string key, Func<string> output)
|
||||
{
|
||||
_reps.AddOrUpdate(key, output, delegate { return output; });
|
||||
return this;
|
||||
}
|
||||
|
||||
public Replacer Build()
|
||||
=> new(_reps.Select(x => (x.Key, x.Value)).ToArray(), _regex.Select(x => (x.Key, x.Value)).ToArray());
|
||||
|
||||
public ReplacementBuilder WithProviders(IEnumerable<IPlaceholderProvider> phProviders)
|
||||
{
|
||||
foreach (var provider in phProviders)
|
||||
foreach (var ovr in provider.GetPlaceholders())
|
||||
_reps.TryAdd(ovr.Name, ovr.Func);
|
||||
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -1,93 +0,0 @@
|
|||
#nullable disable
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace Ellie.Common;
|
||||
|
||||
public class Replacer
|
||||
{
|
||||
private readonly IEnumerable<(Regex Regex, Func<Match, string> Replacement)> _regex;
|
||||
private readonly IEnumerable<(string Key, Func<string> Text)> _replacements;
|
||||
|
||||
public Replacer(IEnumerable<(string, Func<string>)> replacements, IEnumerable<(Regex, Func<Match, string>)> regex)
|
||||
{
|
||||
_replacements = replacements;
|
||||
_regex = regex;
|
||||
}
|
||||
|
||||
public string Replace(string input)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(input))
|
||||
return input;
|
||||
|
||||
foreach (var (key, text) in _replacements)
|
||||
{
|
||||
if (input.Contains(key))
|
||||
input = input.Replace(key, text(), StringComparison.InvariantCulture);
|
||||
}
|
||||
|
||||
foreach (var item in _regex)
|
||||
input = item.Regex.Replace(input, m => item.Replacement(m));
|
||||
|
||||
return input;
|
||||
}
|
||||
|
||||
public SmartText Replace(SmartText data)
|
||||
=> data switch
|
||||
{
|
||||
SmartEmbedText embedData => Replace(embedData) with
|
||||
{
|
||||
PlainText = Replace(embedData.PlainText),
|
||||
Color = embedData.Color
|
||||
},
|
||||
SmartPlainText plain => Replace(plain),
|
||||
SmartEmbedTextArray arr => Replace(arr),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(data), "Unsupported argument type")
|
||||
};
|
||||
|
||||
private SmartEmbedTextArray Replace(SmartEmbedTextArray embedArr)
|
||||
=> new()
|
||||
{
|
||||
Embeds = embedArr.Embeds.Map(e => Replace(e) with
|
||||
{
|
||||
Color = e.Color
|
||||
}),
|
||||
Content = Replace(embedArr.Content)
|
||||
};
|
||||
|
||||
private SmartPlainText Replace(SmartPlainText plain)
|
||||
=> Replace(plain.Text);
|
||||
|
||||
private T Replace<T>(T embedData) where T : SmartEmbedTextBase, new()
|
||||
{
|
||||
var newEmbedData = new T
|
||||
{
|
||||
Description = Replace(embedData.Description),
|
||||
Title = Replace(embedData.Title),
|
||||
Thumbnail = Replace(embedData.Thumbnail),
|
||||
Image = Replace(embedData.Image),
|
||||
Url = Replace(embedData.Url),
|
||||
Author = embedData.Author is null
|
||||
? null
|
||||
: new()
|
||||
{
|
||||
Name = Replace(embedData.Author.Name),
|
||||
IconUrl = Replace(embedData.Author.IconUrl)
|
||||
},
|
||||
Fields = embedData.Fields?.Map(f => new SmartTextEmbedField
|
||||
{
|
||||
Name = Replace(f.Name),
|
||||
Value = Replace(f.Value),
|
||||
Inline = f.Inline
|
||||
}),
|
||||
Footer = embedData.Footer is null
|
||||
? null
|
||||
: new()
|
||||
{
|
||||
Text = Replace(embedData.Footer.Text),
|
||||
IconUrl = Replace(embedData.Footer.IconUrl)
|
||||
}
|
||||
};
|
||||
|
||||
return newEmbedData;
|
||||
}
|
||||
}
|
|
@ -1,427 +0,0 @@
|
|||
#nullable disable
|
||||
using Ellie.Common.Configs;
|
||||
using Ellie.Common.ModuleBehaviors;
|
||||
using Ellie.Db;
|
||||
using ExecuteResult = Discord.Commands.ExecuteResult;
|
||||
using PreconditionResult = Discord.Commands.PreconditionResult;
|
||||
|
||||
namespace Ellie.Services;
|
||||
|
||||
public class CommandHandler : IEService, IReadyExecutor, ICommandHandler
|
||||
{
|
||||
private const int GLOBAL_COMMANDS_COOLDOWN = 750;
|
||||
|
||||
private const float ONE_THOUSANDTH = 1.0f / 1000;
|
||||
|
||||
public event Func<IUserMessage, CommandInfo, Task> CommandExecuted = delegate { return Task.CompletedTask; };
|
||||
public event Func<CommandInfo, ITextChannel, string, Task> CommandErrored = delegate { return Task.CompletedTask; };
|
||||
|
||||
//userid/msg count
|
||||
public ConcurrentDictionary<ulong, uint> UserMessagesSent { get; } = new();
|
||||
|
||||
public ConcurrentHashSet<ulong> UsersOnShortCooldown { get; } = new();
|
||||
|
||||
private readonly DiscordSocketClient _client;
|
||||
private readonly CommandService _commandService;
|
||||
private readonly BotConfigService _bss;
|
||||
private readonly IBot _bot;
|
||||
private readonly IBehaviorHandler _behaviorHandler;
|
||||
private readonly IServiceProvider _services;
|
||||
|
||||
private readonly ConcurrentDictionary<ulong, string> _prefixes;
|
||||
|
||||
private readonly DbService _db;
|
||||
// private readonly InteractionService _interactions;
|
||||
|
||||
public CommandHandler(
|
||||
DiscordSocketClient client,
|
||||
DbService db,
|
||||
CommandService commandService,
|
||||
BotConfigService bss,
|
||||
IBot bot,
|
||||
IBehaviorHandler behaviorHandler,
|
||||
// InteractionService interactions,
|
||||
IServiceProvider services)
|
||||
{
|
||||
_client = client;
|
||||
_commandService = commandService;
|
||||
_bss = bss;
|
||||
_bot = bot;
|
||||
_behaviorHandler = behaviorHandler;
|
||||
_db = db;
|
||||
_services = services;
|
||||
// _interactions = interactions;
|
||||
|
||||
_prefixes = bot.AllGuildConfigs.Where(x => x.Prefix is not null)
|
||||
.ToDictionary(x => x.GuildId, x => x.Prefix)
|
||||
.ToConcurrent();
|
||||
|
||||
}
|
||||
|
||||
public async Task OnReadyAsync()
|
||||
{
|
||||
Log.Information("Command handler runnning on ready");
|
||||
// clear users on short cooldown every GLOBAL_COMMANDS_COOLDOWN miliseconds
|
||||
using var timer = new PeriodicTimer(TimeSpan.FromMilliseconds(GLOBAL_COMMANDS_COOLDOWN));
|
||||
while (await timer.WaitForNextTickAsync())
|
||||
UsersOnShortCooldown.Clear();
|
||||
}
|
||||
|
||||
public string GetPrefix(IGuild guild)
|
||||
=> GetPrefix(guild?.Id);
|
||||
|
||||
public string GetPrefix(ulong? id = null)
|
||||
{
|
||||
if (id is null || !_prefixes.TryGetValue(id.Value, out var prefix))
|
||||
return _bss.Data.Prefix;
|
||||
|
||||
return prefix;
|
||||
}
|
||||
|
||||
public string SetDefaultPrefix(string prefix)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(prefix))
|
||||
throw new ArgumentNullException(nameof(prefix));
|
||||
|
||||
_bss.ModifyConfig(bs =>
|
||||
{
|
||||
bs.Prefix = prefix;
|
||||
});
|
||||
|
||||
return prefix;
|
||||
}
|
||||
|
||||
public string SetPrefix(IGuild guild, string prefix)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(prefix))
|
||||
throw new ArgumentNullException(nameof(prefix));
|
||||
if (guild is null)
|
||||
throw new ArgumentNullException(nameof(guild));
|
||||
|
||||
using (var uow = _db.GetDbContext())
|
||||
{
|
||||
var gc = uow.GuildConfigsForId(guild.Id, set => set);
|
||||
gc.Prefix = prefix;
|
||||
uow.SaveChanges();
|
||||
}
|
||||
|
||||
_prefixes[guild.Id] = prefix;
|
||||
|
||||
return prefix;
|
||||
}
|
||||
|
||||
public async Task ExecuteExternal(ulong? guildId, ulong channelId, string commandText)
|
||||
{
|
||||
if (guildId is not null)
|
||||
{
|
||||
var guild = _client.GetGuild(guildId.Value);
|
||||
if (guild?.GetChannel(channelId) is not SocketTextChannel channel)
|
||||
{
|
||||
Log.Warning("Channel for external execution not found");
|
||||
return;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
IUserMessage msg = await channel.SendMessageAsync(commandText);
|
||||
msg = (IUserMessage)await channel.GetMessageAsync(msg.Id);
|
||||
await TryRunCommand(guild, channel, msg);
|
||||
//msg.DeleteAfter(5);
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
public Task StartHandling()
|
||||
{
|
||||
_client.MessageReceived += MessageReceivedHandler;
|
||||
// _client.SlashCommandExecuted += SlashCommandExecuted;
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
// private async Task SlashCommandExecuted(SocketSlashCommand arg)
|
||||
// {
|
||||
// var ctx = new SocketInteractionContext<SocketSlashCommand>(_client, arg);
|
||||
// await _interactions.ExecuteCommandAsync(ctx, _services);
|
||||
// }
|
||||
|
||||
private Task LogSuccessfulExecution(IUserMessage usrMsg, ITextChannel channel, params int[] execPoints)
|
||||
{
|
||||
if (_bss.Data.ConsoleOutputType == ConsoleOutputType.Normal)
|
||||
{
|
||||
Log.Information("""
|
||||
Command Executed after {ExecTime}s
|
||||
User: {User}
|
||||
Server: {Server}
|
||||
Channel: {Channel}
|
||||
Message: {Message}
|
||||
""",
|
||||
string.Join("/", execPoints.Select(x => (x * ONE_THOUSANDTH).ToString("F3"))),
|
||||
usrMsg.Author + " [" + usrMsg.Author.Id + "]",
|
||||
channel is null ? "PRIVATE" : channel.Guild.Name + " [" + channel.Guild.Id + "]",
|
||||
channel is null ? "PRIVATE" : channel.Name + " [" + channel.Id + "]",
|
||||
usrMsg.Content);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Information("Succ | g:{GuildId} | c: {ChannelId} | u: {UserId} | msg: {Message}",
|
||||
channel?.Guild.Id.ToString() ?? "-",
|
||||
channel?.Id.ToString() ?? "-",
|
||||
usrMsg.Author.Id,
|
||||
usrMsg.Content.TrimTo(10));
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private void LogErroredExecution(
|
||||
string errorMessage,
|
||||
IUserMessage usrMsg,
|
||||
ITextChannel channel,
|
||||
params int[] execPoints)
|
||||
{
|
||||
if (_bss.Data.ConsoleOutputType == ConsoleOutputType.Normal)
|
||||
{
|
||||
Log.Warning("""
|
||||
Command Errored after {ExecTime}s
|
||||
User: {User}
|
||||
Server: {Guild}
|
||||
Channel: {Channel}
|
||||
Message: {Message}
|
||||
Error: {ErrorMessage}
|
||||
""",
|
||||
string.Join("/", execPoints.Select(x => (x * ONE_THOUSANDTH).ToString("F3"))),
|
||||
usrMsg.Author + " [" + usrMsg.Author.Id + "]",
|
||||
channel is null ? "DM" : channel.Guild.Name + " [" + channel.Guild.Id + "]",
|
||||
channel is null ? "DM" : channel.Name + " [" + channel.Id + "]",
|
||||
usrMsg.Content,
|
||||
errorMessage);
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning("""
|
||||
Err | g:{GuildId} | c: {ChannelId} | u: {UserId} | msg: {Message}
|
||||
Err: {ErrorMessage}
|
||||
""",
|
||||
channel?.Guild.Id.ToString() ?? "-",
|
||||
channel?.Id.ToString() ?? "-",
|
||||
usrMsg.Author.Id,
|
||||
usrMsg.Content.TrimTo(10),
|
||||
errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
private Task MessageReceivedHandler(SocketMessage msg)
|
||||
{
|
||||
//no bots, wait until bot connected and initialized
|
||||
if (msg.Author.IsBot || !_bot.IsReady)
|
||||
return Task.CompletedTask;
|
||||
|
||||
if (msg is not SocketUserMessage usrMsg)
|
||||
return Task.CompletedTask;
|
||||
|
||||
Task.Run(async () =>
|
||||
{
|
||||
try
|
||||
{
|
||||
#if !GLOBAL_ELLIE
|
||||
// track how many messages each user is sending
|
||||
UserMessagesSent.AddOrUpdate(usrMsg.Author.Id, 1, (_, old) => ++old);
|
||||
#endif
|
||||
|
||||
var channel = msg.Channel;
|
||||
var guild = (msg.Channel as SocketTextChannel)?.Guild;
|
||||
|
||||
await TryRunCommand(guild, channel, usrMsg);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "Error in CommandHandler");
|
||||
if (ex.InnerException is not null)
|
||||
Log.Warning(ex.InnerException, "Inner Exception of the error in CommandHandler");
|
||||
}
|
||||
});
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task TryRunCommand(SocketGuild guild, ISocketMessageChannel channel, IUserMessage usrMsg)
|
||||
{
|
||||
var startTime = Environment.TickCount;
|
||||
|
||||
var blocked = await _behaviorHandler.RunExecOnMessageAsync(guild, usrMsg);
|
||||
if (blocked)
|
||||
return;
|
||||
|
||||
var blockTime = Environment.TickCount - startTime;
|
||||
|
||||
var messageContent = await _behaviorHandler.RunInputTransformersAsync(guild, usrMsg);
|
||||
|
||||
var prefix = GetPrefix(guild?.Id);
|
||||
var isPrefixCommand = messageContent.StartsWith(".prefix", StringComparison.InvariantCultureIgnoreCase);
|
||||
// execute the command and measure the time it took
|
||||
if (isPrefixCommand || messageContent.StartsWith(prefix, StringComparison.InvariantCulture))
|
||||
{
|
||||
var context = new CommandContext(_client, usrMsg);
|
||||
var (success, error, info) = await ExecuteCommandAsync(context,
|
||||
messageContent,
|
||||
isPrefixCommand ? 1 : prefix.Length,
|
||||
_services,
|
||||
MultiMatchHandling.Best);
|
||||
|
||||
startTime = Environment.TickCount - startTime;
|
||||
|
||||
// if a command is found
|
||||
if (info is not null)
|
||||
{
|
||||
// if it successfully executed
|
||||
if (success)
|
||||
{
|
||||
await LogSuccessfulExecution(usrMsg, channel as ITextChannel, blockTime, startTime);
|
||||
await CommandExecuted(usrMsg, info);
|
||||
await _behaviorHandler.RunPostCommandAsync(context, info.Module.GetTopLevelModule().Name, info);
|
||||
return;
|
||||
}
|
||||
|
||||
// if it errored
|
||||
if (error is not null)
|
||||
{
|
||||
error = HumanizeError(error);
|
||||
LogErroredExecution(error, usrMsg, channel as ITextChannel, blockTime, startTime);
|
||||
|
||||
if (guild is not null)
|
||||
await CommandErrored(info, channel as ITextChannel, error);
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await _behaviorHandler.RunOnNoCommandAsync(guild, usrMsg);
|
||||
}
|
||||
|
||||
private string HumanizeError(string error)
|
||||
{
|
||||
if (error.Contains("parse int", StringComparison.OrdinalIgnoreCase)
|
||||
|| error.Contains("parse float"))
|
||||
return "Invalid number specified. Make sure you're specifying parameters in the correct order.";
|
||||
|
||||
return error;
|
||||
}
|
||||
|
||||
public Task<(bool Success, string Error, CommandInfo Info)> ExecuteCommandAsync(
|
||||
ICommandContext context,
|
||||
string input,
|
||||
int argPos,
|
||||
IServiceProvider serviceProvider,
|
||||
MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception)
|
||||
=> ExecuteCommand(context, input[argPos..], serviceProvider, multiMatchHandling);
|
||||
|
||||
|
||||
public async Task<(bool Success, string Error, CommandInfo Info)> ExecuteCommand(
|
||||
ICommandContext context,
|
||||
string input,
|
||||
IServiceProvider services,
|
||||
MultiMatchHandling multiMatchHandling = MultiMatchHandling.Exception)
|
||||
{
|
||||
var searchResult = _commandService.Search(context, input);
|
||||
if (!searchResult.IsSuccess)
|
||||
return (false, null, null);
|
||||
|
||||
var commands = searchResult.Commands;
|
||||
var preconditionResults = new Dictionary<CommandMatch, PreconditionResult>();
|
||||
|
||||
foreach (var match in commands)
|
||||
preconditionResults[match] = await match.Command.CheckPreconditionsAsync(context, services);
|
||||
|
||||
var successfulPreconditions = preconditionResults.Where(x => x.Value.IsSuccess).ToArray();
|
||||
|
||||
if (successfulPreconditions.Length == 0)
|
||||
{
|
||||
//All preconditions failed, return the one from the highest priority command
|
||||
var bestCandidate = preconditionResults.OrderByDescending(x => x.Key.Command.Priority)
|
||||
.FirstOrDefault(x => !x.Value.IsSuccess);
|
||||
return (false, bestCandidate.Value.ErrorReason, commands[0].Command);
|
||||
}
|
||||
|
||||
var parseResultsDict = new Dictionary<CommandMatch, ParseResult>();
|
||||
foreach (var pair in successfulPreconditions)
|
||||
{
|
||||
var parseResult = await pair.Key.ParseAsync(context, searchResult, pair.Value, services);
|
||||
|
||||
if (parseResult.Error == CommandError.MultipleMatches)
|
||||
{
|
||||
IReadOnlyList<TypeReaderValue> argList, paramList;
|
||||
switch (multiMatchHandling)
|
||||
{
|
||||
case MultiMatchHandling.Best:
|
||||
argList = parseResult.ArgValues
|
||||
.Map(x => x.Values.MaxBy(y => y.Score));
|
||||
paramList = parseResult.ParamValues
|
||||
.Map(x => x.Values.MaxBy(y => y.Score));
|
||||
parseResult = ParseResult.FromSuccess(argList, paramList);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
parseResultsDict[pair.Key] = parseResult;
|
||||
}
|
||||
|
||||
// Calculates the 'score' of a command given a parse result
|
||||
float CalculateScore(CommandMatch match, ParseResult parseResult)
|
||||
{
|
||||
float argValuesScore = 0, paramValuesScore = 0;
|
||||
|
||||
if (match.Command.Parameters.Count > 0)
|
||||
{
|
||||
var argValuesSum =
|
||||
parseResult.ArgValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score)
|
||||
?? 0;
|
||||
var paramValuesSum =
|
||||
parseResult.ParamValues?.Sum(x => x.Values.OrderByDescending(y => y.Score).FirstOrDefault().Score)
|
||||
?? 0;
|
||||
|
||||
argValuesScore = argValuesSum / match.Command.Parameters.Count;
|
||||
paramValuesScore = paramValuesSum / match.Command.Parameters.Count;
|
||||
}
|
||||
|
||||
var totalArgsScore = (argValuesScore + paramValuesScore) / 2;
|
||||
return match.Command.Priority + (totalArgsScore * 0.99f);
|
||||
}
|
||||
|
||||
//Order the parse results by their score so that we choose the most likely result to execute
|
||||
var parseResults = parseResultsDict.OrderByDescending(x => CalculateScore(x.Key, x.Value)).ToList();
|
||||
|
||||
var successfulParses = parseResults.Where(x => x.Value.IsSuccess).ToArray();
|
||||
|
||||
if (successfulParses.Length == 0)
|
||||
{
|
||||
//All parses failed, return the one from the highest priority command, using score as a tie breaker
|
||||
var bestMatch = parseResults.FirstOrDefault(x => !x.Value.IsSuccess);
|
||||
return (false, bestMatch.Value.ErrorReason, commands[0].Command);
|
||||
}
|
||||
|
||||
var cmd = successfulParses[0].Key.Command;
|
||||
|
||||
// Bot will ignore commands which are ran more often than what specified by
|
||||
// GlobalCommandsCooldown constant (miliseconds)
|
||||
if (!UsersOnShortCooldown.Add(context.Message.Author.Id))
|
||||
return (false, null, cmd);
|
||||
//return SearchResult.FromError(CommandError.Exception, "You are on a global cooldown.");
|
||||
|
||||
var blocked = await _behaviorHandler.RunPreCommandAsync(context, cmd);
|
||||
if (blocked)
|
||||
return (false, null, cmd);
|
||||
|
||||
//If we get this far, at least one parse was successful. Execute the most likely overload.
|
||||
var chosenOverload = successfulParses[0];
|
||||
var execResult = (ExecuteResult)await chosenOverload.Key.ExecuteAsync(context, chosenOverload.Value, services);
|
||||
|
||||
if (execResult.Exception is not null
|
||||
&& (execResult.Exception is not HttpException he
|
||||
|| he.DiscordCode != DiscordErrorCode.InsufficientPermissions))
|
||||
Log.Warning(execResult.Exception, "Command Error");
|
||||
|
||||
return (true, null, cmd);
|
||||
}
|
||||
}
|
|
@ -1,109 +0,0 @@
|
|||
#nullable disable
|
||||
using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using Ellie.Db.Models;
|
||||
using Ellie.Services.Currency;
|
||||
|
||||
namespace Ellie.Services;
|
||||
|
||||
public sealed class CurrencyService : ICurrencyService, IEService
|
||||
{
|
||||
private readonly DbService _db;
|
||||
private readonly ITxTracker _txTracker;
|
||||
|
||||
public CurrencyService(DbService db, ITxTracker txTracker)
|
||||
{
|
||||
_db = db;
|
||||
_txTracker = txTracker;
|
||||
}
|
||||
|
||||
public Task<IWallet> GetWalletAsync(ulong userId, CurrencyType type = CurrencyType.Default)
|
||||
{
|
||||
if (type == CurrencyType.Default)
|
||||
return Task.FromResult<IWallet>(new DefaultWallet(userId, _db));
|
||||
|
||||
throw new ArgumentOutOfRangeException(nameof(type));
|
||||
}
|
||||
|
||||
public async Task AddBulkAsync(
|
||||
IReadOnlyCollection<ulong> userIds,
|
||||
long amount,
|
||||
TxData txData,
|
||||
CurrencyType type = CurrencyType.Default)
|
||||
{
|
||||
if (type == CurrencyType.Default)
|
||||
{
|
||||
foreach (var userId in userIds)
|
||||
{
|
||||
var wallet = await GetWalletAsync(userId);
|
||||
await wallet.Add(amount, txData);
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ArgumentOutOfRangeException(nameof(type));
|
||||
}
|
||||
|
||||
public async Task RemoveBulkAsync(
|
||||
IReadOnlyCollection<ulong> userIds,
|
||||
long amount,
|
||||
TxData txData,
|
||||
CurrencyType type = CurrencyType.Default)
|
||||
{
|
||||
if (type == CurrencyType.Default)
|
||||
{
|
||||
await using var ctx = _db.GetDbContext();
|
||||
await ctx
|
||||
.GetTable<DiscordUser>()
|
||||
.Where(x => userIds.Contains(x.UserId))
|
||||
.UpdateAsync(du => new()
|
||||
{
|
||||
CurrencyAmount = du.CurrencyAmount >= amount
|
||||
? du.CurrencyAmount - amount
|
||||
: 0
|
||||
});
|
||||
await ctx.SaveChangesAsync();
|
||||
return;
|
||||
}
|
||||
|
||||
throw new ArgumentOutOfRangeException(nameof(type));
|
||||
}
|
||||
|
||||
public async Task AddAsync(
|
||||
ulong userId,
|
||||
long amount,
|
||||
TxData txData)
|
||||
{
|
||||
var wallet = await GetWalletAsync(userId);
|
||||
await wallet.Add(amount, txData);
|
||||
await _txTracker.TrackAdd(amount, txData);
|
||||
}
|
||||
|
||||
public async Task AddAsync(
|
||||
IUser user,
|
||||
long amount,
|
||||
TxData txData)
|
||||
=> await AddAsync(user.Id, amount, txData);
|
||||
|
||||
public async Task<bool> RemoveAsync(
|
||||
ulong userId,
|
||||
long amount,
|
||||
TxData txData)
|
||||
{
|
||||
if (amount == 0)
|
||||
return true;
|
||||
|
||||
var wallet = await GetWalletAsync(userId);
|
||||
var result = await wallet.Take(amount, txData);
|
||||
if(result)
|
||||
await _txTracker.TrackRemove(amount, txData);
|
||||
return result;
|
||||
}
|
||||
|
||||
public async Task<bool> RemoveAsync(
|
||||
IUser user,
|
||||
long amount,
|
||||
TxData txData)
|
||||
=> await RemoveAsync(user.Id, amount, txData);
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
using Ellie.Services.Currency;
|
||||
|
||||
namespace Ellie.Services;
|
||||
|
||||
public static class CurrencyServiceExtensions
|
||||
{
|
||||
public static async Task<long> GetBalanceAsync(this ICurrencyService cs, ulong userId)
|
||||
{
|
||||
var wallet = await cs.GetWalletAsync(userId);
|
||||
return await wallet.GetBalance();
|
||||
}
|
||||
|
||||
// FUTURE should be a transaction
|
||||
public static async Task<bool> TransferAsync(
|
||||
this ICurrencyService cs,
|
||||
IEmbedBuilderService ebs,
|
||||
IUser from,
|
||||
IUser to,
|
||||
long amount,
|
||||
string? note,
|
||||
string formattedAmount)
|
||||
{
|
||||
var fromWallet = await cs.GetWalletAsync(from.Id);
|
||||
var toWallet = await cs.GetWalletAsync(to.Id);
|
||||
|
||||
var extra = new TxData("gift", from.ToString()!, note, from.Id);
|
||||
|
||||
if (await fromWallet.Transfer(amount, toWallet, extra))
|
||||
{
|
||||
await to.SendConfirmAsync(ebs,
|
||||
string.IsNullOrWhiteSpace(note)
|
||||
? $"Received {formattedAmount} from {from} "
|
||||
: $"Received {formattedAmount} from {from}: {note}");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -1,115 +0,0 @@
|
|||
using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using Ellie.Db.Models;
|
||||
using Ellie.Services.Database.Models;
|
||||
|
||||
namespace Ellie.Services.Currency;
|
||||
|
||||
public class DefaultWallet : IWallet
|
||||
{
|
||||
private readonly DbService _db;
|
||||
public ulong UserId { get; }
|
||||
|
||||
public DefaultWallet(ulong userId, DbService db)
|
||||
{
|
||||
UserId = userId;
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task<long> GetBalance()
|
||||
{
|
||||
await using var ctx = _db.GetDbContext();
|
||||
var userId = UserId;
|
||||
return await ctx
|
||||
.GetTable<DiscordUser>()
|
||||
.Where(x => x.UserId == userId)
|
||||
.Select(x => x.CurrencyAmount)
|
||||
.FirstOrDefaultAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> Take(long amount, TxData? txData)
|
||||
{
|
||||
if (amount < 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(amount), "Amount to take must be non negative.");
|
||||
|
||||
await using var ctx = _db.GetDbContext();
|
||||
|
||||
var userId = UserId;
|
||||
var changed = await ctx
|
||||
.GetTable<DiscordUser>()
|
||||
.Where(x => x.UserId == userId && x.CurrencyAmount >= amount)
|
||||
.UpdateAsync(x => new()
|
||||
{
|
||||
CurrencyAmount = x.CurrencyAmount - amount
|
||||
});
|
||||
|
||||
if (changed == 0)
|
||||
return false;
|
||||
|
||||
if (txData is not null)
|
||||
{
|
||||
await ctx
|
||||
.GetTable<CurrencyTransaction>()
|
||||
.InsertAsync(() => new()
|
||||
{
|
||||
Amount = -amount,
|
||||
Note = txData.Note,
|
||||
UserId = userId,
|
||||
Type = txData.Type,
|
||||
Extra = txData.Extra,
|
||||
OtherId = txData.OtherId,
|
||||
DateAdded = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public async Task Add(long amount, TxData? txData)
|
||||
{
|
||||
if (amount <= 0)
|
||||
throw new ArgumentOutOfRangeException(nameof(amount), "Amount must be greater than 0.");
|
||||
|
||||
await using var ctx = _db.GetDbContext();
|
||||
var userId = UserId;
|
||||
|
||||
await using (var tran = await ctx.Database.BeginTransactionAsync())
|
||||
{
|
||||
var changed = await ctx
|
||||
.GetTable<DiscordUser>()
|
||||
.Where(x => x.UserId == userId)
|
||||
.UpdateAsync(x => new()
|
||||
{
|
||||
CurrencyAmount = x.CurrencyAmount + amount
|
||||
});
|
||||
|
||||
if (changed == 0)
|
||||
{
|
||||
await ctx
|
||||
.GetTable<DiscordUser>()
|
||||
.Value(x => x.UserId, userId)
|
||||
.Value(x => x.Username, "Unknown")
|
||||
.Value(x => x.Discriminator, "????")
|
||||
.Value(x => x.CurrencyAmount, amount)
|
||||
.InsertAsync();
|
||||
}
|
||||
|
||||
await tran.CommitAsync();
|
||||
}
|
||||
|
||||
if (txData is not null)
|
||||
{
|
||||
await ctx.GetTable<CurrencyTransaction>()
|
||||
.InsertAsync(() => new()
|
||||
{
|
||||
Amount = amount,
|
||||
UserId = userId,
|
||||
Note = txData.Note,
|
||||
Type = txData.Type,
|
||||
Extra = txData.Extra,
|
||||
OtherId = txData.OtherId,
|
||||
DateAdded = DateTime.UtcNow
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,110 +0,0 @@
|
|||
using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using Ellie.Common.ModuleBehaviors;
|
||||
using Ellie.Services.Currency;
|
||||
using Ellie.Services.Database.Models;
|
||||
|
||||
namespace Ellie.Services;
|
||||
|
||||
public sealed class GamblingTxTracker : ITxTracker, IEService, IReadyExecutor
|
||||
{
|
||||
private static readonly IReadOnlySet<string> _gamblingTypes = new HashSet<string>(new[]
|
||||
{
|
||||
"lula",
|
||||
"betroll",
|
||||
"betflip",
|
||||
"blackjack",
|
||||
"betdraw",
|
||||
"slot",
|
||||
});
|
||||
|
||||
private ConcurrentDictionary<string, (decimal Bet, decimal PaidOut)> _stats = new();
|
||||
|
||||
private readonly DbService _db;
|
||||
|
||||
public GamblingTxTracker(DbService db)
|
||||
{
|
||||
_db = db;
|
||||
}
|
||||
|
||||
public async Task OnReadyAsync()
|
||||
{
|
||||
using var timer = new PeriodicTimer(TimeSpan.FromHours(1));
|
||||
while (await timer.WaitForNextTickAsync())
|
||||
{
|
||||
await using var ctx = _db.GetDbContext();
|
||||
await using var trans = await ctx.Database.BeginTransactionAsync();
|
||||
|
||||
try
|
||||
{
|
||||
var keys = _stats.Keys;
|
||||
foreach (var key in keys)
|
||||
{
|
||||
if (_stats.TryRemove(key, out var stat))
|
||||
{
|
||||
await ctx.GetTable<GamblingStats>()
|
||||
.InsertOrUpdateAsync(() => new()
|
||||
{
|
||||
Feature = key,
|
||||
Bet = stat.Bet,
|
||||
PaidOut = stat.PaidOut,
|
||||
DateAdded = DateTime.UtcNow
|
||||
}, old => new()
|
||||
{
|
||||
Bet = old.Bet + stat.Bet,
|
||||
PaidOut = old.PaidOut + stat.PaidOut,
|
||||
}, () => new()
|
||||
{
|
||||
Feature = key
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex, "An error occurred in gambling tx tracker");
|
||||
}
|
||||
finally
|
||||
{
|
||||
await trans.CommitAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public Task TrackAdd(long amount, TxData? txData)
|
||||
{
|
||||
if (txData is null)
|
||||
return Task.CompletedTask;
|
||||
|
||||
if (_gamblingTypes.Contains(txData.Type))
|
||||
{
|
||||
_stats.AddOrUpdate(txData.Type,
|
||||
_ => (0, amount),
|
||||
(_, old) => (old.Bet, old.PaidOut + amount));
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public Task TrackRemove(long amount, TxData? txData)
|
||||
{
|
||||
if (txData is null)
|
||||
return Task.CompletedTask;
|
||||
|
||||
if (_gamblingTypes.Contains(txData.Type))
|
||||
{
|
||||
_stats.AddOrUpdate(txData.Type,
|
||||
_ => (amount, 0),
|
||||
(_, old) => (old.Bet + amount, old.PaidOut));
|
||||
}
|
||||
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
public async Task<IReadOnlyCollection<GamblingStats>> GetAllAsync()
|
||||
{
|
||||
await using var ctx = _db.GetDbContext();
|
||||
return await ctx.Set<GamblingStats>()
|
||||
.ToListAsyncEF();
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
#nullable disable
|
||||
namespace Ellie.Services;
|
||||
|
||||
public interface IBehaviorHandler
|
||||
{
|
||||
Task<bool> AddAsync(ICustomBehavior behavior);
|
||||
Task AddRangeAsync(IEnumerable<ICustomBehavior> behavior);
|
||||
Task<bool> RemoveAsync(ICustomBehavior behavior);
|
||||
Task RemoveRangeAsync(IEnumerable<ICustomBehavior> behs);
|
||||
|
||||
Task<bool> RunExecOnMessageAsync(SocketGuild guild, IUserMessage usrMsg);
|
||||
Task<string> RunInputTransformersAsync(SocketGuild guild, IUserMessage usrMsg);
|
||||
Task<bool> RunPreCommandAsync(ICommandContext context, CommandInfo cmd);
|
||||
ValueTask RunPostCommandAsync(ICommandContext ctx, string moduleName, CommandInfo cmd);
|
||||
Task RunOnNoCommandAsync(SocketGuild guild, IUserMessage usrMsg);
|
||||
void Initialize();
|
||||
}
|
|
@ -1,12 +0,0 @@
|
|||
namespace Ellie.Services;
|
||||
|
||||
public interface ICommandHandler
|
||||
{
|
||||
string GetPrefix(IGuild ctxGuild);
|
||||
string GetPrefix(ulong? id = null);
|
||||
string SetDefaultPrefix(string toSet);
|
||||
string SetPrefix(IGuild ctxGuild, string toSet);
|
||||
ConcurrentDictionary<ulong, uint> UserMessagesSent { get; }
|
||||
|
||||
Task TryRunCommand(SocketGuild guild, ISocketMessageChannel channel, IUserMessage usrMsg);
|
||||
}
|
|
@ -1,20 +0,0 @@
|
|||
#nullable disable
|
||||
namespace Ellie.Services;
|
||||
|
||||
public interface ICoordinator
|
||||
{
|
||||
bool RestartBot();
|
||||
void Die(bool graceful);
|
||||
bool RestartShard(int shardId);
|
||||
IList<ShardStatus> GetAllShardStatuses();
|
||||
int GetGuildCount();
|
||||
Task Reload();
|
||||
}
|
||||
|
||||
public class ShardStatus
|
||||
{
|
||||
public ConnectionState ConnectionState { get; set; }
|
||||
public DateTime LastUpdate { get; set; }
|
||||
public int ShardId { get; set; }
|
||||
public int GuildCount { get; set; }
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
using Ellie.Common.ModuleBehaviors;
|
||||
|
||||
namespace Ellie.Services;
|
||||
|
||||
public interface ICustomBehavior
|
||||
: IExecOnMessage,
|
||||
IInputTransformer,
|
||||
IExecPreCommand,
|
||||
IExecNoCommand,
|
||||
IExecPostCommand
|
||||
{
|
||||
|
||||
}
|
|
@ -1,9 +0,0 @@
|
|||
#nullable disable
|
||||
namespace Ellie.Services;
|
||||
|
||||
/// <summary>
|
||||
/// All services must implement this interface in order to be auto-discovered by the DI system
|
||||
/// </summary>
|
||||
public interface IEService
|
||||
{
|
||||
}
|
|
@ -1,81 +0,0 @@
|
|||
#nullable disable
|
||||
using Ellie.Common.Configs;
|
||||
|
||||
namespace Ellie.Services;
|
||||
|
||||
public interface IEmbedBuilderService
|
||||
{
|
||||
IEmbedBuilder Create(ICommandContext ctx = null);
|
||||
IEmbedBuilder Create(EmbedBuilder eb);
|
||||
}
|
||||
|
||||
public class EmbedBuilderService : IEmbedBuilderService, IEService
|
||||
{
|
||||
private readonly BotConfigService _botConfigService;
|
||||
|
||||
public EmbedBuilderService(BotConfigService botConfigService)
|
||||
=> _botConfigService = botConfigService;
|
||||
|
||||
public IEmbedBuilder Create(ICommandContext ctx = null)
|
||||
=> new DiscordEmbedBuilderWrapper(_botConfigService.Data);
|
||||
|
||||
public IEmbedBuilder Create(EmbedBuilder embed)
|
||||
=> new DiscordEmbedBuilderWrapper(_botConfigService.Data, embed);
|
||||
}
|
||||
|
||||
public sealed class DiscordEmbedBuilderWrapper : IEmbedBuilder
|
||||
{
|
||||
private readonly BotConfig _botConfig;
|
||||
private EmbedBuilder embed;
|
||||
|
||||
public DiscordEmbedBuilderWrapper(in BotConfig botConfig, EmbedBuilder embed = null)
|
||||
{
|
||||
_botConfig = botConfig;
|
||||
this.embed = embed ?? new EmbedBuilder();
|
||||
}
|
||||
|
||||
public IEmbedBuilder WithDescription(string desc)
|
||||
=> Wrap(embed.WithDescription(desc));
|
||||
|
||||
public IEmbedBuilder WithTitle(string title)
|
||||
=> Wrap(embed.WithTitle(title));
|
||||
|
||||
public IEmbedBuilder AddField(string title, object value, bool isInline = false)
|
||||
=> Wrap(embed.AddField(title, value, isInline));
|
||||
|
||||
public IEmbedBuilder WithFooter(string text, string iconUrl = null)
|
||||
=> Wrap(embed.WithFooter(text, iconUrl));
|
||||
|
||||
public IEmbedBuilder WithAuthor(string name, string iconUrl = null, string url = null)
|
||||
=> Wrap(embed.WithAuthor(name, iconUrl, url));
|
||||
|
||||
public IEmbedBuilder WithUrl(string url)
|
||||
=> Wrap(embed.WithUrl(url));
|
||||
|
||||
public IEmbedBuilder WithImageUrl(string url)
|
||||
=> Wrap(embed.WithImageUrl(url));
|
||||
|
||||
public IEmbedBuilder WithThumbnailUrl(string url)
|
||||
=> Wrap(embed.WithThumbnailUrl(url));
|
||||
|
||||
public IEmbedBuilder WithColor(EmbedColor color)
|
||||
=> color switch
|
||||
{
|
||||
EmbedColor.Ok => Wrap(embed.WithColor(_botConfig.Color.Ok.ToDiscordColor())),
|
||||
EmbedColor.Pending => Wrap(embed.WithColor(_botConfig.Color.Pending.ToDiscordColor())),
|
||||
EmbedColor.Error => Wrap(embed.WithColor(_botConfig.Color.Error.ToDiscordColor())),
|
||||
_ => throw new ArgumentOutOfRangeException(nameof(color), "Unsupported EmbedColor type")
|
||||
};
|
||||
|
||||
public IEmbedBuilder WithDiscordColor(Color color)
|
||||
=> Wrap(embed.WithColor(color));
|
||||
|
||||
public Embed Build()
|
||||
=> embed.Build();
|
||||
|
||||
private IEmbedBuilder Wrap(EmbedBuilder eb)
|
||||
{
|
||||
embed = eb;
|
||||
return this;
|
||||
}
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
#nullable disable
|
||||
|
||||
namespace Ellie.Services;
|
||||
|
||||
public interface IGoogleApiService
|
||||
{
|
||||
IReadOnlyDictionary<string, string> Languages { get; }
|
||||
|
||||
Task<IEnumerable<string>> GetVideoLinksByKeywordAsync(string keywords, int count = 1);
|
||||
Task<IEnumerable<(string Name, string Id, string Url)>> GetVideoInfosByKeywordAsync(string keywords, int count = 1);
|
||||
Task<IEnumerable<string>> GetPlaylistIdsByKeywordsAsync(string keywords, int count = 1);
|
||||
Task<IEnumerable<string>> GetRelatedVideosAsync(string id, int count = 1, string user = null);
|
||||
Task<IEnumerable<string>> GetPlaylistTracksAsync(string playlistId, int count = 50);
|
||||
Task<IReadOnlyDictionary<string, TimeSpan>> GetVideoDurationsAsync(IEnumerable<string> videoIds);
|
||||
Task<string> Translate(string sourceText, string sourceLanguage, string targetLanguage);
|
||||
|
||||
Task<string> ShortenUrl(string url);
|
||||
Task<string> ShortenUrl(Uri url);
|
||||
}
|
|
@ -1,13 +0,0 @@
|
|||
#nullable disable
|
||||
using Ellie.Common.Pokemon;
|
||||
using Ellie.Modules.Games.Common.Trivia;
|
||||
|
||||
namespace Ellie.Services;
|
||||
|
||||
public interface ILocalDataCache
|
||||
{
|
||||
Task<IReadOnlyDictionary<string, SearchPokemon>> GetPokemonsAsync();
|
||||
Task<IReadOnlyDictionary<string, SearchPokemonAbility>> GetPokemonAbilitiesAsync();
|
||||
Task<TriviaQuestionModel[]> GetTriviaQuestionsAsync();
|
||||
Task<IReadOnlyDictionary<int, string>> GetPokemonMapAsync();
|
||||
}
|
|
@ -1,19 +0,0 @@
|
|||
#nullable disable
|
||||
using System.Globalization;
|
||||
|
||||
namespace Ellie.Services;
|
||||
|
||||
public interface ILocalization
|
||||
{
|
||||
CultureInfo DefaultCultureInfo { get; }
|
||||
IDictionary<ulong, CultureInfo> GuildCultureInfos { get; }
|
||||
|
||||
CultureInfo GetCultureInfo(IGuild guild);
|
||||
CultureInfo GetCultureInfo(ulong? guildId);
|
||||
void RemoveGuildCulture(IGuild guild);
|
||||
void RemoveGuildCulture(ulong guildId);
|
||||
void ResetDefaultCulture();
|
||||
void SetDefaultCulture(CultureInfo ci);
|
||||
void SetGuildCulture(IGuild guild, CultureInfo ci);
|
||||
void SetGuildCulture(ulong guildId, CultureInfo ci);
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
#nullable disable
|
||||
using Ellie.Services.Database.Models;
|
||||
|
||||
namespace Ellie.Modules.Utility.Services;
|
||||
|
||||
public interface IRemindService
|
||||
{
|
||||
Task AddReminderAsync(ulong userId,
|
||||
ulong targetId,
|
||||
ulong? guildId,
|
||||
bool isPrivate,
|
||||
DateTime time,
|
||||
string message,
|
||||
ReminderType reminderType);
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
#nullable disable
|
||||
namespace Ellie.Services;
|
||||
|
||||
public interface IStatsService
|
||||
{
|
||||
/// <summary>
|
||||
/// The author of the bot.
|
||||
/// </summary>
|
||||
string Author { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The total amount of commands ran since startup.
|
||||
/// </summary>
|
||||
long CommandsRan { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The amount of messages seen by the bot since startup.
|
||||
/// </summary>
|
||||
long MessageCounter { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The rate of messages the bot sees every second.
|
||||
/// </summary>
|
||||
double MessagesPerSecond { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The total amount of text channels the bot can see.
|
||||
/// </summary>
|
||||
long TextChannels { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The total amount of voice channels the bot can see.
|
||||
/// </summary>
|
||||
long VoiceChannels { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets for how long the bot has been up since startup.
|
||||
/// </summary>
|
||||
TimeSpan GetUptime();
|
||||
|
||||
/// <summary>
|
||||
/// Gets a formatted string of how long the bot has been up since startup.
|
||||
/// </summary>
|
||||
/// <param name="separator">The formatting separator.</param>
|
||||
string GetUptimeString(string separator = ", ");
|
||||
|
||||
/// <summary>
|
||||
/// Gets total amount of private memory currently in use by the bot, in Megabytes.
|
||||
/// </summary>
|
||||
double GetPrivateMemoryMegabytes();
|
||||
}
|
|
@ -1,6 +0,0 @@
|
|||
namespace Ellie.Common;
|
||||
|
||||
public interface ITimezoneService
|
||||
{
|
||||
TimeZoneInfo GetTimeZoneOrUtc(ulong? guildId);
|
||||
}
|
|
@ -1,302 +0,0 @@
|
|||
#nullable disable
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
using Ellie.Common.ModuleBehaviors;
|
||||
|
||||
namespace Ellie.Services;
|
||||
|
||||
// should be renamed to handler as it's not only executing
|
||||
public sealed class BehaviorHandler : IBehaviorHandler
|
||||
{
|
||||
private readonly IServiceProvider _services;
|
||||
|
||||
private IReadOnlyCollection<IExecNoCommand> noCommandExecs;
|
||||
private IReadOnlyCollection<IExecPreCommand> preCommandExecs;
|
||||
private IReadOnlyCollection<IExecOnMessage> onMessageExecs;
|
||||
private IReadOnlyCollection<IInputTransformer> inputTransformers;
|
||||
|
||||
private readonly SemaphoreSlim _customLock = new(1, 1);
|
||||
private readonly List<ICustomBehavior> _customExecs = new();
|
||||
|
||||
public BehaviorHandler(IServiceProvider services)
|
||||
{
|
||||
_services = services;
|
||||
}
|
||||
|
||||
public void Initialize()
|
||||
{
|
||||
noCommandExecs = _services.GetServices<IExecNoCommand>().ToArray();
|
||||
preCommandExecs = _services.GetServices<IExecPreCommand>().OrderByDescending(x => x.Priority).ToArray();
|
||||
onMessageExecs = _services.GetServices<IExecOnMessage>().OrderByDescending(x => x.Priority).ToArray();
|
||||
inputTransformers = _services.GetServices<IInputTransformer>().ToArray();
|
||||
}
|
||||
|
||||
#region Add/Remove
|
||||
|
||||
public async Task AddRangeAsync(IEnumerable<ICustomBehavior> execs)
|
||||
{
|
||||
await _customLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
foreach (var exe in execs)
|
||||
{
|
||||
if (_customExecs.Contains(exe))
|
||||
continue;
|
||||
|
||||
_customExecs.Add(exe);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_customLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> AddAsync(ICustomBehavior behavior)
|
||||
{
|
||||
await _customLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (_customExecs.Contains(behavior))
|
||||
return false;
|
||||
|
||||
_customExecs.Add(behavior);
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_customLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<bool> RemoveAsync(ICustomBehavior behavior)
|
||||
{
|
||||
await _customLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
return _customExecs.Remove(behavior);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_customLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task RemoveRangeAsync(IEnumerable<ICustomBehavior> behs)
|
||||
{
|
||||
await _customLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
foreach(var beh in behs)
|
||||
_customExecs.Remove(beh);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_customLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Running
|
||||
|
||||
public async Task<bool> RunExecOnMessageAsync(SocketGuild guild, IUserMessage usrMsg)
|
||||
{
|
||||
async Task<bool> Exec<T>(IReadOnlyCollection<T> execs)
|
||||
where T : IExecOnMessage
|
||||
{
|
||||
foreach (var exec in execs)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (await exec.ExecOnMessageAsync(guild, usrMsg))
|
||||
{
|
||||
Log.Information("{TypeName} blocked message g:{GuildId} u:{UserId} c:{ChannelId} msg:{Message}",
|
||||
GetExecName(exec),
|
||||
guild?.Id,
|
||||
usrMsg.Author.Id,
|
||||
usrMsg.Channel.Id,
|
||||
usrMsg.Content?.TrimTo(10));
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex,
|
||||
"An error occurred in {TypeName} late blocker: {ErrorMessage}",
|
||||
GetExecName(exec),
|
||||
ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (await Exec(onMessageExecs))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
await _customLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (await Exec(_customExecs))
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_customLock.Release();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private string GetExecName(IBehavior exec)
|
||||
=> exec.Name;
|
||||
|
||||
public async Task<bool> RunPreCommandAsync(ICommandContext ctx, CommandInfo cmd)
|
||||
{
|
||||
async Task<bool> Exec<T>(IReadOnlyCollection<T> execs) where T: IExecPreCommand
|
||||
{
|
||||
foreach (var exec in execs)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (await exec.ExecPreCommandAsync(ctx, cmd.Module.GetTopLevelModule().Name, cmd))
|
||||
{
|
||||
Log.Information("{TypeName} Pre-Command blocked [{User}] Command: [{Command}]",
|
||||
GetExecName(exec),
|
||||
ctx.User,
|
||||
cmd.Aliases[0]);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex,
|
||||
"An error occurred in {TypeName} PreCommand: {ErrorMessage}",
|
||||
GetExecName(exec),
|
||||
ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (await Exec(preCommandExecs))
|
||||
return true;
|
||||
|
||||
await _customLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
if (await Exec(_customExecs))
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_customLock.Release();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task RunOnNoCommandAsync(SocketGuild guild, IUserMessage usrMsg)
|
||||
{
|
||||
async Task Exec<T>(IReadOnlyCollection<T> execs) where T : IExecNoCommand
|
||||
{
|
||||
foreach (var exec in execs)
|
||||
{
|
||||
try
|
||||
{
|
||||
await exec.ExecOnNoCommandAsync(guild, usrMsg);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Error(ex,
|
||||
"An error occurred in {TypeName} OnNoCommand: {ErrorMessage}",
|
||||
GetExecName(exec),
|
||||
ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Exec(noCommandExecs);
|
||||
|
||||
await _customLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
await Exec(_customExecs);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_customLock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<string> RunInputTransformersAsync(SocketGuild guild, IUserMessage usrMsg)
|
||||
{
|
||||
async Task<string> Exec<T>(IReadOnlyCollection<T> execs, string content)
|
||||
where T : IInputTransformer
|
||||
{
|
||||
foreach (var exec in execs)
|
||||
{
|
||||
try
|
||||
{
|
||||
var newContent = await exec.TransformInput(guild, usrMsg.Channel, usrMsg.Author, content);
|
||||
if (newContent is not null)
|
||||
{
|
||||
Log.Information("{ExecName} transformed content {OldContent} -> {NewContent}",
|
||||
GetExecName(exec),
|
||||
content,
|
||||
newContent);
|
||||
return newContent;
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex, "An error occured during InputTransform handling: {ErrorMessage}", ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
var newContent = await Exec(inputTransformers, usrMsg.Content);
|
||||
if (newContent is not null)
|
||||
return newContent;
|
||||
|
||||
await _customLock.WaitAsync();
|
||||
try
|
||||
{
|
||||
newContent = await Exec(_customExecs, usrMsg.Content);
|
||||
if (newContent is not null)
|
||||
return newContent;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_customLock.Release();
|
||||
}
|
||||
|
||||
return usrMsg.Content;
|
||||
}
|
||||
|
||||
public async ValueTask RunPostCommandAsync(ICommandContext ctx, string moduleName, CommandInfo cmd)
|
||||
{
|
||||
foreach (var exec in _customExecs)
|
||||
{
|
||||
try
|
||||
{
|
||||
await exec.ExecPostCommandAsync(ctx, moduleName, cmd.Name);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Warning(ex,
|
||||
"An error occured during PostCommand handling in {ExecName}: {ErrorMessage}",
|
||||
GetExecName(exec),
|
||||
ex.Message);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
|
@ -1,136 +0,0 @@
|
|||
#nullable disable
|
||||
using LinqToDB;
|
||||
using LinqToDB.EntityFrameworkCore;
|
||||
using Ellie.Common.ModuleBehaviors;
|
||||
using Ellie.Db;
|
||||
using Ellie.Db.Models;
|
||||
using Ellie.Services.Database.Models;
|
||||
|
||||
namespace Ellie.Modules.Permissions.Services;
|
||||
|
||||
public sealed class BlacklistService : IExecOnMessage
|
||||
{
|
||||
public int Priority
|
||||
=> int.MaxValue;
|
||||
|
||||
private readonly DbService _db;
|
||||
private readonly IPubSub _pubSub;
|
||||
private readonly IBotCredentials _creds;
|
||||
private IReadOnlyList<BlacklistEntry> blacklist;
|
||||
|
||||
private readonly TypedKey<BlacklistEntry[]> _blPubKey = new("blacklist.reload");
|
||||
|
||||
public BlacklistService(DbService db, IPubSub pubSub, IBotCredentials creds)
|
||||
{
|
||||
_db = db;
|
||||
_pubSub = pubSub;
|
||||
_creds = creds;
|
||||
|
||||
Reload(false);
|
||||
_pubSub.Sub(_blPubKey, OnReload);
|
||||
}
|
||||
|
||||
private ValueTask OnReload(BlacklistEntry[] newBlacklist)
|
||||
{
|
||||
blacklist = newBlacklist;
|
||||
return default;
|
||||
}
|
||||
|
||||
public Task<bool> ExecOnMessageAsync(IGuild guild, IUserMessage usrMsg)
|
||||
{
|
||||
foreach (var bl in blacklist)
|
||||
{
|
||||
if (guild is not null && bl.Type == BlacklistType.Server && bl.ItemId == guild.Id)
|
||||
{
|
||||
Log.Information("Blocked input from blacklisted guild: {GuildName} [{GuildId}]", guild.Name, guild.Id);
|
||||
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
if (bl.Type == BlacklistType.Channel && bl.ItemId == usrMsg.Channel.Id)
|
||||
{
|
||||
Log.Information("Blocked input from blacklisted channel: {ChannelName} [{ChannelId}]",
|
||||
usrMsg.Channel.Name,
|
||||
usrMsg.Channel.Id);
|
||||
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
|
||||
if (bl.Type == BlacklistType.User && bl.ItemId == usrMsg.Author.Id)
|
||||
{
|
||||
Log.Information("Blocked input from blacklisted user: {UserName} [{UserId}]",
|
||||
usrMsg.Author.ToString(),
|
||||
usrMsg.Author.Id);
|
||||
|
||||
return Task.FromResult(true);
|
||||
}
|
||||
}
|
||||
|
||||
return Task.FromResult(false);
|
||||
}
|
||||
|
||||
public IReadOnlyList<BlacklistEntry> GetBlacklist()
|
||||
=> blacklist;
|
||||
|
||||
public void Reload(bool publish = true)
|
||||
{
|
||||
using var uow = _db.GetDbContext();
|
||||
var toPublish = uow.GetTable<BlacklistEntry>().ToArray();
|
||||
blacklist = toPublish;
|
||||
if (publish)
|
||||
_pubSub.Pub(_blPubKey, toPublish);
|
||||
}
|
||||
|
||||
public async Task Blacklist(BlacklistType type, ulong id)
|
||||
{
|
||||
if (_creds.OwnerIds.Contains(id))
|
||||
return;
|
||||
|
||||
await using var uow = _db.GetDbContext();
|
||||
|
||||
await uow
|
||||
.GetTable<BlacklistEntry>()
|
||||
.InsertAsync(() => new()
|
||||
{
|
||||
ItemId = id,
|
||||
Type = type,
|
||||
});
|
||||
|
||||
Reload();
|
||||
}
|
||||
|
||||
public async Task UnBlacklist(BlacklistType type, ulong id)
|
||||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
await uow.GetTable<BlacklistEntry>()
|
||||
.Where(bi => bi.ItemId == id && bi.Type == type)
|
||||
.DeleteAsync();
|
||||
|
||||
Reload();
|
||||
}
|
||||
|
||||
public void BlacklistUsers(IReadOnlyCollection<ulong> toBlacklist)
|
||||
{
|
||||
using (var uow = _db.GetDbContext())
|
||||
{
|
||||
var bc = uow.Set<BlacklistEntry>();
|
||||
bc.AddRange(toBlacklist.Select(x => new BlacklistEntry
|
||||
{
|
||||
ItemId = x,
|
||||
Type = BlacklistType.User
|
||||
}));
|
||||
|
||||
// todo check if blacklist works and removes currency
|
||||
uow.GetTable<DiscordUser>()
|
||||
.UpdateAsync(x => toBlacklist.Contains(x.UserId),
|
||||
_ => new()
|
||||
{
|
||||
CurrencyAmount = 0
|
||||
});
|
||||
|
||||
uow.SaveChanges();
|
||||
}
|
||||
|
||||
Reload();
|
||||
}
|
||||
}
|
|
@ -1,172 +0,0 @@
|
|||
using CommandLine;
|
||||
|
||||
namespace Ellie.Common;
|
||||
|
||||
public sealed class CommandsUtilityService : ICommandsUtilityService, IEService
|
||||
{
|
||||
private readonly CommandHandler _ch;
|
||||
private readonly IBotStrings _strings;
|
||||
private readonly DiscordPermOverrideService _dpos;
|
||||
private readonly IEmbedBuilderService _eb;
|
||||
private readonly ILocalization _loc;
|
||||
private readonly Ellie.Marmalade.IMarmaladeLoaderSevice _marmalades;
|
||||
|
||||
public CommandsUtilityService(
|
||||
CommandHandler ch,
|
||||
IBotStrings strings,
|
||||
DiscordPermOverrideService dpos,
|
||||
IEmbedBuilderService eb,
|
||||
ILocalization loc,
|
||||
Ellie.Marmalade.IMarmaladeLoaderSevice marmalades)
|
||||
{
|
||||
_ch = ch;
|
||||
_strings = strings;
|
||||
_dpos = dpos;
|
||||
_eb = eb;
|
||||
_loc = loc;
|
||||
_marmalades = marmalades;
|
||||
}
|
||||
|
||||
public IEmbedBuilder GetCommandHelp(CommandInfo com, IGuild guild)
|
||||
{
|
||||
var prefix = _ch.GetPrefix(guild);
|
||||
|
||||
var str = $"**`{prefix + com.Aliases.First()}`**";
|
||||
var alias = com.Aliases.Skip(1).FirstOrDefault();
|
||||
if (alias is not null)
|
||||
str += $" **/ `{prefix + alias}`**";
|
||||
|
||||
var culture = _loc.GetCultureInfo(guild);
|
||||
|
||||
var em = _eb.Create()
|
||||
.AddField(str, $"{com.RealSummary(_strings, _marmalades, culture, prefix)}", true);
|
||||
|
||||
_dpos.TryGetOverrides(guild?.Id ?? 0, com.Name, out var overrides);
|
||||
var reqs = GetCommandRequirements(com, (GuildPermission?)overrides);
|
||||
if (reqs.Any())
|
||||
em.AddField(GetText(strs.requires, guild), string.Join("\n", reqs));
|
||||
|
||||
em.AddField(_strings.GetText(strs.usage),
|
||||
string.Join("\n", com.RealRemarksArr(_strings, _marmalades, culture, prefix).Map(arg => Format.Code(arg))))
|
||||
.WithFooter(GetText(strs.module(com.Module.GetTopLevelModule().Name), guild))
|
||||
.WithOkColor();
|
||||
|
||||
var opt = GetEllieOptionType(com.Attributes);
|
||||
if (opt is not null)
|
||||
{
|
||||
var hs = GetCommandOptionHelp(opt);
|
||||
if (!string.IsNullOrWhiteSpace(hs))
|
||||
em.AddField(GetText(strs.options, guild), hs);
|
||||
}
|
||||
|
||||
return em;
|
||||
}
|
||||
|
||||
public static string GetCommandOptionHelp(Type opt)
|
||||
{
|
||||
var strs = GetCommandOptionHelpList(opt);
|
||||
|
||||
return string.Join("\n", strs);
|
||||
}
|
||||
|
||||
public static List<string> GetCommandOptionHelpList(Type opt)
|
||||
{
|
||||
var strs = opt.GetProperties()
|
||||
.Select(x => x.GetCustomAttributes(true).FirstOrDefault(a => a is OptionAttribute))
|
||||
.Where(x => x is not null)
|
||||
.Cast<OptionAttribute>()
|
||||
.Select(x =>
|
||||
{
|
||||
var toReturn = $"`--{x.LongName}`";
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(x.ShortName))
|
||||
toReturn += $" (`-{x.ShortName}`)";
|
||||
|
||||
toReturn += $" {x.HelpText} ";
|
||||
return toReturn;
|
||||
})
|
||||
.ToList();
|
||||
|
||||
return strs;
|
||||
}
|
||||
|
||||
public static Type? GetEllieOptionType(IEnumerable<Attribute> attributes)
|
||||
=> attributes
|
||||
.Select(a => a.GetType())
|
||||
.Where(a => a.IsGenericType
|
||||
&& a.GetGenericTypeDefinition() == typeof(EllieOptionsAttribute<>))
|
||||
.Select(a => a.GenericTypeArguments[0])
|
||||
.FirstOrDefault();
|
||||
|
||||
public static string[] GetCommandRequirements(CommandInfo cmd, GuildPerm? overrides = null)
|
||||
{
|
||||
var toReturn = new List<string>();
|
||||
|
||||
if (cmd.Preconditions.Any(x => x is OwnerOnlyAttribute))
|
||||
toReturn.Add("Bot Owner Only");
|
||||
|
||||
if (cmd.Preconditions.Any(x => x is NoPublicBotAttribute)
|
||||
|| cmd.Module
|
||||
.Preconditions
|
||||
.Any(x => x is NoPublicBotAttribute)
|
||||
|| cmd.Module.GetTopLevelModule()
|
||||
.Preconditions
|
||||
.Any(x => x is NoPublicBotAttribute))
|
||||
toReturn.Add("No Public Bot");
|
||||
|
||||
if (cmd.Preconditions
|
||||
.Any(x => x is OnlyPublicBotAttribute)
|
||||
|| cmd.Module
|
||||
.Preconditions
|
||||
.Any(x => x is OnlyPublicBotAttribute)
|
||||
|| cmd.Module.GetTopLevelModule()
|
||||
.Preconditions
|
||||
.Any(x => x is OnlyPublicBotAttribute))
|
||||
toReturn.Add("Only Public Bot");
|
||||
|
||||
var userPermString = cmd.Preconditions
|
||||
.Where(ca => ca is UserPermAttribute)
|
||||
.Cast<UserPermAttribute>()
|
||||
.Select(userPerm =>
|
||||
{
|
||||
if (userPerm.ChannelPermission is { } cPerm)
|
||||
return GetPreconditionString(cPerm);
|
||||
|
||||
if (userPerm.GuildPermission is { } gPerm)
|
||||
return GetPreconditionString(gPerm);
|
||||
|
||||
return string.Empty;
|
||||
})
|
||||
.Where(x => !string.IsNullOrWhiteSpace(x))
|
||||
.Join('\n');
|
||||
|
||||
if (overrides is null)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(userPermString))
|
||||
toReturn.Add(userPermString);
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(userPermString))
|
||||
toReturn.Add(Format.Strikethrough(userPermString));
|
||||
|
||||
toReturn.Add(GetPreconditionString(overrides.Value));
|
||||
}
|
||||
|
||||
return toReturn.ToArray();
|
||||
}
|
||||
|
||||
public static string GetPreconditionString(ChannelPerm perm)
|
||||
=> (perm + " Channel Permission").Replace("Guild", "Server");
|
||||
|
||||
public static string GetPreconditionString(GuildPerm perm)
|
||||
=> (perm + " Server Permission").Replace("Guild", "Server");
|
||||
|
||||
public string GetText(LocStr str, IGuild? guild)
|
||||
=> _strings.GetText(str, guild?.Id);
|
||||
}
|
||||
|
||||
public interface ICommandsUtilityService
|
||||
{
|
||||
IEmbedBuilder GetCommandHelp(CommandInfo com, IGuild guild);
|
||||
}
|
|
@ -1,136 +0,0 @@
|
|||
#nullable disable
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
using Ellie.Common.ModuleBehaviors;
|
||||
using Ellie.Services.Database.Models;
|
||||
|
||||
namespace Ellie.Services;
|
||||
|
||||
public class DiscordPermOverrideService : IEService, IExecPreCommand, IDiscordPermOverrideService
|
||||
{
|
||||
public int Priority { get; } = int.MaxValue;
|
||||
private readonly DbService _db;
|
||||
private readonly IServiceProvider _services;
|
||||
|
||||
private readonly ConcurrentDictionary<(ulong, string), DiscordPermOverride> _overrides;
|
||||
|
||||
public DiscordPermOverrideService(DbService db, IServiceProvider services)
|
||||
{
|
||||
_db = db;
|
||||
_services = services;
|
||||
using var uow = _db.GetDbContext();
|
||||
_overrides = uow.Set<DiscordPermOverride>()
|
||||
.AsNoTracking()
|
||||
.AsEnumerable()
|
||||
.ToDictionary(o => (o.GuildId ?? 0, o.Command), o => o)
|
||||
.ToConcurrent();
|
||||
}
|
||||
|
||||
public bool TryGetOverrides(ulong guildId, string commandName, out Ellie.Bot.Db.GuildPerm? perm)
|
||||
{
|
||||
commandName = commandName.ToLowerInvariant();
|
||||
if (_overrides.TryGetValue((guildId, commandName), out var dpo))
|
||||
{
|
||||
perm = dpo.Perm;
|
||||
return true;
|
||||
}
|
||||
|
||||
perm = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
public Task<PreconditionResult> ExecuteOverrides(
|
||||
ICommandContext ctx,
|
||||
CommandInfo command,
|
||||
GuildPerm perms,
|
||||
IServiceProvider services)
|
||||
{
|
||||
var rupa = new RequireUserPermissionAttribute(perms);
|
||||
return rupa.CheckPermissionsAsync(ctx, command, services);
|
||||
}
|
||||
|
||||
public async Task AddOverride(ulong guildId, string commandName, GuildPerm perm)
|
||||
{
|
||||
commandName = commandName.ToLowerInvariant();
|
||||
await using var uow = _db.GetDbContext();
|
||||
var over = await uow.Set<DiscordPermOverride>()
|
||||
.AsQueryable()
|
||||
.FirstOrDefaultAsync(x => x.GuildId == guildId && commandName == x.Command);
|
||||
|
||||
if (over is null)
|
||||
{
|
||||
uow.Set<DiscordPermOverride>()
|
||||
.Add(over = new()
|
||||
{
|
||||
Command = commandName,
|
||||
Perm = (Ellie.Bot.Db.GuildPerm)perm,
|
||||
GuildId = guildId
|
||||
});
|
||||
}
|
||||
else
|
||||
over.Perm = (Ellie.Bot.Db.GuildPerm)perm;
|
||||
|
||||
_overrides[(guildId, commandName)] = over;
|
||||
|
||||
await uow.SaveChangesAsync();
|
||||
}
|
||||
|
||||
public async Task ClearAllOverrides(ulong guildId)
|
||||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
var overrides = await uow.Set<DiscordPermOverride>()
|
||||
.AsQueryable()
|
||||
.AsNoTracking()
|
||||
.Where(x => x.GuildId == guildId)
|
||||
.ToListAsync();
|
||||
|
||||
uow.RemoveRange(overrides);
|
||||
await uow.SaveChangesAsync();
|
||||
|
||||
foreach (var over in overrides)
|
||||
_overrides.TryRemove((guildId, over.Command), out _);
|
||||
}
|
||||
|
||||
public async Task RemoveOverride(ulong guildId, string commandName)
|
||||
{
|
||||
commandName = commandName.ToLowerInvariant();
|
||||
|
||||
await using var uow = _db.GetDbContext();
|
||||
var over = await uow.Set<DiscordPermOverride>()
|
||||
.AsQueryable()
|
||||
.AsNoTracking()
|
||||
.FirstOrDefaultAsync(x => x.GuildId == guildId && x.Command == commandName);
|
||||
|
||||
if (over is null)
|
||||
return;
|
||||
|
||||
uow.Remove(over);
|
||||
await uow.SaveChangesAsync();
|
||||
|
||||
_overrides.TryRemove((guildId, commandName), out _);
|
||||
}
|
||||
|
||||
public async Task<List<DiscordPermOverride>> GetAllOverrides(ulong guildId)
|
||||
{
|
||||
await using var uow = _db.GetDbContext();
|
||||
return await uow.Set<DiscordPermOverride>()
|
||||
.AsQueryable()
|
||||
.AsNoTracking()
|
||||
.Where(x => x.GuildId == guildId)
|
||||
.ToListAsync();
|
||||
}
|
||||
|
||||
public async Task<bool> ExecPreCommandAsync(ICommandContext context, string moduleName, CommandInfo command)
|
||||
{
|
||||
if (TryGetOverrides(context.Guild?.Id ?? 0, command.Name, out var perm) && perm is not null)
|
||||
{
|
||||
var result =
|
||||
await new RequireUserPermissionAttribute((GuildPermission)perm).CheckPermissionsAsync(context,
|
||||
command,
|
||||
_services);
|
||||
|
||||
return !result.IsSuccess;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
|
@ -1,60 +0,0 @@
|
|||
#nullable disable
|
||||
using SixLabors.Fonts;
|
||||
|
||||
namespace Ellie.Services;
|
||||
|
||||
public class FontProvider : IEService
|
||||
{
|
||||
public FontFamily DottyFont { get; }
|
||||
|
||||
public FontFamily UniSans { get; }
|
||||
|
||||
public FontFamily NotoSans { get; }
|
||||
//public FontFamily Emojis { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Font used for .rip command
|
||||
/// </summary>
|
||||
public Font RipFont { get; }
|
||||
|
||||
public List<FontFamily> FallBackFonts { get; }
|
||||
private readonly FontCollection _fonts;
|
||||
|
||||
public FontProvider()
|
||||
{
|
||||
_fonts = new();
|
||||
|
||||
NotoSans = _fonts.Add("data/fonts/NotoSans-Bold.ttf");
|
||||
UniSans = _fonts.Add("data/fonts/Uni Sans.ttf");
|
||||
|
||||
FallBackFonts = new();
|
||||
|
||||
//FallBackFonts.Add(_fonts.Install("data/fonts/OpenSansEmoji.ttf"));
|
||||
|
||||
// try loading some emoji and jap fonts on windows as fallback fonts
|
||||
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
|
||||
{
|
||||
try
|
||||
{
|
||||
var fontsfolder = Environment.GetFolderPath(Environment.SpecialFolder.Fonts);
|
||||
FallBackFonts.Add(_fonts.Add(Path.Combine(fontsfolder, "seguiemj.ttf")));
|
||||
FallBackFonts.AddRange(_fonts.AddCollection(Path.Combine(fontsfolder, "msgothic.ttc")));
|
||||
FallBackFonts.AddRange(_fonts.AddCollection(Path.Combine(fontsfolder, "segoe.ttc")));
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
// any fonts present in data/fonts should be added as fallback fonts
|
||||
// this will allow support for special characters when drawing text
|
||||
foreach (var font in Directory.GetFiles(@"data/fonts"))
|
||||
{
|
||||
if (font.EndsWith(".ttf"))
|
||||
FallBackFonts.Add(_fonts.Add(font));
|
||||
else if (font.EndsWith(".ttc"))
|
||||
FallBackFonts.AddRange(_fonts.AddCollection(font));
|
||||
}
|
||||
|
||||
RipFont = NotoSans.CreateFont(20, FontStyle.Bold);
|
||||
DottyFont = FallBackFonts.First(x => x.Name == "dotty");
|
||||
}
|
||||
}
|
|
@ -1,17 +0,0 @@
|
|||
namespace Ellie.Services;
|
||||
|
||||
public interface IImageCache
|
||||
{
|
||||
Task<byte[]?> GetHeadsImageAsync();
|
||||
Task<byte[]?> GetTailsImageAsync();
|
||||
Task<byte[]?> GetCurrencyImageAsync();
|
||||
Task<byte[]?> GetXpBackgroundImageAsync();
|
||||
Task<byte[]?> GetRategirlBgAsync();
|
||||
Task<byte[]?> GetRategirlDotAsync();
|
||||
Task<byte[]?> GetDiceAsync(int num);
|
||||
Task<byte[]?> GetSlotEmojiAsync(int number);
|
||||
Task<byte[]?> GetSlotBgAsync();
|
||||
Task<byte[]?> GetRipBgAsync();
|
||||
Task<byte[]?> GetRipOverlayAsync();
|
||||
Task<byte[]?> GetImageDataAsync(Uri url);
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue