Full system rewrite incoming

This commit is contained in:
Toastie 2023-12-20 20:56:08 +13:00
parent 0fecee1265
commit 4cde58b3e2
No known key found for this signature in database
GPG key ID: FF47E5D4A3C8DC5E
756 changed files with 26 additions and 66237 deletions

View file

@ -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; }
}

View file

@ -1,8 +0,0 @@
namespace Ellie;
public interface IBotCredsProvider
{
public void Reload();
public IBotCredentials GetCreds();
public void ModifyCredsFile(Action<IBotCredentials> func);
}

View file

@ -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; }
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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();
}

View file

@ -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;
}
}

View file

@ -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))
{
}
}

View file

@ -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();
}
}

View file

@ -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
{
}

View file

@ -1,7 +0,0 @@
namespace Ellie.Common.Attributes;
[AttributeUsage(AttributeTargets.Method)]
public sealed class EllieOptionsAttribute<TOption> : Attribute
where TOption: IEllieCommandOptions
{
}

View file

@ -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
}
}

View file

@ -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
}
}

View file

@ -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"));
}
}

View file

@ -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);
}
}

View file

@ -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);
}
}

View file

@ -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));
}
}

View file

@ -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());
}
}
}

View file

@ -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";
}

View file

@ -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;
}
}

View file

@ -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
}

View file

@ -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);
}

View file

@ -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; }
}

View file

@ -1,5 +0,0 @@
namespace Ellie.Services.Currency;
public enum CurrencyType{
Default
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -1,7 +0,0 @@
namespace Ellie.Services.Currency;
public record class TxData(
string Type,
string Extra,
string? Note = "",
ulong? OtherId = null);

View file

@ -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();
}

View file

@ -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;
}

View file

@ -34,7 +34,8 @@
</ItemGroup> </ItemGroup>
<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> </ItemGroup>
</Project> </Project>

View file

@ -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; }
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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;

View file

@ -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; }
}

View file

@ -1,8 +0,0 @@
#nullable disable
namespace Ellie.Common;
public interface ICloneable<T>
where T : new()
{
public T Clone();
}

View file

@ -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;
}
}

View file

@ -1,7 +0,0 @@
#nullable disable
namespace Ellise.Common;
public interface IDiscordPermOverrideService
{
bool TryGetOverrides(ulong guildId, string commandName, out Ellie.Bot.Db.GuildPerm? perm);
}

View file

@ -1,7 +0,0 @@
#nullable disable
namespace Ellie.Common;
public interface IEllieCommandOptions
{
void NormalizeOptions();
}

View file

@ -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
}

View file

@ -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);
}

View file

@ -1,7 +0,0 @@
#nullable disable
namespace Ellie.Common;
public interface IPlaceholderProvider
{
public IEnumerable<(string Name, Func<string> Func)> GetPlaceholders();
}

View file

@ -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);
}

View file

@ -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);

View file

@ -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);
}

View file

@ -1,8 +0,0 @@
namespace Ellie;
public interface IEllieInteractionService
{
public EllieInteraction Create<T>(
ulong userId,
SimpleInteraction<T> inter);
}

View file

@ -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!);
}
}

View file

@ -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);

View file

@ -1,10 +0,0 @@
namespace Ellie.Marmalade;
public enum MarmaladeLoadResult
{
Success,
NotFound,
AlreadyLoaded,
Empty,
UnknownError,
}

View file

@ -1,9 +0,0 @@
namespace Ellie.Marmalade;
public enum MarmaladeUnloadResult
{
Success,
NotLoaded,
PossiblyUnable,
NotFound,
}

View file

@ -1,8 +0,0 @@
namespace Ellie.Common;
public enum MsgType
{
Ok,
Pending,
Error
}

View file

@ -1,6 +0,0 @@
namespace Ellie.Common.ModuleBehaviors;
public interface IBehavior
{
public virtual string Name => this.GetType().Name;
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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();
}

View file

@ -1,7 +0,0 @@
namespace Ellie.Modules.Patronage;
public readonly struct FeatureLimitKey
{
public string PrettyName { get; init; }
public string Key { get; init; }
}

View file

@ -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; }
}

View file

@ -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; }
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}

View file

@ -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();
}
}

View file

@ -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}";
}
}

View file

@ -1,14 +0,0 @@
// ReSharper disable InconsistentNaming
namespace Ellie.Modules.Patronage;
public enum PatronTier
{
None,
I,
V,
X,
XX,
L,
C,
ComingSoon
}

View file

@ -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()
{
}
}

View file

@ -1,8 +0,0 @@
namespace Ellie.Modules.Patronage;
public enum QuotaPer
{
PerHour,
PerDay,
PerMonth,
}

View file

@ -1,10 +0,0 @@
#nullable disable
namespace Ellie.Modules.Patronage;
public enum SubscriptionChargeStatus
{
Paid,
Refunded,
Unpaid,
Other,
}

View file

@ -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()
{
}
}

View file

@ -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;
}
}

View file

@ -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;
}
}

View file

@ -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);
}
}

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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
});
}
}
}

View file

@ -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();
}
}

View file

@ -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();
}

View file

@ -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);
}

View file

@ -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; }
}

View file

@ -1,13 +0,0 @@
using Ellie.Common.ModuleBehaviors;
namespace Ellie.Services;
public interface ICustomBehavior
: IExecOnMessage,
IInputTransformer,
IExecPreCommand,
IExecNoCommand,
IExecPostCommand
{
}

View file

@ -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
{
}

View file

@ -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;
}
}

View file

@ -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);
}

View file

@ -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();
}

View file

@ -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);
}

View file

@ -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);
}

View file

@ -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();
}

View file

@ -1,6 +0,0 @@
namespace Ellie.Common;
public interface ITimezoneService
{
TimeZoneInfo GetTimeZoneOrUtc(ulong? guildId);
}

View file

@ -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
}

View file

@ -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();
}
}

View file

@ -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);
}

View file

@ -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;
}
}

View file

@ -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");
}
}

View file

@ -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