mirror of
				https://gitlab.com/Kwoth/nadekobot.git
				synced 2025-11-04 00:34:26 -05:00 
			
		
		
		
	NadekoBot Patronage system, Search commands improvements + fixes
This commit is contained in:
		@@ -4,9 +4,11 @@ using NadekoBot.Common.Configs;
 | 
			
		||||
using NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
using NadekoBot.Db;
 | 
			
		||||
using NadekoBot.Modules.Administration;
 | 
			
		||||
using NadekoBot.Modules.Utility;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
using System.Collections.Immutable;
 | 
			
		||||
using System.Diagnostics;
 | 
			
		||||
using System.Net;
 | 
			
		||||
using System.Reflection;
 | 
			
		||||
using RunMode = Discord.Commands.RunMode;
 | 
			
		||||
 | 
			
		||||
@@ -125,6 +127,12 @@ public sealed class Bot
 | 
			
		||||
            {
 | 
			
		||||
                AllowAutoRedirect = false
 | 
			
		||||
            });
 | 
			
		||||
        
 | 
			
		||||
        svcs.AddHttpClient("google:search")
 | 
			
		||||
            .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler()
 | 
			
		||||
            {
 | 
			
		||||
                AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
        if (Environment.GetEnvironmentVariable("NADEKOBOT_IS_COORDINATED") != "1")
 | 
			
		||||
            svcs.AddSingleton<ICoordinator, SingleProcessCoordinator>();
 | 
			
		||||
@@ -164,6 +172,7 @@ public sealed class Bot
 | 
			
		||||
        //initialize Services
 | 
			
		||||
        Services = svcs.BuildServiceProvider();
 | 
			
		||||
        Services.GetRequiredService<IBehaviorHandler>().Initialize();
 | 
			
		||||
        Services.GetRequiredService<CurrencyRewardService>();
 | 
			
		||||
 | 
			
		||||
        if (Client.ShardId == 0)
 | 
			
		||||
            ApplyConfigMigrations();
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,11 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Common;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Classed marked with this attribute will not be added to the service provider 
 | 
			
		||||
/// </summary>
 | 
			
		||||
[AttributeUsage(AttributeTargets.Class)]
 | 
			
		||||
public class DontAddToIocContainerAttribute : Attribute
 | 
			
		||||
{
 | 
			
		||||
    
 | 
			
		||||
}
 | 
			
		||||
@@ -20,11 +20,19 @@ public sealed class NoPublicBotAttribute : PreconditionAttribute
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Classed marked with this attribute will not be added to the service provider 
 | 
			
		||||
/// </summary>
 | 
			
		||||
[AttributeUsage(AttributeTargets.Class)]
 | 
			
		||||
public class DontAddToIocContainerAttribute : Attribute
 | 
			
		||||
[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
 | 
			
		||||
[SuppressMessage("Style", "IDE0022:Use expression body for methods")]
 | 
			
		||||
public sealed class OnlyPublicBotAttribute : PreconditionAttribute
 | 
			
		||||
{
 | 
			
		||||
    
 | 
			
		||||
    public override Task<PreconditionResult> CheckPermissionsAsync(
 | 
			
		||||
        ICommandContext context,
 | 
			
		||||
        CommandInfo command,
 | 
			
		||||
        IServiceProvider services)
 | 
			
		||||
    {
 | 
			
		||||
#if GLOBAL_NADEKO || DEBUG
 | 
			
		||||
        return Task.FromResult(PreconditionResult.FromSuccess());
 | 
			
		||||
#else
 | 
			
		||||
        return Task.FromResult(PreconditionResult.FromError("Only available on the public bot."));
 | 
			
		||||
#endif
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -18,7 +18,7 @@ public sealed class Creds : IBotCredentials
 | 
			
		||||
    [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 running on.
 | 
			
		||||
    [Comment(@"The number of shards that the bot will be running on.
 | 
			
		||||
Leave at 1 if you don't know what you're doing.")]
 | 
			
		||||
    public int TotalShards { get; set; }
 | 
			
		||||
 | 
			
		||||
@@ -27,6 +27,16 @@ Leave at 1 if you don't know what you're doing.")]
 | 
			
		||||
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 Nadeko.")]
 | 
			
		||||
    public VotesSettings Votes { get; set; }
 | 
			
		||||
@@ -119,6 +129,7 @@ Windows default
 | 
			
		||||
        CoordinatorUrl = "http://localhost:3442";
 | 
			
		||||
 | 
			
		||||
        RestartCommand = new();
 | 
			
		||||
        Google = new();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -200,4 +211,10 @@ This should be equivalent to the DiscordsKey in your NadekoBot.Votes api appsett
 | 
			
		||||
            DiscordsKey = discordsKey;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class GoogleApiConfig
 | 
			
		||||
{
 | 
			
		||||
    public string SearchId { get; init; }
 | 
			
		||||
    public string ImageSearchId { get; init; }
 | 
			
		||||
}
 | 
			
		||||
@@ -25,6 +25,7 @@ public interface IBotCredentials
 | 
			
		||||
    string CoordinatorUrl { get; set; }
 | 
			
		||||
    string TwitchClientId { get; set; }
 | 
			
		||||
    string TwitchClientSecret { get; set; }
 | 
			
		||||
    GoogleApiConfig Google { get; set; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class RestartConfig
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,11 @@
 | 
			
		||||
namespace NadekoBot;
 | 
			
		||||
 | 
			
		||||
public sealed class NadekoActionInteraction : NadekoOwnInteraction
 | 
			
		||||
public sealed class NadekoButtonActionInteraction : NadekoButtonOwnInteraction
 | 
			
		||||
{
 | 
			
		||||
    private readonly NadekoInteractionData _data;
 | 
			
		||||
    private readonly Func<SocketMessageComponent, Task> _action;
 | 
			
		||||
 | 
			
		||||
    public NadekoActionInteraction(
 | 
			
		||||
    public NadekoButtonActionInteraction(
 | 
			
		||||
        DiscordSocketClient client,
 | 
			
		||||
        ulong authorId,
 | 
			
		||||
        NadekoInteractionData data,
 | 
			
		||||
@@ -17,10 +17,12 @@ public sealed class NadekoActionInteraction : NadekoOwnInteraction
 | 
			
		||||
        _action = action;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override string Name
 | 
			
		||||
    protected override string Name
 | 
			
		||||
        => _data.CustomId;
 | 
			
		||||
    public override IEmote Emote
 | 
			
		||||
    protected override IEmote Emote
 | 
			
		||||
        => _data.Emote;
 | 
			
		||||
    protected override string? Text
 | 
			
		||||
        => _data.Text;
 | 
			
		||||
 | 
			
		||||
    public override Task ExecuteOnActionAsync(SocketMessageComponent smc)
 | 
			
		||||
        => _action(smc);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,23 +1,24 @@
 | 
			
		||||
namespace NadekoBot;
 | 
			
		||||
 | 
			
		||||
public abstract class NadekoInteraction
 | 
			
		||||
public abstract class NadekoButtonInteraction
 | 
			
		||||
{
 | 
			
		||||
    // improvements:
 | 
			
		||||
    //  - state in OnAction
 | 
			
		||||
    //  - configurable delay
 | 
			
		||||
    //  - 
 | 
			
		||||
    public abstract string Name { get; }
 | 
			
		||||
    public abstract IEmote Emote { get; }
 | 
			
		||||
    protected abstract string Name { get; }
 | 
			
		||||
    protected abstract IEmote Emote { get; }
 | 
			
		||||
    protected virtual string? Text { get; } = null;
 | 
			
		||||
 | 
			
		||||
    protected readonly DiscordSocketClient _client;
 | 
			
		||||
    public DiscordSocketClient Client { get; }
 | 
			
		||||
 | 
			
		||||
    protected readonly TaskCompletionSource<bool> _interactionCompletedSource;
 | 
			
		||||
 | 
			
		||||
    protected IUserMessage message = null!;
 | 
			
		||||
 | 
			
		||||
    protected NadekoInteraction(DiscordSocketClient client)
 | 
			
		||||
    protected NadekoButtonInteraction(DiscordSocketClient client)
 | 
			
		||||
    {
 | 
			
		||||
        _client = client;
 | 
			
		||||
        Client = client;
 | 
			
		||||
        _interactionCompletedSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -25,9 +26,9 @@ public abstract class NadekoInteraction
 | 
			
		||||
    {
 | 
			
		||||
        message = msg;
 | 
			
		||||
 | 
			
		||||
        _client.InteractionCreated += OnInteraction;
 | 
			
		||||
        Client.InteractionCreated += OnInteraction;
 | 
			
		||||
        await Task.WhenAny(Task.Delay(10_000), _interactionCompletedSource.Task);
 | 
			
		||||
        _client.InteractionCreated -= OnInteraction;
 | 
			
		||||
        Client.InteractionCreated -= OnInteraction;
 | 
			
		||||
 | 
			
		||||
        await msg.ModifyAsync(m => m.Components = new ComponentBuilder().Build());
 | 
			
		||||
    }
 | 
			
		||||
@@ -65,13 +66,18 @@ public abstract class NadekoInteraction
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public MessageComponent CreateComponent()
 | 
			
		||||
    public virtual MessageComponent CreateComponent()
 | 
			
		||||
    {
 | 
			
		||||
        var comp = new ComponentBuilder()
 | 
			
		||||
            .WithButton(new ButtonBuilder(style: ButtonStyle.Secondary, emote: Emote, customId: Name));
 | 
			
		||||
            .WithButton(GetButtonBuilder());
 | 
			
		||||
 | 
			
		||||
        return comp.Build();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public ButtonBuilder GetButtonBuilder()
 | 
			
		||||
        => new ButtonBuilder(style: ButtonStyle.Secondary, emote: Emote, customId: Name, label: Text);
 | 
			
		||||
 | 
			
		||||
    public abstract Task ExecuteOnActionAsync(SocketMessageComponent smc);
 | 
			
		||||
}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// this is all so wrong ...
 | 
			
		||||
							
								
								
									
										43
									
								
								src/NadekoBot/Common/Interaction/NadekoInteractionArray.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								src/NadekoBot/Common/Interaction/NadekoInteractionArray.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
			
		||||
// namespace NadekoBot;
 | 
			
		||||
//
 | 
			
		||||
// public class NadekoButtonInteractionArray : NadekoButtonInteraction
 | 
			
		||||
// {
 | 
			
		||||
//     private readonly ButtonBuilder[] _bbs;
 | 
			
		||||
//     private readonly NadekoButtonInteraction[] _inters;
 | 
			
		||||
//
 | 
			
		||||
//     public NadekoButtonInteractionArray(params NadekoButtonInteraction[] inters)
 | 
			
		||||
//         : base(inters[0].Client)
 | 
			
		||||
//     {
 | 
			
		||||
//         _inters = inters;
 | 
			
		||||
//         _bbs = inters.Map(x => x.GetButtonBuilder());
 | 
			
		||||
//     }
 | 
			
		||||
//
 | 
			
		||||
//     protected override string Name
 | 
			
		||||
//         => throw new NotSupportedException();
 | 
			
		||||
//     protected override IEmote Emote
 | 
			
		||||
//         => throw new NotSupportedException();
 | 
			
		||||
//
 | 
			
		||||
//     protected override ValueTask<bool> Validate(SocketMessageComponent smc)
 | 
			
		||||
//         => new(true);
 | 
			
		||||
//
 | 
			
		||||
//     public override Task ExecuteOnActionAsync(SocketMessageComponent smc)
 | 
			
		||||
//     {
 | 
			
		||||
//         for (var i = 0; i < _bbs.Length; i++)
 | 
			
		||||
//         {
 | 
			
		||||
//             if (_bbs[i].CustomId == smc.Data.CustomId)
 | 
			
		||||
//                 return _inters[i].ExecuteOnActionAsync(smc);
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         return Task.CompletedTask;
 | 
			
		||||
//     }
 | 
			
		||||
//
 | 
			
		||||
//     public override MessageComponent CreateComponent()
 | 
			
		||||
//     {
 | 
			
		||||
//         var comp = new ComponentBuilder();
 | 
			
		||||
//
 | 
			
		||||
//         foreach (var bb in _bbs)
 | 
			
		||||
//             comp.WithButton(bb);
 | 
			
		||||
//         
 | 
			
		||||
//         return comp.Build();
 | 
			
		||||
//     }
 | 
			
		||||
// }
 | 
			
		||||
@@ -20,6 +20,7 @@ public class NadekoInteractionBuilder
 | 
			
		||||
    // {
 | 
			
		||||
    //     this.isOwn = isOwn;
 | 
			
		||||
    //     return this;
 | 
			
		||||
    
 | 
			
		||||
    // }
 | 
			
		||||
    
 | 
			
		||||
    public NadekoInteractionBuilder WithAction(in Func<SocketMessageComponent, Task> fn)
 | 
			
		||||
@@ -28,7 +29,7 @@ public class NadekoInteractionBuilder
 | 
			
		||||
        return this;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public NadekoActionInteraction Build(DiscordSocketClient client, ulong userId)
 | 
			
		||||
    public NadekoButtonActionInteraction Build(DiscordSocketClient client, ulong userId)
 | 
			
		||||
    {
 | 
			
		||||
        if (iData is null)
 | 
			
		||||
            throw new InvalidOperationException("You have to specify the data before building the interaction");
 | 
			
		||||
 
 | 
			
		||||
@@ -5,4 +5,4 @@
 | 
			
		||||
/// </summary>
 | 
			
		||||
/// <param name="Emote">Emote which will show on a button</param>
 | 
			
		||||
/// <param name="CustomId">Custom interaction id</param>
 | 
			
		||||
public record NadekoInteractionData(IEmote Emote, string CustomId);
 | 
			
		||||
public record NadekoInteractionData(IEmote Emote, string CustomId, string? Text = null);
 | 
			
		||||
@@ -3,11 +3,11 @@
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Interaction which only the author can use
 | 
			
		||||
/// </summary>
 | 
			
		||||
public abstract class NadekoOwnInteraction : NadekoInteraction
 | 
			
		||||
public abstract class NadekoButtonOwnInteraction : NadekoButtonInteraction
 | 
			
		||||
{
 | 
			
		||||
    protected readonly ulong _authorId;
 | 
			
		||||
 | 
			
		||||
    protected NadekoOwnInteraction(DiscordSocketClient client, ulong authorId) : base(client)
 | 
			
		||||
    protected NadekoButtonOwnInteraction(DiscordSocketClient client, ulong authorId) : base(client)
 | 
			
		||||
        => _authorId = authorId;
 | 
			
		||||
 | 
			
		||||
    protected override ValueTask<bool> Validate(SocketMessageComponent smc)
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										26
									
								
								src/NadekoBot/Common/NInteraction.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/NadekoBot/Common/NInteraction.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
namespace NadekoBot.Common;
 | 
			
		||||
 | 
			
		||||
public abstract class NInteraction
 | 
			
		||||
{
 | 
			
		||||
    private readonly DiscordSocketClient _client;
 | 
			
		||||
    private readonly ulong _userId;
 | 
			
		||||
    private readonly Func<SocketMessageComponent, Task> _action;
 | 
			
		||||
 | 
			
		||||
    protected abstract NadekoInteractionData Data { get; }
 | 
			
		||||
 | 
			
		||||
    public NInteraction(
 | 
			
		||||
        DiscordSocketClient client,
 | 
			
		||||
        ulong userId,
 | 
			
		||||
        Func<SocketMessageComponent, Task> action)
 | 
			
		||||
    {
 | 
			
		||||
        _client = client;
 | 
			
		||||
        _userId = userId;
 | 
			
		||||
        _action = action;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public NadekoButtonInteraction GetInteraction()
 | 
			
		||||
        => new NadekoInteractionBuilder()
 | 
			
		||||
           .WithData(Data)
 | 
			
		||||
           .WithAction(_action)
 | 
			
		||||
           .Build(_client, _userId);
 | 
			
		||||
}
 | 
			
		||||
@@ -36,7 +36,7 @@ public abstract class NadekoModule : ModuleBase
 | 
			
		||||
        string error,
 | 
			
		||||
        string url = null,
 | 
			
		||||
        string footer = null, 
 | 
			
		||||
        NadekoInteraction inter = null)
 | 
			
		||||
        NadekoButtonInteraction inter = null)
 | 
			
		||||
        => ctx.Channel.SendErrorAsync(_eb, title, error, url, footer);
 | 
			
		||||
    
 | 
			
		||||
    public Task<IUserMessage> SendConfirmAsync(
 | 
			
		||||
@@ -47,32 +47,32 @@ public abstract class NadekoModule : ModuleBase
 | 
			
		||||
        => ctx.Channel.SendConfirmAsync(_eb, title, text, url, footer);
 | 
			
		||||
 | 
			
		||||
    // 
 | 
			
		||||
    public Task<IUserMessage> SendErrorAsync(string text, NadekoInteraction inter = null)
 | 
			
		||||
    public Task<IUserMessage> SendErrorAsync(string text, NadekoButtonInteraction inter = null)
 | 
			
		||||
        => ctx.Channel.SendAsync(_eb, text, MessageType.Error, inter);
 | 
			
		||||
    public Task<IUserMessage> SendConfirmAsync(string text, NadekoInteraction inter = null)
 | 
			
		||||
    public Task<IUserMessage> SendConfirmAsync(string text, NadekoButtonInteraction inter = null)
 | 
			
		||||
        => ctx.Channel.SendAsync(_eb, text, MessageType.Ok, inter);
 | 
			
		||||
    public Task<IUserMessage> SendPendingAsync(string text, NadekoInteraction inter = null)
 | 
			
		||||
    public Task<IUserMessage> SendPendingAsync(string text, NadekoButtonInteraction inter = null)
 | 
			
		||||
        => ctx.Channel.SendAsync(_eb, text, MessageType.Pending, inter);
 | 
			
		||||
 | 
			
		||||
    
 | 
			
		||||
    // localized normal
 | 
			
		||||
    public Task<IUserMessage> ErrorLocalizedAsync(LocStr str, NadekoInteraction inter = null)
 | 
			
		||||
    public Task<IUserMessage> ErrorLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null)
 | 
			
		||||
        => SendErrorAsync(GetText(str), inter);
 | 
			
		||||
 | 
			
		||||
    public Task<IUserMessage> PendingLocalizedAsync(LocStr str, NadekoInteraction inter = null)
 | 
			
		||||
    public Task<IUserMessage> PendingLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null)
 | 
			
		||||
        => SendPendingAsync(GetText(str), inter);
 | 
			
		||||
 | 
			
		||||
    public Task<IUserMessage> ConfirmLocalizedAsync(LocStr str, NadekoInteraction inter = null)
 | 
			
		||||
    public Task<IUserMessage> ConfirmLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null)
 | 
			
		||||
        => SendConfirmAsync(GetText(str), inter);
 | 
			
		||||
 | 
			
		||||
    // localized replies
 | 
			
		||||
    public Task<IUserMessage> ReplyErrorLocalizedAsync(LocStr str, NadekoInteraction inter = null)
 | 
			
		||||
    public Task<IUserMessage> ReplyErrorLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null)
 | 
			
		||||
        => SendErrorAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}");
 | 
			
		||||
 | 
			
		||||
    public Task<IUserMessage> ReplyPendingLocalizedAsync(LocStr str, NadekoInteraction inter = null)
 | 
			
		||||
    public Task<IUserMessage> ReplyPendingLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null)
 | 
			
		||||
        => SendPendingAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}");
 | 
			
		||||
 | 
			
		||||
    public Task<IUserMessage> ReplyConfirmLocalizedAsync(LocStr str, NadekoInteraction inter = null)
 | 
			
		||||
    public Task<IUserMessage> ReplyConfirmLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null)
 | 
			
		||||
        => SendConfirmAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}");
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> PromptUserConfirmAsync(IEmbedBuilder embed)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Newtonsoft.Json;
 | 
			
		||||
using Newtonsoft.Json.Linq;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot;
 | 
			
		||||
@@ -45,7 +44,6 @@ public abstract record SmartText
 | 
			
		||||
            _ => throw new ArgumentOutOfRangeException(nameof(text))
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
    [CanBeNull]
 | 
			
		||||
    public static SmartText CreateFrom(string input)
 | 
			
		||||
    {
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(input))
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,8 @@ using NadekoBot.Services.Database.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Db.Models;
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
// FUTURE remove LastLevelUp from here and UserXpStats
 | 
			
		||||
public class DiscordUser : DbEntity
 | 
			
		||||
{
 | 
			
		||||
    public ulong UserId { get; set; }
 | 
			
		||||
 
 | 
			
		||||
@@ -84,7 +84,7 @@ public class GuildConfig : DbEntity
 | 
			
		||||
 | 
			
		||||
    public List<ShopEntry> ShopEntries { get; set; }
 | 
			
		||||
    public ulong? GameVoiceChannel { get; set; }
 | 
			
		||||
    public bool VerboseErrors { get; set; }
 | 
			
		||||
    public bool VerboseErrors { get; set; } = true;
 | 
			
		||||
 | 
			
		||||
    public StreamRoleSettings StreamRole { get; set; }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										38
									
								
								src/NadekoBot/Db/Models/PatronQuota.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										38
									
								
								src/NadekoBot/Db/Models/PatronQuota.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,38 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Db.Models;
 | 
			
		||||
 | 
			
		||||
/// <summary>
 | 
			
		||||
/// Contains data about usage of Patron-Only commands per user
 | 
			
		||||
/// in order to provide support for quota limitations
 | 
			
		||||
/// (allow user x who is pledging amount y to use the specified command only
 | 
			
		||||
///  x amount of times in the specified time period)
 | 
			
		||||
/// </summary>
 | 
			
		||||
public class PatronQuota
 | 
			
		||||
{
 | 
			
		||||
    public ulong UserId { get; set; }
 | 
			
		||||
    public FeatureType FeatureType { get; set; }
 | 
			
		||||
    public string Feature { get; set; }
 | 
			
		||||
    public uint HourlyCount { get; set; }
 | 
			
		||||
    public uint DailyCount { get; set; }
 | 
			
		||||
    public uint MonthlyCount { get; set; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public enum FeatureType
 | 
			
		||||
{
 | 
			
		||||
    Command,
 | 
			
		||||
    Group,
 | 
			
		||||
    Module,
 | 
			
		||||
    Limit
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class PatronUser
 | 
			
		||||
{
 | 
			
		||||
    public string UniquePlatformUserId { get; set; }
 | 
			
		||||
    public ulong UserId { get; set; }
 | 
			
		||||
    public int AmountCents { get; set; }
 | 
			
		||||
    
 | 
			
		||||
    public DateTime LastCharge { get; set; }
 | 
			
		||||
    
 | 
			
		||||
    // Date Only component
 | 
			
		||||
    public DateTime ValidThru { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -4,7 +4,7 @@ namespace NadekoBot.Services.Database.Models;
 | 
			
		||||
public class RewardedUser : DbEntity
 | 
			
		||||
{
 | 
			
		||||
    public ulong UserId { get; set; }
 | 
			
		||||
    public string PatreonUserId { get; set; }
 | 
			
		||||
    public int AmountRewardedThisMonth { get; set; }
 | 
			
		||||
    public string PlatformUserId { get; set; }
 | 
			
		||||
    public long AmountRewardedThisMonth { get; set; }
 | 
			
		||||
    public DateTime LastReward { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Design;
 | 
			
		||||
using Microsoft.Extensions.Logging;
 | 
			
		||||
using NadekoBot.Db.Models;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
@@ -26,7 +25,7 @@ public abstract class NadekoContext : DbContext
 | 
			
		||||
    public DbSet<ClubInfo> Clubs { get; set; }
 | 
			
		||||
    public DbSet<ClubBans> ClubBans { get; set; }
 | 
			
		||||
    public DbSet<ClubApplicants> ClubApplicants { get; set; }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    //logging
 | 
			
		||||
    public DbSet<LogSetting> LogSettings { get; set; }
 | 
			
		||||
@@ -51,19 +50,23 @@ public abstract class NadekoContext : DbContext
 | 
			
		||||
    public DbSet<AutoTranslateUser> AutoTranslateUsers { get; set; }
 | 
			
		||||
 | 
			
		||||
    public DbSet<Permissionv2> Permissions { get; set; }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    public DbSet<BankUser> BankUsers { get; set; }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    public DbSet<ReactionRoleV2> ReactionRoles { get; set; }
 | 
			
		||||
 | 
			
		||||
    public DbSet<PatronUser> Patrons { get; set; }
 | 
			
		||||
 | 
			
		||||
    public DbSet<PatronQuota> PatronQuotas { get; set; }
 | 
			
		||||
 | 
			
		||||
    #region Mandatory Provider-Specific Values
 | 
			
		||||
 | 
			
		||||
    protected abstract string CurrencyTransactionOtherIdDefaultValue { get; }
 | 
			
		||||
    protected abstract string DiscordUserLastXpGainDefaultValue { get; }
 | 
			
		||||
    protected abstract string LastLevelUpDefaultValue { get; }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    #endregion
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    protected override void OnModelCreating(ModelBuilder modelBuilder)
 | 
			
		||||
    {
 | 
			
		||||
        #region QUOTES
 | 
			
		||||
@@ -77,7 +80,11 @@ public abstract class NadekoContext : DbContext
 | 
			
		||||
        #region GuildConfig
 | 
			
		||||
 | 
			
		||||
        var configEntity = modelBuilder.Entity<GuildConfig>();
 | 
			
		||||
        configEntity.HasIndex(c => c.GuildId).IsUnique();
 | 
			
		||||
        configEntity.HasIndex(c => c.GuildId)
 | 
			
		||||
                    .IsUnique();
 | 
			
		||||
 | 
			
		||||
        configEntity.Property(x => x.VerboseErrors)
 | 
			
		||||
                    .HasDefaultValue(true);
 | 
			
		||||
 | 
			
		||||
        modelBuilder.Entity<AntiSpamSetting>().HasOne(x => x.GuildConfig).WithOne(x => x.AntiSpamSetting);
 | 
			
		||||
 | 
			
		||||
@@ -193,13 +200,6 @@ public abstract class NadekoContext : DbContext
 | 
			
		||||
 | 
			
		||||
        #endregion
 | 
			
		||||
 | 
			
		||||
        #region PatreonRewards
 | 
			
		||||
 | 
			
		||||
        var pr = modelBuilder.Entity<RewardedUser>();
 | 
			
		||||
        pr.HasIndex(x => x.PatreonUserId).IsUnique();
 | 
			
		||||
 | 
			
		||||
        #endregion
 | 
			
		||||
 | 
			
		||||
        #region XpStats
 | 
			
		||||
 | 
			
		||||
        var xps = modelBuilder.Entity<UserXpStats>();
 | 
			
		||||
@@ -369,12 +369,13 @@ public abstract class NadekoContext : DbContext
 | 
			
		||||
               .IsUnique(false);
 | 
			
		||||
 | 
			
		||||
            rr2.HasIndex(x => new
 | 
			
		||||
            {
 | 
			
		||||
                x.MessageId,
 | 
			
		||||
                x.Emote
 | 
			
		||||
            }).IsUnique();
 | 
			
		||||
               {
 | 
			
		||||
                   x.MessageId,
 | 
			
		||||
                   x.Emote
 | 
			
		||||
               })
 | 
			
		||||
               .IsUnique();
 | 
			
		||||
        });
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        #endregion
 | 
			
		||||
 | 
			
		||||
        #region LogSettings
 | 
			
		||||
@@ -419,7 +420,37 @@ public abstract class NadekoContext : DbContext
 | 
			
		||||
        modelBuilder.Entity<BankUser>(bu => bu.HasIndex(x => x.UserId).IsUnique());
 | 
			
		||||
 | 
			
		||||
        #endregion
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        #region Patron
 | 
			
		||||
 | 
			
		||||
        // currency rewards
 | 
			
		||||
        var pr = modelBuilder.Entity<RewardedUser>();
 | 
			
		||||
        pr.HasIndex(x => x.PlatformUserId).IsUnique();
 | 
			
		||||
 | 
			
		||||
        // patrons
 | 
			
		||||
        // patrons are not identified by their user id, but by their platform user id
 | 
			
		||||
        // as multiple accounts (even maybe on different platforms) could have
 | 
			
		||||
        // the same account connected to them
 | 
			
		||||
        modelBuilder.Entity<PatronUser>(pu =>
 | 
			
		||||
        {
 | 
			
		||||
            pu.HasIndex(x => x.UniquePlatformUserId).IsUnique();
 | 
			
		||||
            pu.HasKey(x => x.UserId);
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        // quotes are per user id
 | 
			
		||||
        modelBuilder.Entity<PatronQuota>(pq =>
 | 
			
		||||
        {
 | 
			
		||||
            pq.HasIndex(x => x.UserId).IsUnique(false);
 | 
			
		||||
            pq.HasKey(x => new
 | 
			
		||||
            {
 | 
			
		||||
                x.UserId,
 | 
			
		||||
                x.FeatureType,
 | 
			
		||||
                x.Feature
 | 
			
		||||
            });
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        #endregion
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
#if DEBUG
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3481
									
								
								src/NadekoBot/Migrations/MySql/20220614071410_patronage-system.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										3481
									
								
								src/NadekoBot/Migrations/MySql/20220614071410_patronage-system.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -0,0 +1,176 @@
 | 
			
		||||
using System;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Metadata;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Migrations.Mysql
 | 
			
		||||
{
 | 
			
		||||
    public partial class patronagesystem : Migration
 | 
			
		||||
    {
 | 
			
		||||
        protected override void Up(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.RenameColumn(
 | 
			
		||||
                name: "patreonuserid",
 | 
			
		||||
                table: "rewardedusers",
 | 
			
		||||
                newName: "platformuserid");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.RenameIndex(
 | 
			
		||||
                name: "ix_rewardedusers_patreonuserid",
 | 
			
		||||
                table: "rewardedusers",
 | 
			
		||||
                newName: "ix_rewardedusers_platformuserid");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<long>(
 | 
			
		||||
                name: "xp",
 | 
			
		||||
                table: "userxpstats",
 | 
			
		||||
                type: "bigint",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                oldClrType: typeof(int),
 | 
			
		||||
                oldType: "int");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<long>(
 | 
			
		||||
                name: "awardedxp",
 | 
			
		||||
                table: "userxpstats",
 | 
			
		||||
                type: "bigint",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                oldClrType: typeof(int),
 | 
			
		||||
                oldType: "int");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<long>(
 | 
			
		||||
                name: "amountrewardedthismonth",
 | 
			
		||||
                table: "rewardedusers",
 | 
			
		||||
                type: "bigint",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                oldClrType: typeof(int),
 | 
			
		||||
                oldType: "int");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<bool>(
 | 
			
		||||
                name: "verboseerrors",
 | 
			
		||||
                table: "guildconfigs",
 | 
			
		||||
                type: "tinyint(1)",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                defaultValue: true,
 | 
			
		||||
                oldClrType: typeof(bool),
 | 
			
		||||
                oldType: "tinyint(1)");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<long>(
 | 
			
		||||
                name: "totalxp",
 | 
			
		||||
                table: "discorduser",
 | 
			
		||||
                type: "bigint",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                defaultValue: 0L,
 | 
			
		||||
                oldClrType: typeof(int),
 | 
			
		||||
                oldType: "int",
 | 
			
		||||
                oldDefaultValue: 0);
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateTable(
 | 
			
		||||
                name: "patronquotas",
 | 
			
		||||
                columns: table => new
 | 
			
		||||
                {
 | 
			
		||||
                    userid = table.Column<ulong>(type: "bigint unsigned", nullable: false),
 | 
			
		||||
                    featuretype = table.Column<int>(type: "int", nullable: false),
 | 
			
		||||
                    feature = table.Column<string>(type: "varchar(255)", nullable: false)
 | 
			
		||||
                        .Annotation("MySql:CharSet", "utf8mb4"),
 | 
			
		||||
                    hourlycount = table.Column<uint>(type: "int unsigned", nullable: false),
 | 
			
		||||
                    dailycount = table.Column<uint>(type: "int unsigned", nullable: false),
 | 
			
		||||
                    monthlycount = table.Column<uint>(type: "int unsigned", nullable: false)
 | 
			
		||||
                },
 | 
			
		||||
                constraints: table =>
 | 
			
		||||
                {
 | 
			
		||||
                    table.PrimaryKey("pk_patronquotas", x => new { x.userid, x.featuretype, x.feature });
 | 
			
		||||
                })
 | 
			
		||||
                .Annotation("MySql:CharSet", "utf8mb4");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateTable(
 | 
			
		||||
                name: "patrons",
 | 
			
		||||
                columns: table => new
 | 
			
		||||
                {
 | 
			
		||||
                    userid = table.Column<ulong>(type: "bigint unsigned", nullable: false)
 | 
			
		||||
                        .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn),
 | 
			
		||||
                    uniqueplatformuserid = table.Column<string>(type: "varchar(255)", nullable: true)
 | 
			
		||||
                        .Annotation("MySql:CharSet", "utf8mb4"),
 | 
			
		||||
                    amountcents = table.Column<int>(type: "int", nullable: false),
 | 
			
		||||
                    lastcharge = table.Column<DateTime>(type: "datetime(6)", nullable: false),
 | 
			
		||||
                    validthru = table.Column<DateTime>(type: "datetime(6)", nullable: false)
 | 
			
		||||
                },
 | 
			
		||||
                constraints: table =>
 | 
			
		||||
                {
 | 
			
		||||
                    table.PrimaryKey("pk_patrons", x => x.userid);
 | 
			
		||||
                })
 | 
			
		||||
                .Annotation("MySql:CharSet", "utf8mb4");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateIndex(
 | 
			
		||||
                name: "ix_patronquotas_userid",
 | 
			
		||||
                table: "patronquotas",
 | 
			
		||||
                column: "userid");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateIndex(
 | 
			
		||||
                name: "ix_patrons_uniqueplatformuserid",
 | 
			
		||||
                table: "patrons",
 | 
			
		||||
                column: "uniqueplatformuserid",
 | 
			
		||||
                unique: true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected override void Down(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.DropTable(
 | 
			
		||||
                name: "patronquotas");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.DropTable(
 | 
			
		||||
                name: "patrons");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.RenameColumn(
 | 
			
		||||
                name: "platformuserid",
 | 
			
		||||
                table: "rewardedusers",
 | 
			
		||||
                newName: "patreonuserid");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.RenameIndex(
 | 
			
		||||
                name: "ix_rewardedusers_platformuserid",
 | 
			
		||||
                table: "rewardedusers",
 | 
			
		||||
                newName: "ix_rewardedusers_patreonuserid");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<int>(
 | 
			
		||||
                name: "xp",
 | 
			
		||||
                table: "userxpstats",
 | 
			
		||||
                type: "int",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                oldClrType: typeof(long),
 | 
			
		||||
                oldType: "bigint");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<int>(
 | 
			
		||||
                name: "awardedxp",
 | 
			
		||||
                table: "userxpstats",
 | 
			
		||||
                type: "int",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                oldClrType: typeof(long),
 | 
			
		||||
                oldType: "bigint");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<int>(
 | 
			
		||||
                name: "amountrewardedthismonth",
 | 
			
		||||
                table: "rewardedusers",
 | 
			
		||||
                type: "int",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                oldClrType: typeof(long),
 | 
			
		||||
                oldType: "bigint");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<bool>(
 | 
			
		||||
                name: "verboseerrors",
 | 
			
		||||
                table: "guildconfigs",
 | 
			
		||||
                type: "tinyint(1)",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                oldClrType: typeof(bool),
 | 
			
		||||
                oldType: "tinyint(1)",
 | 
			
		||||
                oldDefaultValue: true);
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<int>(
 | 
			
		||||
                name: "totalxp",
 | 
			
		||||
                table: "discorduser",
 | 
			
		||||
                type: "int",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                defaultValue: 0,
 | 
			
		||||
                oldClrType: typeof(long),
 | 
			
		||||
                oldType: "bigint",
 | 
			
		||||
                oldDefaultValue: 0L);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -16,7 +16,7 @@ namespace NadekoBot.Migrations.Mysql
 | 
			
		||||
        {
 | 
			
		||||
#pragma warning disable 612, 618
 | 
			
		||||
            modelBuilder
 | 
			
		||||
                .HasAnnotation("ProductVersion", "6.0.4")
 | 
			
		||||
                .HasAnnotation("ProductVersion", "6.0.5")
 | 
			
		||||
                .HasAnnotation("Relational:MaxIdentifierLength", 64);
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Db.Models.BankUser", b =>
 | 
			
		||||
@@ -186,10 +186,10 @@ namespace NadekoBot.Migrations.Mysql
 | 
			
		||||
                        .HasDefaultValue(0)
 | 
			
		||||
                        .HasColumnName("notifyonlevelup");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int>("TotalXp")
 | 
			
		||||
                    b.Property<long>("TotalXp")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("int")
 | 
			
		||||
                        .HasDefaultValue(0)
 | 
			
		||||
                        .HasColumnType("bigint")
 | 
			
		||||
                        .HasDefaultValue(0L)
 | 
			
		||||
                        .HasColumnName("totalxp");
 | 
			
		||||
 | 
			
		||||
                    b.Property<ulong>("UserId")
 | 
			
		||||
@@ -265,6 +265,74 @@ namespace NadekoBot.Migrations.Mysql
 | 
			
		||||
                    b.ToTable("followedstream", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Db.Models.PatronQuota", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<ulong>("UserId")
 | 
			
		||||
                        .HasColumnType("bigint unsigned")
 | 
			
		||||
                        .HasColumnName("userid");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int>("FeatureType")
 | 
			
		||||
                        .HasColumnType("int")
 | 
			
		||||
                        .HasColumnName("featuretype");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Feature")
 | 
			
		||||
                        .HasColumnType("varchar(255)")
 | 
			
		||||
                        .HasColumnName("feature");
 | 
			
		||||
 | 
			
		||||
                    b.Property<uint>("DailyCount")
 | 
			
		||||
                        .HasColumnType("int unsigned")
 | 
			
		||||
                        .HasColumnName("dailycount");
 | 
			
		||||
 | 
			
		||||
                    b.Property<uint>("HourlyCount")
 | 
			
		||||
                        .HasColumnType("int unsigned")
 | 
			
		||||
                        .HasColumnName("hourlycount");
 | 
			
		||||
 | 
			
		||||
                    b.Property<uint>("MonthlyCount")
 | 
			
		||||
                        .HasColumnType("int unsigned")
 | 
			
		||||
                        .HasColumnName("monthlycount");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("UserId", "FeatureType", "Feature")
 | 
			
		||||
                        .HasName("pk_patronquotas");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("UserId")
 | 
			
		||||
                        .HasDatabaseName("ix_patronquotas_userid");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("patronquotas", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Db.Models.PatronUser", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<ulong>("UserId")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("bigint unsigned")
 | 
			
		||||
                        .HasColumnName("userid");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int>("AmountCents")
 | 
			
		||||
                        .HasColumnType("int")
 | 
			
		||||
                        .HasColumnName("amountcents");
 | 
			
		||||
 | 
			
		||||
                    b.Property<DateTime>("LastCharge")
 | 
			
		||||
                        .HasColumnType("datetime(6)")
 | 
			
		||||
                        .HasColumnName("lastcharge");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("UniquePlatformUserId")
 | 
			
		||||
                        .HasColumnType("varchar(255)")
 | 
			
		||||
                        .HasColumnName("uniqueplatformuserid");
 | 
			
		||||
 | 
			
		||||
                    b.Property<DateTime>("ValidThru")
 | 
			
		||||
                        .HasColumnType("datetime(6)")
 | 
			
		||||
                        .HasColumnName("validthru");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("UserId")
 | 
			
		||||
                        .HasName("pk_patrons");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("UniquePlatformUserId")
 | 
			
		||||
                        .IsUnique()
 | 
			
		||||
                        .HasDatabaseName("ix_patrons_uniqueplatformuserid");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("patrons", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiAltSetting", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<int>("Id")
 | 
			
		||||
@@ -1138,7 +1206,9 @@ namespace NadekoBot.Migrations.Mysql
 | 
			
		||||
                        .HasColumnName("timezoneid");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("VerboseErrors")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("tinyint(1)")
 | 
			
		||||
                        .HasDefaultValue(true)
 | 
			
		||||
                        .HasColumnName("verboseerrors");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("VerbosePermissions")
 | 
			
		||||
@@ -1962,8 +2032,8 @@ namespace NadekoBot.Migrations.Mysql
 | 
			
		||||
                        .HasColumnType("int")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int>("AmountRewardedThisMonth")
 | 
			
		||||
                        .HasColumnType("int")
 | 
			
		||||
                    b.Property<long>("AmountRewardedThisMonth")
 | 
			
		||||
                        .HasColumnType("bigint")
 | 
			
		||||
                        .HasColumnName("amountrewardedthismonth");
 | 
			
		||||
 | 
			
		||||
                    b.Property<DateTime?>("DateAdded")
 | 
			
		||||
@@ -1974,9 +2044,9 @@ namespace NadekoBot.Migrations.Mysql
 | 
			
		||||
                        .HasColumnType("datetime(6)")
 | 
			
		||||
                        .HasColumnName("lastreward");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("PatreonUserId")
 | 
			
		||||
                    b.Property<string>("PlatformUserId")
 | 
			
		||||
                        .HasColumnType("varchar(255)")
 | 
			
		||||
                        .HasColumnName("patreonuserid");
 | 
			
		||||
                        .HasColumnName("platformuserid");
 | 
			
		||||
 | 
			
		||||
                    b.Property<ulong>("UserId")
 | 
			
		||||
                        .HasColumnType("bigint unsigned")
 | 
			
		||||
@@ -1985,9 +2055,9 @@ namespace NadekoBot.Migrations.Mysql
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_rewardedusers");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("PatreonUserId")
 | 
			
		||||
                    b.HasIndex("PlatformUserId")
 | 
			
		||||
                        .IsUnique()
 | 
			
		||||
                        .HasDatabaseName("ix_rewardedusers_patreonuserid");
 | 
			
		||||
                        .HasDatabaseName("ix_rewardedusers_platformuserid");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("rewardedusers", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
@@ -2404,8 +2474,8 @@ namespace NadekoBot.Migrations.Mysql
 | 
			
		||||
                        .HasColumnType("int")
 | 
			
		||||
                        .HasColumnName("id");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int>("AwardedXp")
 | 
			
		||||
                        .HasColumnType("int")
 | 
			
		||||
                    b.Property<long>("AwardedXp")
 | 
			
		||||
                        .HasColumnType("bigint")
 | 
			
		||||
                        .HasColumnName("awardedxp");
 | 
			
		||||
 | 
			
		||||
                    b.Property<DateTime?>("DateAdded")
 | 
			
		||||
@@ -2430,8 +2500,8 @@ namespace NadekoBot.Migrations.Mysql
 | 
			
		||||
                        .HasColumnType("bigint unsigned")
 | 
			
		||||
                        .HasColumnName("userid");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int>("Xp")
 | 
			
		||||
                        .HasColumnType("int")
 | 
			
		||||
                    b.Property<long>("Xp")
 | 
			
		||||
                        .HasColumnType("bigint")
 | 
			
		||||
                        .HasColumnName("xp");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										3621
									
								
								src/NadekoBot/Migrations/Postgresql/20220614071421_patronage-system.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										3621
									
								
								src/NadekoBot/Migrations/Postgresql/20220614071421_patronage-system.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -0,0 +1,170 @@
 | 
			
		||||
using System;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Migrations.PostgreSql
 | 
			
		||||
{
 | 
			
		||||
    public partial class patronagesystem : Migration
 | 
			
		||||
    {
 | 
			
		||||
        protected override void Up(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.RenameColumn(
 | 
			
		||||
                name: "patreonuserid",
 | 
			
		||||
                table: "rewardedusers",
 | 
			
		||||
                newName: "platformuserid");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.RenameIndex(
 | 
			
		||||
                name: "ix_rewardedusers_patreonuserid",
 | 
			
		||||
                table: "rewardedusers",
 | 
			
		||||
                newName: "ix_rewardedusers_platformuserid");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<long>(
 | 
			
		||||
                name: "xp",
 | 
			
		||||
                table: "userxpstats",
 | 
			
		||||
                type: "bigint",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                oldClrType: typeof(int),
 | 
			
		||||
                oldType: "integer");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<long>(
 | 
			
		||||
                name: "awardedxp",
 | 
			
		||||
                table: "userxpstats",
 | 
			
		||||
                type: "bigint",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                oldClrType: typeof(int),
 | 
			
		||||
                oldType: "integer");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<long>(
 | 
			
		||||
                name: "amountrewardedthismonth",
 | 
			
		||||
                table: "rewardedusers",
 | 
			
		||||
                type: "bigint",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                oldClrType: typeof(int),
 | 
			
		||||
                oldType: "integer");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<bool>(
 | 
			
		||||
                name: "verboseerrors",
 | 
			
		||||
                table: "guildconfigs",
 | 
			
		||||
                type: "boolean",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                defaultValue: true,
 | 
			
		||||
                oldClrType: typeof(bool),
 | 
			
		||||
                oldType: "boolean");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<long>(
 | 
			
		||||
                name: "totalxp",
 | 
			
		||||
                table: "discorduser",
 | 
			
		||||
                type: "bigint",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                defaultValue: 0L,
 | 
			
		||||
                oldClrType: typeof(int),
 | 
			
		||||
                oldType: "integer",
 | 
			
		||||
                oldDefaultValue: 0);
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateTable(
 | 
			
		||||
                name: "patronquotas",
 | 
			
		||||
                columns: table => new
 | 
			
		||||
                {
 | 
			
		||||
                    userid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
 | 
			
		||||
                    featuretype = table.Column<int>(type: "integer", nullable: false),
 | 
			
		||||
                    feature = table.Column<string>(type: "text", nullable: false),
 | 
			
		||||
                    hourlycount = table.Column<long>(type: "bigint", nullable: false),
 | 
			
		||||
                    dailycount = table.Column<long>(type: "bigint", nullable: false),
 | 
			
		||||
                    monthlycount = table.Column<long>(type: "bigint", nullable: false)
 | 
			
		||||
                },
 | 
			
		||||
                constraints: table =>
 | 
			
		||||
                {
 | 
			
		||||
                    table.PrimaryKey("pk_patronquotas", x => new { x.userid, x.featuretype, x.feature });
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateTable(
 | 
			
		||||
                name: "patrons",
 | 
			
		||||
                columns: table => new
 | 
			
		||||
                {
 | 
			
		||||
                    userid = table.Column<decimal>(type: "numeric(20,0)", nullable: false),
 | 
			
		||||
                    uniqueplatformuserid = table.Column<string>(type: "text", nullable: true),
 | 
			
		||||
                    amountcents = table.Column<int>(type: "integer", nullable: false),
 | 
			
		||||
                    lastcharge = table.Column<DateTime>(type: "timestamp with time zone", nullable: false),
 | 
			
		||||
                    validthru = table.Column<DateTime>(type: "timestamp with time zone", nullable: false)
 | 
			
		||||
                },
 | 
			
		||||
                constraints: table =>
 | 
			
		||||
                {
 | 
			
		||||
                    table.PrimaryKey("pk_patrons", x => x.userid);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateIndex(
 | 
			
		||||
                name: "ix_patronquotas_userid",
 | 
			
		||||
                table: "patronquotas",
 | 
			
		||||
                column: "userid");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateIndex(
 | 
			
		||||
                name: "ix_patrons_uniqueplatformuserid",
 | 
			
		||||
                table: "patrons",
 | 
			
		||||
                column: "uniqueplatformuserid",
 | 
			
		||||
                unique: true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected override void Down(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.DropTable(
 | 
			
		||||
                name: "patronquotas");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.DropTable(
 | 
			
		||||
                name: "patrons");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.RenameColumn(
 | 
			
		||||
                name: "platformuserid",
 | 
			
		||||
                table: "rewardedusers",
 | 
			
		||||
                newName: "patreonuserid");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.RenameIndex(
 | 
			
		||||
                name: "ix_rewardedusers_platformuserid",
 | 
			
		||||
                table: "rewardedusers",
 | 
			
		||||
                newName: "ix_rewardedusers_patreonuserid");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<int>(
 | 
			
		||||
                name: "xp",
 | 
			
		||||
                table: "userxpstats",
 | 
			
		||||
                type: "integer",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                oldClrType: typeof(long),
 | 
			
		||||
                oldType: "bigint");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<int>(
 | 
			
		||||
                name: "awardedxp",
 | 
			
		||||
                table: "userxpstats",
 | 
			
		||||
                type: "integer",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                oldClrType: typeof(long),
 | 
			
		||||
                oldType: "bigint");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<int>(
 | 
			
		||||
                name: "amountrewardedthismonth",
 | 
			
		||||
                table: "rewardedusers",
 | 
			
		||||
                type: "integer",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                oldClrType: typeof(long),
 | 
			
		||||
                oldType: "bigint");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<bool>(
 | 
			
		||||
                name: "verboseerrors",
 | 
			
		||||
                table: "guildconfigs",
 | 
			
		||||
                type: "boolean",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                oldClrType: typeof(bool),
 | 
			
		||||
                oldType: "boolean",
 | 
			
		||||
                oldDefaultValue: true);
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<int>(
 | 
			
		||||
                name: "totalxp",
 | 
			
		||||
                table: "discorduser",
 | 
			
		||||
                type: "integer",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                defaultValue: 0,
 | 
			
		||||
                oldClrType: typeof(long),
 | 
			
		||||
                oldType: "bigint",
 | 
			
		||||
                oldDefaultValue: 0L);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -17,7 +17,7 @@ namespace NadekoBot.Migrations.PostgreSql
 | 
			
		||||
        {
 | 
			
		||||
#pragma warning disable 612, 618
 | 
			
		||||
            modelBuilder
 | 
			
		||||
                .HasAnnotation("ProductVersion", "6.0.4")
 | 
			
		||||
                .HasAnnotation("ProductVersion", "6.0.5")
 | 
			
		||||
                .HasAnnotation("Relational:MaxIdentifierLength", 63);
 | 
			
		||||
 | 
			
		||||
            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
 | 
			
		||||
@@ -194,10 +194,10 @@ namespace NadekoBot.Migrations.PostgreSql
 | 
			
		||||
                        .HasDefaultValue(0)
 | 
			
		||||
                        .HasColumnName("notifyonlevelup");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int>("TotalXp")
 | 
			
		||||
                    b.Property<long>("TotalXp")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("integer")
 | 
			
		||||
                        .HasDefaultValue(0)
 | 
			
		||||
                        .HasColumnType("bigint")
 | 
			
		||||
                        .HasDefaultValue(0L)
 | 
			
		||||
                        .HasColumnName("totalxp");
 | 
			
		||||
 | 
			
		||||
                    b.Property<decimal>("UserId")
 | 
			
		||||
@@ -275,6 +275,74 @@ namespace NadekoBot.Migrations.PostgreSql
 | 
			
		||||
                    b.ToTable("followedstream", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Db.Models.PatronQuota", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<decimal>("UserId")
 | 
			
		||||
                        .HasColumnType("numeric(20,0)")
 | 
			
		||||
                        .HasColumnName("userid");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int>("FeatureType")
 | 
			
		||||
                        .HasColumnType("integer")
 | 
			
		||||
                        .HasColumnName("featuretype");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Feature")
 | 
			
		||||
                        .HasColumnType("text")
 | 
			
		||||
                        .HasColumnName("feature");
 | 
			
		||||
 | 
			
		||||
                    b.Property<long>("DailyCount")
 | 
			
		||||
                        .HasColumnType("bigint")
 | 
			
		||||
                        .HasColumnName("dailycount");
 | 
			
		||||
 | 
			
		||||
                    b.Property<long>("HourlyCount")
 | 
			
		||||
                        .HasColumnType("bigint")
 | 
			
		||||
                        .HasColumnName("hourlycount");
 | 
			
		||||
 | 
			
		||||
                    b.Property<long>("MonthlyCount")
 | 
			
		||||
                        .HasColumnType("bigint")
 | 
			
		||||
                        .HasColumnName("monthlycount");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("UserId", "FeatureType", "Feature")
 | 
			
		||||
                        .HasName("pk_patronquotas");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("UserId")
 | 
			
		||||
                        .HasDatabaseName("ix_patronquotas_userid");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("patronquotas", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Db.Models.PatronUser", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<decimal>("UserId")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("numeric(20,0)")
 | 
			
		||||
                        .HasColumnName("userid");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int>("AmountCents")
 | 
			
		||||
                        .HasColumnType("integer")
 | 
			
		||||
                        .HasColumnName("amountcents");
 | 
			
		||||
 | 
			
		||||
                    b.Property<DateTime>("LastCharge")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("lastcharge");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("UniquePlatformUserId")
 | 
			
		||||
                        .HasColumnType("text")
 | 
			
		||||
                        .HasColumnName("uniqueplatformuserid");
 | 
			
		||||
 | 
			
		||||
                    b.Property<DateTime>("ValidThru")
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("validthru");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("UserId")
 | 
			
		||||
                        .HasName("pk_patrons");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("UniquePlatformUserId")
 | 
			
		||||
                        .IsUnique()
 | 
			
		||||
                        .HasDatabaseName("ix_patrons_uniqueplatformuserid");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("patrons", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiAltSetting", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<int>("Id")
 | 
			
		||||
@@ -1194,7 +1262,9 @@ namespace NadekoBot.Migrations.PostgreSql
 | 
			
		||||
                        .HasColumnName("timezoneid");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("VerboseErrors")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("boolean")
 | 
			
		||||
                        .HasDefaultValue(true)
 | 
			
		||||
                        .HasColumnName("verboseerrors");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("VerbosePermissions")
 | 
			
		||||
@@ -2058,8 +2128,8 @@ namespace NadekoBot.Migrations.PostgreSql
 | 
			
		||||
 | 
			
		||||
                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
 | 
			
		||||
 | 
			
		||||
                    b.Property<int>("AmountRewardedThisMonth")
 | 
			
		||||
                        .HasColumnType("integer")
 | 
			
		||||
                    b.Property<long>("AmountRewardedThisMonth")
 | 
			
		||||
                        .HasColumnType("bigint")
 | 
			
		||||
                        .HasColumnName("amountrewardedthismonth");
 | 
			
		||||
 | 
			
		||||
                    b.Property<DateTime?>("DateAdded")
 | 
			
		||||
@@ -2070,9 +2140,9 @@ namespace NadekoBot.Migrations.PostgreSql
 | 
			
		||||
                        .HasColumnType("timestamp with time zone")
 | 
			
		||||
                        .HasColumnName("lastreward");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("PatreonUserId")
 | 
			
		||||
                    b.Property<string>("PlatformUserId")
 | 
			
		||||
                        .HasColumnType("text")
 | 
			
		||||
                        .HasColumnName("patreonuserid");
 | 
			
		||||
                        .HasColumnName("platformuserid");
 | 
			
		||||
 | 
			
		||||
                    b.Property<decimal>("UserId")
 | 
			
		||||
                        .HasColumnType("numeric(20,0)")
 | 
			
		||||
@@ -2081,9 +2151,9 @@ namespace NadekoBot.Migrations.PostgreSql
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
                        .HasName("pk_rewardedusers");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("PatreonUserId")
 | 
			
		||||
                    b.HasIndex("PlatformUserId")
 | 
			
		||||
                        .IsUnique()
 | 
			
		||||
                        .HasDatabaseName("ix_rewardedusers_patreonuserid");
 | 
			
		||||
                        .HasDatabaseName("ix_rewardedusers_platformuserid");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("rewardedusers", (string)null);
 | 
			
		||||
                });
 | 
			
		||||
@@ -2526,8 +2596,8 @@ namespace NadekoBot.Migrations.PostgreSql
 | 
			
		||||
 | 
			
		||||
                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
 | 
			
		||||
 | 
			
		||||
                    b.Property<int>("AwardedXp")
 | 
			
		||||
                        .HasColumnType("integer")
 | 
			
		||||
                    b.Property<long>("AwardedXp")
 | 
			
		||||
                        .HasColumnType("bigint")
 | 
			
		||||
                        .HasColumnName("awardedxp");
 | 
			
		||||
 | 
			
		||||
                    b.Property<DateTime?>("DateAdded")
 | 
			
		||||
@@ -2552,8 +2622,8 @@ namespace NadekoBot.Migrations.PostgreSql
 | 
			
		||||
                        .HasColumnType("numeric(20,0)")
 | 
			
		||||
                        .HasColumnName("userid");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int>("Xp")
 | 
			
		||||
                        .HasColumnType("integer")
 | 
			
		||||
                    b.Property<long>("Xp")
 | 
			
		||||
                        .HasColumnType("bigint")
 | 
			
		||||
                        .HasColumnName("xp");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id")
 | 
			
		||||
 
 | 
			
		||||
@@ -87,7 +87,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
                name: "VoicePresenceChannelId",
 | 
			
		||||
                table: "LogSettings");
 | 
			
		||||
            
 | 
			
		||||
            // todo cleanup guildconfigs which have logsettings id set to null
 | 
			
		||||
            // FUTURE cleanup guildconfigs which have logsettings id set to null
 | 
			
		||||
            migrationBuilder.Sql("UPDATE GuildConfigs SET LogSettingId = null WHERE LogSettingId NOT IN (SELECT Id from LogSettings)");
 | 
			
		||||
            
 | 
			
		||||
            migrationBuilder.DropTable(
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2796
									
								
								src/NadekoBot/Migrations/Sqlite/20220614071359_patronage-system.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
							
						
						
									
										2796
									
								
								src/NadekoBot/Migrations/Sqlite/20220614071359_patronage-system.Designer.cs
									
									
									
										generated
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -0,0 +1,123 @@
 | 
			
		||||
using System;
 | 
			
		||||
using Microsoft.EntityFrameworkCore.Migrations;
 | 
			
		||||
 | 
			
		||||
#nullable disable
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Migrations
 | 
			
		||||
{
 | 
			
		||||
    public partial class patronagesystem : Migration
 | 
			
		||||
    {
 | 
			
		||||
        protected override void Up(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.RenameColumn(
 | 
			
		||||
                name: "PatreonUserId",
 | 
			
		||||
                table: "RewardedUsers",
 | 
			
		||||
                newName: "PlatformUserId");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.RenameIndex(
 | 
			
		||||
                name: "IX_RewardedUsers_PatreonUserId",
 | 
			
		||||
                table: "RewardedUsers",
 | 
			
		||||
                newName: "IX_RewardedUsers_PlatformUserId");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<bool>(
 | 
			
		||||
                name: "VerboseErrors",
 | 
			
		||||
                table: "GuildConfigs",
 | 
			
		||||
                type: "INTEGER",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                defaultValue: true,
 | 
			
		||||
                oldClrType: typeof(bool),
 | 
			
		||||
                oldType: "INTEGER");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<long>(
 | 
			
		||||
                name: "TotalXp",
 | 
			
		||||
                table: "DiscordUser",
 | 
			
		||||
                type: "INTEGER",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                defaultValue: 0L,
 | 
			
		||||
                oldClrType: typeof(int),
 | 
			
		||||
                oldType: "INTEGER",
 | 
			
		||||
                oldDefaultValue: 0);
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateTable(
 | 
			
		||||
                name: "PatronQuotas",
 | 
			
		||||
                columns: table => new
 | 
			
		||||
                {
 | 
			
		||||
                    UserId = table.Column<ulong>(type: "INTEGER", nullable: false),
 | 
			
		||||
                    FeatureType = table.Column<int>(type: "INTEGER", nullable: false),
 | 
			
		||||
                    Feature = table.Column<string>(type: "TEXT", nullable: false),
 | 
			
		||||
                    HourlyCount = table.Column<uint>(type: "INTEGER", nullable: false),
 | 
			
		||||
                    DailyCount = table.Column<uint>(type: "INTEGER", nullable: false),
 | 
			
		||||
                    MonthlyCount = table.Column<uint>(type: "INTEGER", nullable: false)
 | 
			
		||||
                },
 | 
			
		||||
                constraints: table =>
 | 
			
		||||
                {
 | 
			
		||||
                    table.PrimaryKey("PK_PatronQuotas", x => new { x.UserId, x.FeatureType, x.Feature });
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateTable(
 | 
			
		||||
                name: "Patrons",
 | 
			
		||||
                columns: table => new
 | 
			
		||||
                {
 | 
			
		||||
                    UserId = table.Column<ulong>(type: "INTEGER", nullable: false)
 | 
			
		||||
                        .Annotation("Sqlite:Autoincrement", true),
 | 
			
		||||
                    UniquePlatformUserId = table.Column<string>(type: "TEXT", nullable: true),
 | 
			
		||||
                    AmountCents = table.Column<int>(type: "INTEGER", nullable: false),
 | 
			
		||||
                    LastCharge = table.Column<DateTime>(type: "TEXT", nullable: false),
 | 
			
		||||
                    ValidThru = table.Column<DateTime>(type: "TEXT", nullable: false)
 | 
			
		||||
                },
 | 
			
		||||
                constraints: table =>
 | 
			
		||||
                {
 | 
			
		||||
                    table.PrimaryKey("PK_Patrons", x => x.UserId);
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateIndex(
 | 
			
		||||
                name: "IX_PatronQuotas_UserId",
 | 
			
		||||
                table: "PatronQuotas",
 | 
			
		||||
                column: "UserId");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.CreateIndex(
 | 
			
		||||
                name: "IX_Patrons_UniquePlatformUserId",
 | 
			
		||||
                table: "Patrons",
 | 
			
		||||
                column: "UniquePlatformUserId",
 | 
			
		||||
                unique: true);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        protected override void Down(MigrationBuilder migrationBuilder)
 | 
			
		||||
        {
 | 
			
		||||
            migrationBuilder.DropTable(
 | 
			
		||||
                name: "PatronQuotas");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.DropTable(
 | 
			
		||||
                name: "Patrons");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.RenameColumn(
 | 
			
		||||
                name: "PlatformUserId",
 | 
			
		||||
                table: "RewardedUsers",
 | 
			
		||||
                newName: "PatreonUserId");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.RenameIndex(
 | 
			
		||||
                name: "IX_RewardedUsers_PlatformUserId",
 | 
			
		||||
                table: "RewardedUsers",
 | 
			
		||||
                newName: "IX_RewardedUsers_PatreonUserId");
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<bool>(
 | 
			
		||||
                name: "VerboseErrors",
 | 
			
		||||
                table: "GuildConfigs",
 | 
			
		||||
                type: "INTEGER",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                oldClrType: typeof(bool),
 | 
			
		||||
                oldType: "INTEGER",
 | 
			
		||||
                oldDefaultValue: true);
 | 
			
		||||
 | 
			
		||||
            migrationBuilder.AlterColumn<int>(
 | 
			
		||||
                name: "TotalXp",
 | 
			
		||||
                table: "DiscordUser",
 | 
			
		||||
                type: "INTEGER",
 | 
			
		||||
                nullable: false,
 | 
			
		||||
                defaultValue: 0,
 | 
			
		||||
                oldClrType: typeof(long),
 | 
			
		||||
                oldType: "INTEGER",
 | 
			
		||||
                oldDefaultValue: 0L);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -15,7 +15,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
        protected override void BuildModel(ModelBuilder modelBuilder)
 | 
			
		||||
        {
 | 
			
		||||
#pragma warning disable 612, 618
 | 
			
		||||
            modelBuilder.HasAnnotation("ProductVersion", "6.0.3");
 | 
			
		||||
            modelBuilder.HasAnnotation("ProductVersion", "6.0.5");
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Db.Models.BankUser", b =>
 | 
			
		||||
                {
 | 
			
		||||
@@ -149,10 +149,10 @@ namespace NadekoBot.Migrations
 | 
			
		||||
                        .HasColumnType("INTEGER")
 | 
			
		||||
                        .HasDefaultValue(0);
 | 
			
		||||
 | 
			
		||||
                    b.Property<int>("TotalXp")
 | 
			
		||||
                    b.Property<long>("TotalXp")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("INTEGER")
 | 
			
		||||
                        .HasDefaultValue(0);
 | 
			
		||||
                        .HasDefaultValue(0L);
 | 
			
		||||
 | 
			
		||||
                    b.Property<ulong>("UserId")
 | 
			
		||||
                        .HasColumnType("INTEGER");
 | 
			
		||||
@@ -209,6 +209,59 @@ namespace NadekoBot.Migrations
 | 
			
		||||
                    b.ToTable("FollowedStream");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Db.Models.PatronQuota", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<ulong>("UserId")
 | 
			
		||||
                        .HasColumnType("INTEGER");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int>("FeatureType")
 | 
			
		||||
                        .HasColumnType("INTEGER");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("Feature")
 | 
			
		||||
                        .HasColumnType("TEXT");
 | 
			
		||||
 | 
			
		||||
                    b.Property<uint>("DailyCount")
 | 
			
		||||
                        .HasColumnType("INTEGER");
 | 
			
		||||
 | 
			
		||||
                    b.Property<uint>("HourlyCount")
 | 
			
		||||
                        .HasColumnType("INTEGER");
 | 
			
		||||
 | 
			
		||||
                    b.Property<uint>("MonthlyCount")
 | 
			
		||||
                        .HasColumnType("INTEGER");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("UserId", "FeatureType", "Feature");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("UserId");
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("PatronQuotas");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Db.Models.PatronUser", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<ulong>("UserId")
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("INTEGER");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int>("AmountCents")
 | 
			
		||||
                        .HasColumnType("INTEGER");
 | 
			
		||||
 | 
			
		||||
                    b.Property<DateTime>("LastCharge")
 | 
			
		||||
                        .HasColumnType("TEXT");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("UniquePlatformUserId")
 | 
			
		||||
                        .HasColumnType("TEXT");
 | 
			
		||||
 | 
			
		||||
                    b.Property<DateTime>("ValidThru")
 | 
			
		||||
                        .HasColumnType("TEXT");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("UserId");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("UniquePlatformUserId")
 | 
			
		||||
                        .IsUnique();
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("Patrons");
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
            modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiAltSetting", b =>
 | 
			
		||||
                {
 | 
			
		||||
                    b.Property<int>("Id")
 | 
			
		||||
@@ -890,7 +943,9 @@ namespace NadekoBot.Migrations
 | 
			
		||||
                        .HasColumnType("TEXT");
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("VerboseErrors")
 | 
			
		||||
                        .HasColumnType("INTEGER");
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("INTEGER")
 | 
			
		||||
                        .HasDefaultValue(true);
 | 
			
		||||
 | 
			
		||||
                    b.Property<bool>("VerbosePermissions")
 | 
			
		||||
                        .HasColumnType("INTEGER");
 | 
			
		||||
@@ -1531,7 +1586,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("INTEGER");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int>("AmountRewardedThisMonth")
 | 
			
		||||
                    b.Property<long>("AmountRewardedThisMonth")
 | 
			
		||||
                        .HasColumnType("INTEGER");
 | 
			
		||||
 | 
			
		||||
                    b.Property<DateTime?>("DateAdded")
 | 
			
		||||
@@ -1540,7 +1595,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
                    b.Property<DateTime>("LastReward")
 | 
			
		||||
                        .HasColumnType("TEXT");
 | 
			
		||||
 | 
			
		||||
                    b.Property<string>("PatreonUserId")
 | 
			
		||||
                    b.Property<string>("PlatformUserId")
 | 
			
		||||
                        .HasColumnType("TEXT");
 | 
			
		||||
 | 
			
		||||
                    b.Property<ulong>("UserId")
 | 
			
		||||
@@ -1548,7 +1603,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id");
 | 
			
		||||
 | 
			
		||||
                    b.HasIndex("PatreonUserId")
 | 
			
		||||
                    b.HasIndex("PlatformUserId")
 | 
			
		||||
                        .IsUnique();
 | 
			
		||||
 | 
			
		||||
                    b.ToTable("RewardedUsers");
 | 
			
		||||
@@ -1877,7 +1932,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
                        .ValueGeneratedOnAdd()
 | 
			
		||||
                        .HasColumnType("INTEGER");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int>("AwardedXp")
 | 
			
		||||
                    b.Property<long>("AwardedXp")
 | 
			
		||||
                        .HasColumnType("INTEGER");
 | 
			
		||||
 | 
			
		||||
                    b.Property<DateTime?>("DateAdded")
 | 
			
		||||
@@ -1897,7 +1952,7 @@ namespace NadekoBot.Migrations
 | 
			
		||||
                    b.Property<ulong>("UserId")
 | 
			
		||||
                        .HasColumnType("INTEGER");
 | 
			
		||||
 | 
			
		||||
                    b.Property<int>("Xp")
 | 
			
		||||
                    b.Property<long>("Xp")
 | 
			
		||||
                        .HasColumnType("INTEGER");
 | 
			
		||||
 | 
			
		||||
                    b.HasKey("Id");
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,8 @@ public class DangerousCommandsService : INService
 | 
			
		||||
            // IsClubAdmin = false,
 | 
			
		||||
            TotalXp = 0
 | 
			
		||||
        });
 | 
			
		||||
 | 
			
		||||
        await ctx.UserXpStats.DeleteAsync();
 | 
			
		||||
        await ctx.ClubApplicants.DeleteAsync();
 | 
			
		||||
        await ctx.ClubBans.DeleteAsync();
 | 
			
		||||
        await ctx.Clubs.DeleteAsync();
 | 
			
		||||
 
 | 
			
		||||
@@ -23,12 +23,13 @@ public class PruneService : INService
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var now = DateTime.UtcNow;
 | 
			
		||||
            IMessage[] msgs;
 | 
			
		||||
            IMessage lastMessage = null;
 | 
			
		||||
            msgs = (await channel.GetMessagesAsync(50).FlattenAsync()).Where(predicate).Take(amount).ToArray();
 | 
			
		||||
            while (amount > 0 && msgs.Any())
 | 
			
		||||
            {
 | 
			
		||||
                lastMessage = msgs[msgs.Length - 1];
 | 
			
		||||
                lastMessage = msgs[^1];
 | 
			
		||||
 | 
			
		||||
                var bulkDeletable = new List<IMessage>();
 | 
			
		||||
                var singleDeletable = new List<IMessage>();
 | 
			
		||||
@@ -36,17 +37,23 @@ public class PruneService : INService
 | 
			
		||||
                {
 | 
			
		||||
                    _logService.AddDeleteIgnore(x.Id);
 | 
			
		||||
 | 
			
		||||
                    if (DateTime.UtcNow - x.CreatedAt < _twoWeeks)
 | 
			
		||||
                    if (now - x.CreatedAt < _twoWeeks)
 | 
			
		||||
                        bulkDeletable.Add(x);
 | 
			
		||||
                    else
 | 
			
		||||
                        singleDeletable.Add(x);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                if (bulkDeletable.Count > 0)
 | 
			
		||||
                    await Task.WhenAll(Task.Delay(1000), channel.DeleteMessagesAsync(bulkDeletable));
 | 
			
		||||
                {
 | 
			
		||||
                    await channel.DeleteMessagesAsync(bulkDeletable);
 | 
			
		||||
                    await Task.Delay(2000);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                foreach (var group in singleDeletable.Chunk(5))
 | 
			
		||||
                    await Task.WhenAll(Task.Delay(5000), group.Select(x => x.DeleteAsync()).WhenAll());
 | 
			
		||||
                {
 | 
			
		||||
                    await group.Select(x => x.DeleteAsync()).WhenAll();
 | 
			
		||||
                    await Task.Delay(5000);
 | 
			
		||||
                }
 | 
			
		||||
 | 
			
		||||
                //this isn't good, because this still work as if i want to remove only specific user's messages from the last
 | 
			
		||||
                //100 messages, Maybe this needs to be reduced by msgs.Length instead of 100
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,8 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Modules.Utility.Patronage;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
using System.Collections;
 | 
			
		||||
using OneOf;
 | 
			
		||||
using OneOf.Types;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
@@ -9,18 +11,16 @@ public interface IReactionRoleService
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Adds a single reaction role
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="guildId"></param>
 | 
			
		||||
    /// <param name="msg"></param>
 | 
			
		||||
    /// <param name="channel"></param>
 | 
			
		||||
    /// <param name="guild">Guild where to add a reaction role</param>
 | 
			
		||||
    /// <param name="msg">Message to which to add a reaction role</param>
 | 
			
		||||
    /// <param name="emote"></param>
 | 
			
		||||
    /// <param name="role"></param>
 | 
			
		||||
    /// <param name="group"></param>
 | 
			
		||||
    /// <param name="levelReq"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    Task<bool> AddReactionRole(
 | 
			
		||||
        ulong guildId,
 | 
			
		||||
    /// <returns>The result of the operation</returns>
 | 
			
		||||
    Task<OneOf<Success, FeatureLimit>> AddReactionRole(
 | 
			
		||||
        IGuild guild,
 | 
			
		||||
        IMessage msg,
 | 
			
		||||
        ITextChannel channel,
 | 
			
		||||
        string emote,
 | 
			
		||||
        IRole role,
 | 
			
		||||
        int group = 0,
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,6 @@ public partial class Administration
 | 
			
		||||
        
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [NoPublicBot]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        public async partial Task ReactionRoleAdd(
 | 
			
		||||
@@ -46,27 +45,26 @@ public partial class Administration
 | 
			
		||||
 | 
			
		||||
            var emote = emoteStr.ToIEmote();
 | 
			
		||||
            await msg.AddReactionAsync(emote);
 | 
			
		||||
            var succ = await _rero.AddReactionRole(ctx.Guild.Id,
 | 
			
		||||
            var res = await _rero.AddReactionRole(ctx.Guild,
 | 
			
		||||
                msg,
 | 
			
		||||
                (ITextChannel)ctx.Channel,
 | 
			
		||||
                emoteStr,
 | 
			
		||||
                role,
 | 
			
		||||
                group,
 | 
			
		||||
                levelReq);
 | 
			
		||||
            
 | 
			
		||||
            if (succ)
 | 
			
		||||
            {
 | 
			
		||||
                await ctx.OkAsync();
 | 
			
		||||
            }
 | 
			
		||||
            else
 | 
			
		||||
            {
 | 
			
		||||
                await ctx.ErrorAsync();
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await res.Match(
 | 
			
		||||
                _ => ctx.OkAsync(),
 | 
			
		||||
                fl =>
 | 
			
		||||
                {
 | 
			
		||||
                    _ = msg.RemoveReactionAsync(emote, ctx.Client.CurrentUser);
 | 
			
		||||
                    return !fl.IsPatronLimit
 | 
			
		||||
                        ? ReplyErrorLocalizedAsync(strs.limit_reached(fl.Quota))
 | 
			
		||||
                        : ReplyPendingLocalizedAsync(strs.feature_limit_reached_owner(fl.Quota, fl.Name));
 | 
			
		||||
                });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [NoPublicBot]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        public async partial Task ReactionRolesList()
 | 
			
		||||
@@ -109,7 +107,6 @@ public partial class Administration
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [NoPublicBot]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        public async partial Task ReactionRolesRemove(ulong messageId)
 | 
			
		||||
@@ -123,7 +120,6 @@ public partial class Administration
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [NoPublicBot]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        public async partial Task ReactionRolesDeleteAll()
 | 
			
		||||
@@ -134,7 +130,6 @@ public partial class Administration
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [NoPublicBot]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [Ratelimit(60)]
 | 
			
		||||
 
 | 
			
		||||
@@ -2,8 +2,11 @@
 | 
			
		||||
using LinqToDB;
 | 
			
		||||
using LinqToDB.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
using NadekoBot.Modules.Utility.Patronage;
 | 
			
		||||
using NadekoBot.Modules.Xp.Extensions;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
using OneOf.Types;
 | 
			
		||||
using OneOf;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Administration.Services;
 | 
			
		||||
 | 
			
		||||
@@ -16,20 +19,33 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
 | 
			
		||||
    private ConcurrentDictionary<ulong, List<ReactionRoleV2>> _cache;
 | 
			
		||||
    private readonly object _cacheLock = new();
 | 
			
		||||
    private readonly SemaphoreSlim _assignementLock = new(1, 1);
 | 
			
		||||
    private readonly IPatronageService _ps;
 | 
			
		||||
 | 
			
		||||
    public ReactionRolesService(DiscordSocketClient client, DbService db, IBotCredentials creds)
 | 
			
		||||
    private static readonly FeatureLimitKey _reroFLKey = new()
 | 
			
		||||
    {
 | 
			
		||||
        Key = "rero:max_count",
 | 
			
		||||
        PrettyName = "Reaction Role"
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    public ReactionRolesService(
 | 
			
		||||
        DiscordSocketClient client,
 | 
			
		||||
        DbService db,
 | 
			
		||||
        IBotCredentials creds,
 | 
			
		||||
        IPatronageService ps)
 | 
			
		||||
    {
 | 
			
		||||
        _db = db;
 | 
			
		||||
        _ps = ps;
 | 
			
		||||
        _client = client;
 | 
			
		||||
        _creds = creds;
 | 
			
		||||
        _cache = new();
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    public async Task OnReadyAsync()
 | 
			
		||||
    {
 | 
			
		||||
        await using var uow = _db.GetDbContext();
 | 
			
		||||
        var reros = await uow.GetTable<ReactionRoleV2>()
 | 
			
		||||
                             .Where(x => Linq2DbExpressions.GuildOnShard(x.GuildId, _creds.TotalShards, _client.ShardId))
 | 
			
		||||
                             .Where(
 | 
			
		||||
                                 x => Linq2DbExpressions.GuildOnShard(x.GuildId, _creds.TotalShards, _client.ShardId))
 | 
			
		||||
                             .ToListAsyncLinqToDB();
 | 
			
		||||
 | 
			
		||||
        foreach (var group in reros.GroupBy(x => x.MessageId))
 | 
			
		||||
@@ -126,12 +142,12 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
 | 
			
		||||
                    {
 | 
			
		||||
                        await using var ctx = _db.GetDbContext();
 | 
			
		||||
                        var levelData = await ctx.GetTable<UserXpStats>()
 | 
			
		||||
                                 .GetLevelDataFor(user.GuildId, user.Id);
 | 
			
		||||
                                                 .GetLevelDataFor(user.GuildId, user.Id);
 | 
			
		||||
 | 
			
		||||
                        if (levelData.Level < rero.LevelReq)
 | 
			
		||||
                            return;
 | 
			
		||||
                    }
 | 
			
		||||
                    
 | 
			
		||||
 | 
			
		||||
                    // remove all other roles from the same group from the user
 | 
			
		||||
                    // execept in group 0, which is a special, non-exclusive group
 | 
			
		||||
                    if (rero.Group != 0)
 | 
			
		||||
@@ -141,7 +157,7 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
 | 
			
		||||
                                        .Select(x => x.RoleId)
 | 
			
		||||
                                        .Distinct();
 | 
			
		||||
 | 
			
		||||
                        
 | 
			
		||||
 | 
			
		||||
                        try { await user.RemoveRolesAsync(exclusive); }
 | 
			
		||||
                        catch { }
 | 
			
		||||
 | 
			
		||||
@@ -181,18 +197,16 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
 | 
			
		||||
    /// <summary>
 | 
			
		||||
    /// Adds a single reaction role
 | 
			
		||||
    /// </summary>
 | 
			
		||||
    /// <param name="guildId"></param>
 | 
			
		||||
    /// <param name="msg"></param>
 | 
			
		||||
    /// <param name="channel"></param>
 | 
			
		||||
    /// <param name="guild">Guild where to add a reaction role</param>
 | 
			
		||||
    /// <param name="msg">Message to which to add a reaction role</param>
 | 
			
		||||
    /// <param name="emote"></param>
 | 
			
		||||
    /// <param name="role"></param>
 | 
			
		||||
    /// <param name="group"></param>
 | 
			
		||||
    /// <param name="levelReq"></param>
 | 
			
		||||
    /// <returns></returns>
 | 
			
		||||
    public async Task<bool> AddReactionRole(
 | 
			
		||||
        ulong guildId,
 | 
			
		||||
    /// <returns>The result of the operation</returns>
 | 
			
		||||
    public async Task<OneOf<Success, FeatureLimit>> AddReactionRole(
 | 
			
		||||
        IGuild guild,
 | 
			
		||||
        IMessage msg,
 | 
			
		||||
        ITextChannel channel,
 | 
			
		||||
        string emote,
 | 
			
		||||
        IRole role,
 | 
			
		||||
        int group = 0,
 | 
			
		||||
@@ -205,44 +219,46 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
 | 
			
		||||
            throw new ArgumentOutOfRangeException(nameof(group));
 | 
			
		||||
 | 
			
		||||
        await using var ctx = _db.GetDbContext();
 | 
			
		||||
 | 
			
		||||
        await using var tran = await ctx.Database.BeginTransactionAsync();
 | 
			
		||||
        var activeReactionRoles = await ctx.GetTable<ReactionRoleV2>()
 | 
			
		||||
                                           .Where(x => x.GuildId == guildId)
 | 
			
		||||
                                           .Where(x => x.GuildId == guild.Id)
 | 
			
		||||
                                           .CountAsync();
 | 
			
		||||
        
 | 
			
		||||
        var result = await _ps.TryGetFeatureLimitAsync(_reroFLKey, guild.OwnerId, 50);
 | 
			
		||||
        if (result.Quota != -1 && activeReactionRoles >= result.Quota)
 | 
			
		||||
            return result;
 | 
			
		||||
 | 
			
		||||
        if (activeReactionRoles >= 50)
 | 
			
		||||
            return false;
 | 
			
		||||
        await ctx.GetTable<ReactionRoleV2>()
 | 
			
		||||
                 .InsertOrUpdateAsync(() => new()
 | 
			
		||||
                     {
 | 
			
		||||
                         GuildId = guild.Id,
 | 
			
		||||
                         ChannelId = msg.Channel.Id,
 | 
			
		||||
 | 
			
		||||
        var changed = await ctx.GetTable<ReactionRoleV2>()
 | 
			
		||||
                               .InsertOrUpdateAsync(() => new()
 | 
			
		||||
                                   {
 | 
			
		||||
                                       GuildId = guildId,
 | 
			
		||||
                                       ChannelId = channel.Id,
 | 
			
		||||
                         MessageId = msg.Id,
 | 
			
		||||
                         Emote = emote,
 | 
			
		||||
 | 
			
		||||
                                       MessageId = msg.Id,
 | 
			
		||||
                                       Emote = emote,
 | 
			
		||||
                         RoleId = role.Id,
 | 
			
		||||
                         Group = group,
 | 
			
		||||
                         LevelReq = levelReq
 | 
			
		||||
                     },
 | 
			
		||||
                     (old) => new()
 | 
			
		||||
                     {
 | 
			
		||||
                         RoleId = role.Id,
 | 
			
		||||
                         Group = group,
 | 
			
		||||
                         LevelReq = levelReq
 | 
			
		||||
                     },
 | 
			
		||||
                     () => new()
 | 
			
		||||
                     {
 | 
			
		||||
                         MessageId = msg.Id,
 | 
			
		||||
                         Emote = emote,
 | 
			
		||||
                     });
 | 
			
		||||
 | 
			
		||||
                                       RoleId = role.Id,
 | 
			
		||||
                                       Group = group,
 | 
			
		||||
                                       LevelReq = levelReq
 | 
			
		||||
                                   },
 | 
			
		||||
                                   (old) => new()
 | 
			
		||||
                                   {
 | 
			
		||||
                                       RoleId = role.Id,
 | 
			
		||||
                                       Group = group,
 | 
			
		||||
                                       LevelReq = levelReq
 | 
			
		||||
                                   },
 | 
			
		||||
                                   () => new()
 | 
			
		||||
                                   {
 | 
			
		||||
                                       MessageId = msg.Id,
 | 
			
		||||
                                       Emote = emote,
 | 
			
		||||
                                   });
 | 
			
		||||
 | 
			
		||||
        if (changed == 0)
 | 
			
		||||
            return false;
 | 
			
		||||
        await tran.CommitAsync();
 | 
			
		||||
 | 
			
		||||
        var obj = new ReactionRoleV2()
 | 
			
		||||
        {
 | 
			
		||||
            GuildId = guildId,
 | 
			
		||||
            GuildId = guild.Id,
 | 
			
		||||
            MessageId = msg.Id,
 | 
			
		||||
            Emote = emote,
 | 
			
		||||
            RoleId = role.Id,
 | 
			
		||||
@@ -265,7 +281,7 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
 | 
			
		||||
                });
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
        return new Success();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    /// <summary>
 | 
			
		||||
@@ -326,7 +342,10 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
 | 
			
		||||
        return output.Length;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<IReadOnlyCollection<IEmote>> TransferReactionRolesAsync(ulong guildId, ulong fromMessageId, ulong toMessageId)
 | 
			
		||||
    public async Task<IReadOnlyCollection<IEmote>> TransferReactionRolesAsync(
 | 
			
		||||
        ulong guildId,
 | 
			
		||||
        ulong fromMessageId,
 | 
			
		||||
        ulong toMessageId)
 | 
			
		||||
    {
 | 
			
		||||
        await using var ctx = _db.GetDbContext();
 | 
			
		||||
        var updated = ctx.GetTable<ReactionRoleV2>()
 | 
			
		||||
 
 | 
			
		||||
@@ -149,7 +149,7 @@ public partial class Administration
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        [BotPerm(GuildPerm.ManageRoles)]
 | 
			
		||||
        public async partial Task RoleHoist(IRole role)
 | 
			
		||||
        public async partial Task RoleHoist([Leftover] IRole role)
 | 
			
		||||
        {
 | 
			
		||||
            var newHoisted = !role.IsHoisted;
 | 
			
		||||
            await role.ModifyAsync(r => r.Hoist = newHoisted);
 | 
			
		||||
 
 | 
			
		||||
@@ -65,7 +65,7 @@ public partial class Gambling
 | 
			
		||||
            }
 | 
			
		||||
            catch
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.unable_to_dm_user);
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.cant_dm);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
using LinqToDB;
 | 
			
		||||
using LinqToDB.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Db.Models;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Gambling.Bank;
 | 
			
		||||
 | 
			
		||||
@@ -74,4 +75,19 @@ public sealed class BankService : IBankService, INService
 | 
			
		||||
               ?.Balance
 | 
			
		||||
               ?? 0;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<long> BurnAllAsync(ulong userId)
 | 
			
		||||
    {
 | 
			
		||||
        await using var ctx = _db.GetDbContext();
 | 
			
		||||
        var output = await ctx.GetTable<BankUser>()
 | 
			
		||||
                              .Where(x => x.UserId == userId)
 | 
			
		||||
                              .UpdateWithOutputAsync(old => new()
 | 
			
		||||
                              {
 | 
			
		||||
                                  Balance = 0
 | 
			
		||||
                              });
 | 
			
		||||
        if (output.Length == 0)
 | 
			
		||||
            return 0;
 | 
			
		||||
        
 | 
			
		||||
        return output[0].Deleted.Balance;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -5,4 +5,5 @@ public interface IBankService
 | 
			
		||||
    Task<bool> DepositAsync(ulong userId, long amount);
 | 
			
		||||
    Task<bool> WithdrawAsync(ulong userId, long amount);
 | 
			
		||||
    Task<long> GetBalanceAsync(ulong userId);
 | 
			
		||||
    Task<long> BurnAllAsync(ulong userId);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,17 +1,13 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
namespace NadekoBot.Modules.Gambling;
 | 
			
		||||
 | 
			
		||||
public class CashInteraction
 | 
			
		||||
public class CashInteraction : NInteraction
 | 
			
		||||
{
 | 
			
		||||
    public static NadekoInteractionData Data =
 | 
			
		||||
        new NadekoInteractionData(new Emoji("🏦"), "cash:bank_show_balance");
 | 
			
		||||
    protected override NadekoInteractionData Data
 | 
			
		||||
        => new NadekoInteractionData(new Emoji("🏦"), "cash:bank_show_balance");
 | 
			
		||||
 | 
			
		||||
    public static NadekoInteraction CreateInstance(
 | 
			
		||||
        DiscordSocketClient client,
 | 
			
		||||
        ulong userId,
 | 
			
		||||
        Func<SocketMessageComponent, Task> action)
 | 
			
		||||
        => new NadekoInteractionBuilder()
 | 
			
		||||
           .WithData(Data)
 | 
			
		||||
           .WithAction(action)
 | 
			
		||||
           .Build(client, userId);
 | 
			
		||||
    public CashInteraction(DiscordSocketClient client, ulong userId, Func<SocketMessageComponent, Task> action)
 | 
			
		||||
        : base(client, userId, action)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -3,11 +3,13 @@ using LinqToDB;
 | 
			
		||||
using LinqToDB.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Db;
 | 
			
		||||
using NadekoBot.Db.Models;
 | 
			
		||||
using NadekoBot.Modules.Utility.Patronage;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Bank;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Common;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Services;
 | 
			
		||||
using NadekoBot.Services.Currency;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
using System.Collections.Immutable;
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.Text;
 | 
			
		||||
 | 
			
		||||
@@ -42,6 +44,7 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
			
		||||
    private readonly DownloadTracker _tracker;
 | 
			
		||||
    private readonly GamblingConfigService _configService;
 | 
			
		||||
    private readonly IBankService _bank;
 | 
			
		||||
    private readonly IPatronageService _ps;
 | 
			
		||||
 | 
			
		||||
    private IUserMessage rdMsg;
 | 
			
		||||
 | 
			
		||||
@@ -52,7 +55,8 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
			
		||||
        DiscordSocketClient client,
 | 
			
		||||
        DownloadTracker tracker,
 | 
			
		||||
        GamblingConfigService configService,
 | 
			
		||||
        IBankService bank)
 | 
			
		||||
        IBankService bank,
 | 
			
		||||
        IPatronageService ps)
 | 
			
		||||
        : base(configService)
 | 
			
		||||
    {
 | 
			
		||||
        _db = db;
 | 
			
		||||
@@ -60,6 +64,7 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
			
		||||
        _cache = cache;
 | 
			
		||||
        _client = client;
 | 
			
		||||
        _bank = bank;
 | 
			
		||||
        _ps = ps;
 | 
			
		||||
 | 
			
		||||
        _enUsCulture = new CultureInfo("en-US", false).NumberFormat;
 | 
			
		||||
        _enUsCulture.NumberDecimalDigits = 0;
 | 
			
		||||
@@ -102,6 +107,12 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
			
		||||
        await ctx.Channel.EmbedAsync(embed);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static readonly FeatureLimitKey _timelyKey = new FeatureLimitKey()
 | 
			
		||||
    {
 | 
			
		||||
        Key = "timely:extra_percent",
 | 
			
		||||
        PrettyName = "Timely"
 | 
			
		||||
    };
 | 
			
		||||
    
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    public async partial Task Timely()
 | 
			
		||||
    {
 | 
			
		||||
@@ -119,6 +130,10 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var result = await _ps.TryGetFeatureLimitAsync(_timelyKey, ctx.User.Id, 0);
 | 
			
		||||
 | 
			
		||||
        val = (int)(val * (1 + (result.Quota * 0.01f)));
 | 
			
		||||
 | 
			
		||||
        await _cs.AddAsync(ctx.User.Id, val, new("timely", "claim"));
 | 
			
		||||
 | 
			
		||||
        await ReplyConfirmLocalizedAsync(strs.timely(N(val), period));
 | 
			
		||||
@@ -331,8 +346,8 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
			
		||||
              .Pipe(text => smc.RespondConfirmAsync(_eb, text, ephemeral: true));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private NadekoInteraction CreateCashInteraction()
 | 
			
		||||
        => CashInteraction.CreateInstance(_client, ctx.User.Id, BankAction);
 | 
			
		||||
    private NadekoButtonInteraction CreateCashInteraction()
 | 
			
		||||
        => new CashInteraction(_client, ctx.User.Id, BankAction).GetInteraction();
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [Priority(1)]
 | 
			
		||||
@@ -780,4 +795,31 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
			
		||||
 | 
			
		||||
        await ctx.Channel.EmbedAsync(embed);
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    private static readonly ImmutableArray<string> _emojis =
 | 
			
		||||
        new[] { "⬆", "↖", "⬅", "↙", "⬇", "↘", "➡", "↗" }.ToImmutableArray();
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    public async partial Task WheelOfFortune(ShmartNumber amount)
 | 
			
		||||
    {
 | 
			
		||||
        if (!await CheckBetMandatory(amount))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        if (!await _cs.RemoveAsync(ctx.User.Id, amount, new("wheel", "bet")))
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var result = await _service.WheelOfFortuneSpinAsync(ctx.User.Id, amount);
 | 
			
		||||
 | 
			
		||||
        var wofMultipliers = Config.WheelOfFortune.Multipliers;
 | 
			
		||||
        await SendConfirmAsync(Format.Bold($@"{ctx.User} won: {N(result.Amount)}
 | 
			
		||||
 | 
			
		||||
   『{wofMultipliers[1]}』   『{wofMultipliers[0]}』   『{wofMultipliers[7]}』
 | 
			
		||||
 | 
			
		||||
『{wofMultipliers[2]}』      {_emojis[result.Index]}      『{wofMultipliers[6]}』
 | 
			
		||||
 | 
			
		||||
     『{wofMultipliers[3]}』   『{wofMultipliers[4]}』   『{wofMultipliers[5]}』"));
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,49 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Modules.Gambling.Common;
 | 
			
		||||
using NadekoBot.Modules.Gambling.Services;
 | 
			
		||||
using System.Collections.Immutable;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Gambling;
 | 
			
		||||
 | 
			
		||||
public partial class Gambling
 | 
			
		||||
{
 | 
			
		||||
    public partial class WheelOfFortuneCommands : GamblingSubmodule<GamblingService>
 | 
			
		||||
    {
 | 
			
		||||
        private static readonly ImmutableArray<string> _emojis =
 | 
			
		||||
            new[] { "⬆", "↖", "⬅", "↙", "⬇", "↘", "➡", "↗" }.ToImmutableArray();
 | 
			
		||||
 | 
			
		||||
        private readonly ICurrencyService _cs;
 | 
			
		||||
        private readonly DbService _db;
 | 
			
		||||
 | 
			
		||||
        public WheelOfFortuneCommands(ICurrencyService cs, DbService db, GamblingConfigService gamblingConfService)
 | 
			
		||||
            : base(gamblingConfService)
 | 
			
		||||
        {
 | 
			
		||||
            _cs = cs;
 | 
			
		||||
            _db = db;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        public async partial Task WheelOfFortune(ShmartNumber amount)
 | 
			
		||||
        {
 | 
			
		||||
            if (!await CheckBetMandatory(amount))
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            if (!await _cs.RemoveAsync(ctx.User.Id, amount, new("wheel", "bet")))
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.not_enough(CurrencySign));
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var result = await _service.WheelOfFortuneSpinAsync(ctx.User.Id, amount);
 | 
			
		||||
 | 
			
		||||
            var wofMultipliers = Config.WheelOfFortune.Multipliers;
 | 
			
		||||
            await SendConfirmAsync(Format.Bold($@"{ctx.User} won: {N(result.Amount)}
 | 
			
		||||
 | 
			
		||||
   『{wofMultipliers[1]}』   『{wofMultipliers[0]}』   『{wofMultipliers[7]}』
 | 
			
		||||
 | 
			
		||||
『{wofMultipliers[2]}』      {_emojis[result.Index]}      『{wofMultipliers[6]}』
 | 
			
		||||
 | 
			
		||||
     『{wofMultipliers[3]}』   『{wofMultipliers[4]}』   『{wofMultipliers[5]}』"));
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,8 +1,11 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
using NadekoBot.Db.Models;
 | 
			
		||||
using NadekoBot.Modules.Games.Common.ChatterBot;
 | 
			
		||||
using NadekoBot.Modules.Permissions;
 | 
			
		||||
using NadekoBot.Modules.Permissions.Common;
 | 
			
		||||
using NadekoBot.Modules.Permissions.Services;
 | 
			
		||||
using NadekoBot.Modules.Utility.Patronage;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Games.Services;
 | 
			
		||||
 | 
			
		||||
@@ -13,6 +16,8 @@ public class ChatterBotService : IExecOnMessage
 | 
			
		||||
    public int Priority
 | 
			
		||||
        => 1;
 | 
			
		||||
 | 
			
		||||
    private readonly FeatureLimitKey _flKey;
 | 
			
		||||
 | 
			
		||||
    private readonly DiscordSocketClient _client;
 | 
			
		||||
    private readonly PermissionService _perms;
 | 
			
		||||
    private readonly CommandHandler _cmd;
 | 
			
		||||
@@ -20,6 +25,8 @@ public class ChatterBotService : IExecOnMessage
 | 
			
		||||
    private readonly IBotCredentials _creds;
 | 
			
		||||
    private readonly IEmbedBuilderService _eb;
 | 
			
		||||
    private readonly IHttpClientFactory _httpFactory;
 | 
			
		||||
    private readonly IPatronageService _ps;
 | 
			
		||||
    private readonly CmdCdService _ccs;
 | 
			
		||||
 | 
			
		||||
    public ChatterBotService(
 | 
			
		||||
        DiscordSocketClient client,
 | 
			
		||||
@@ -29,7 +36,9 @@ public class ChatterBotService : IExecOnMessage
 | 
			
		||||
        IBotStrings strings,
 | 
			
		||||
        IHttpClientFactory factory,
 | 
			
		||||
        IBotCredentials creds,
 | 
			
		||||
        IEmbedBuilderService eb)
 | 
			
		||||
        IEmbedBuilderService eb,
 | 
			
		||||
        IPatronageService ps,
 | 
			
		||||
        CmdCdService cmdCdService)
 | 
			
		||||
    {
 | 
			
		||||
        _client = client;
 | 
			
		||||
        _perms = perms;
 | 
			
		||||
@@ -38,8 +47,17 @@ public class ChatterBotService : IExecOnMessage
 | 
			
		||||
        _creds = creds;
 | 
			
		||||
        _eb = eb;
 | 
			
		||||
        _httpFactory = factory;
 | 
			
		||||
        _ps = ps;
 | 
			
		||||
        _ccs = cmdCdService;
 | 
			
		||||
 | 
			
		||||
        ChatterBotGuilds = new(bot.AllGuildConfigs.Where(gc => gc.CleverbotEnabled)
 | 
			
		||||
        _flKey = new FeatureLimitKey()
 | 
			
		||||
        {
 | 
			
		||||
            Key = CleverBotResponseStr.CLEVERBOT_RESPONSE,
 | 
			
		||||
            PrettyName = "Cleverbot Replies"
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
        ChatterBotGuilds = new(bot.AllGuildConfigs
 | 
			
		||||
                                  .Where(gc => gc.CleverbotEnabled)
 | 
			
		||||
                                  .ToDictionary(gc => gc.GuildId,
 | 
			
		||||
                                      _ => new Lazy<IChatterBotSession>(() => CreateSession(), true)));
 | 
			
		||||
    }
 | 
			
		||||
@@ -48,7 +66,9 @@ public class ChatterBotService : IExecOnMessage
 | 
			
		||||
    {
 | 
			
		||||
        if (!string.IsNullOrWhiteSpace(_creds.CleverbotApiKey))
 | 
			
		||||
            return new OfficialCleverbotSession(_creds.CleverbotApiKey, _httpFactory);
 | 
			
		||||
        return new CleverbotIoSession("GAh3wUfzDCpDpdpT", "RStKgqn7tcO9blbrv4KbXM8NDlb7H37C", _httpFactory);
 | 
			
		||||
 | 
			
		||||
        Log.Information("Cleverbot will not work as the api key is missing.");
 | 
			
		||||
        return null;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public string PrepareMessage(IUserMessage msg, out IChatterBotSession cleverbot)
 | 
			
		||||
@@ -78,27 +98,11 @@ public class ChatterBotService : IExecOnMessage
 | 
			
		||||
        return message;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> TryAsk(IChatterBotSession cleverbot, ITextChannel channel, string message)
 | 
			
		||||
    {
 | 
			
		||||
        await channel.TriggerTypingAsync();
 | 
			
		||||
 | 
			
		||||
        var response = await cleverbot.Think(message);
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await channel.SendConfirmAsync(_eb, response.SanitizeMentions(true));
 | 
			
		||||
        }
 | 
			
		||||
        catch
 | 
			
		||||
        {
 | 
			
		||||
            await channel.SendConfirmAsync(_eb, response.SanitizeMentions(true)); // try twice :\
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<bool> ExecOnMessageAsync(IGuild guild, IUserMessage usrMsg)
 | 
			
		||||
    {
 | 
			
		||||
        if (guild is not SocketGuild sg)
 | 
			
		||||
            return false;
 | 
			
		||||
        
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var message = PrepareMessage(usrMsg, out var cbs);
 | 
			
		||||
@@ -106,7 +110,10 @@ public class ChatterBotService : IExecOnMessage
 | 
			
		||||
                return false;
 | 
			
		||||
 | 
			
		||||
            var pc = _perms.GetCacheFor(guild.Id);
 | 
			
		||||
            if (!pc.Permissions.CheckPermissions(usrMsg, "cleverbot", "Games".ToLowerInvariant(), out var index))
 | 
			
		||||
            if (!pc.Permissions.CheckPermissions(usrMsg,
 | 
			
		||||
                    "cleverbot",
 | 
			
		||||
                    "games",
 | 
			
		||||
                    out var index))
 | 
			
		||||
            {
 | 
			
		||||
                if (pc.Verbose)
 | 
			
		||||
                {
 | 
			
		||||
@@ -122,24 +129,78 @@ public class ChatterBotService : IExecOnMessage
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var cleverbotExecuted = await TryAsk(cbs, (ITextChannel)usrMsg.Channel, message);
 | 
			
		||||
            if (cleverbotExecuted)
 | 
			
		||||
            if (await _ccs.TryBlock(sg, usrMsg.Author, CleverBotResponseStr.CLEVERBOT_RESPONSE))
 | 
			
		||||
            {
 | 
			
		||||
                Log.Information(@"CleverBot Executed
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var channel = (ITextChannel)usrMsg.Channel;
 | 
			
		||||
            var conf = _ps.GetConfig();
 | 
			
		||||
            if (conf.IsEnabled)
 | 
			
		||||
            {
 | 
			
		||||
                var quota = await _ps.TryGetFeatureLimitAsync(_flKey, sg.OwnerId, 0);
 | 
			
		||||
 | 
			
		||||
                uint? daily = quota.Quota is int dVal and < 0
 | 
			
		||||
                    ? (uint)-dVal
 | 
			
		||||
                    : null;
 | 
			
		||||
 | 
			
		||||
                uint? monthly = quota.Quota is int mVal and >= 0
 | 
			
		||||
                    ? (uint)mVal
 | 
			
		||||
                    : null;
 | 
			
		||||
                
 | 
			
		||||
                var maybeLimit = await _ps.TryIncrementQuotaCounterAsync(sg.OwnerId,
 | 
			
		||||
                    sg.OwnerId == usrMsg.Author.Id,
 | 
			
		||||
                    FeatureType.Limit,
 | 
			
		||||
                    _flKey.Key,
 | 
			
		||||
                    null,
 | 
			
		||||
                    daily,
 | 
			
		||||
                    monthly);
 | 
			
		||||
                
 | 
			
		||||
                if (maybeLimit.TryPickT1(out var ql, out var counters))
 | 
			
		||||
                {
 | 
			
		||||
                    if (ql.Quota == 0)
 | 
			
		||||
                    {
 | 
			
		||||
                        await channel.SendErrorAsync(_eb,
 | 
			
		||||
                            null!,
 | 
			
		||||
                            text:
 | 
			
		||||
                            "In order to use the cleverbot feature, the owner of this server should be [Patron Tier X](https://patreon.com/join/nadekobot) on patreon.",
 | 
			
		||||
                            footer:
 | 
			
		||||
                            "You may disable the cleverbot feature, and this message via '.cleverbot' command");
 | 
			
		||||
                        
 | 
			
		||||
                        return true;
 | 
			
		||||
                    }
 | 
			
		||||
 | 
			
		||||
                    await channel.SendErrorAsync(_eb,
 | 
			
		||||
                        null!,
 | 
			
		||||
                        $"You've reached your quota limit of **{ql.Quota}** responses {ql.QuotaPeriod.ToFullName()} for the cleverbot feature.",
 | 
			
		||||
                        footer: "You may wait for the quota reset or .");
 | 
			
		||||
                    
 | 
			
		||||
                    return true;
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            _ = channel.TriggerTypingAsync();
 | 
			
		||||
            var response = await cbs.Think(message);
 | 
			
		||||
            await channel.SendConfirmAsync(_eb,
 | 
			
		||||
                title: null,
 | 
			
		||||
                response.SanitizeMentions(true)
 | 
			
		||||
                // , footer: counter > 0 ? counter.ToString() : null
 | 
			
		||||
                );
 | 
			
		||||
 | 
			
		||||
            Log.Information(@"CleverBot Executed
 | 
			
		||||
Server: {GuildName} [{GuildId}]
 | 
			
		||||
Channel: {ChannelName} [{ChannelId}]
 | 
			
		||||
UserId: {Author} [{AuthorId}]
 | 
			
		||||
Message: {Content}",
 | 
			
		||||
                    guild.Name,
 | 
			
		||||
                    guild.Id,
 | 
			
		||||
                    usrMsg.Channel?.Name,
 | 
			
		||||
                    usrMsg.Channel?.Id,
 | 
			
		||||
                    usrMsg.Author,
 | 
			
		||||
                    usrMsg.Author.Id,
 | 
			
		||||
                    usrMsg.Content);
 | 
			
		||||
                guild.Name,
 | 
			
		||||
                guild.Id,
 | 
			
		||||
                usrMsg.Channel?.Name,
 | 
			
		||||
                usrMsg.Channel?.Id,
 | 
			
		||||
                usrMsg.Author,
 | 
			
		||||
                usrMsg.Author.Id,
 | 
			
		||||
                usrMsg.Content);
 | 
			
		||||
 | 
			
		||||
                return true;
 | 
			
		||||
            }
 | 
			
		||||
            return true;
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,6 @@ public partial class Games
 | 
			
		||||
        public ChatterBotCommands(DbService db)
 | 
			
		||||
            => _db = db;
 | 
			
		||||
 | 
			
		||||
        [NoPublicBot]
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [UserPerm(GuildPerm.ManageMessages)]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,34 +0,0 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using Newtonsoft.Json;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Games.Common.ChatterBot;
 | 
			
		||||
 | 
			
		||||
public class ChatterBotSession : IChatterBotSession
 | 
			
		||||
{
 | 
			
		||||
    private static NadekoRandom Rng { get; } = new();
 | 
			
		||||
 | 
			
		||||
    private string ApiEndpoint
 | 
			
		||||
        => "http://api.program-o.com/v2/chatbot/"
 | 
			
		||||
           + $"?bot_id={_botId}&"
 | 
			
		||||
           + "say={0}&"
 | 
			
		||||
           + $"convo_id=nadekobot_{_chatterBotId}&"
 | 
			
		||||
           + "format=json";
 | 
			
		||||
 | 
			
		||||
    private readonly string _chatterBotId;
 | 
			
		||||
    private readonly IHttpClientFactory _httpFactory;
 | 
			
		||||
    private readonly int _botId = 6;
 | 
			
		||||
 | 
			
		||||
    public ChatterBotSession(IHttpClientFactory httpFactory)
 | 
			
		||||
    {
 | 
			
		||||
        _chatterBotId = Rng.Next(0, 1000000).ToString().ToBase64();
 | 
			
		||||
        _httpFactory = httpFactory;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<string> Think(string message)
 | 
			
		||||
    {
 | 
			
		||||
        using var http = _httpFactory.CreateClient();
 | 
			
		||||
        var res = await http.GetStringAsync(string.Format(ApiEndpoint, message));
 | 
			
		||||
        var cbr = JsonConvert.DeserializeObject<ChatterBotResponse>(res);
 | 
			
		||||
        return cbr.BotSay.Replace("<br/>", "\n", StringComparison.InvariantCulture);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -5,16 +5,4 @@ public class CleverbotResponse
 | 
			
		||||
{
 | 
			
		||||
    public string Cs { get; set; }
 | 
			
		||||
    public string Output { get; set; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class CleverbotIoCreateResponse
 | 
			
		||||
{
 | 
			
		||||
    public string Status { get; set; }
 | 
			
		||||
    public string Nick { get; set; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class CleverbotIoAskResponse
 | 
			
		||||
{
 | 
			
		||||
    public string Status { get; set; }
 | 
			
		||||
    public string Response { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -35,57 +35,4 @@ public class OfficialCleverbotSession : IChatterBotSession
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public class CleverbotIoSession : IChatterBotSession
 | 
			
		||||
{
 | 
			
		||||
    private readonly string _key;
 | 
			
		||||
    private readonly string _user;
 | 
			
		||||
    private readonly IHttpClientFactory _httpFactory;
 | 
			
		||||
    private readonly AsyncLazy<string> _nick;
 | 
			
		||||
 | 
			
		||||
    private readonly string _createEndpoint = "https://cleverbot.io/1.0/create";
 | 
			
		||||
    private readonly string _askEndpoint = "https://cleverbot.io/1.0/ask";
 | 
			
		||||
 | 
			
		||||
    public CleverbotIoSession(string user, string key, IHttpClientFactory factory)
 | 
			
		||||
    {
 | 
			
		||||
        _key = key;
 | 
			
		||||
        _user = user;
 | 
			
		||||
        _httpFactory = factory;
 | 
			
		||||
 | 
			
		||||
        _nick = new(GetNick);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task<string> GetNick()
 | 
			
		||||
    {
 | 
			
		||||
        using var http = _httpFactory.CreateClient();
 | 
			
		||||
        using var msg = new FormUrlEncodedContent(new[]
 | 
			
		||||
        {
 | 
			
		||||
            new KeyValuePair<string, string>("user", _user), new KeyValuePair<string, string>("key", _key)
 | 
			
		||||
        });
 | 
			
		||||
        using var data = await http.PostAsync(_createEndpoint, msg);
 | 
			
		||||
        var str = await data.Content.ReadAsStringAsync();
 | 
			
		||||
        var obj = JsonConvert.DeserializeObject<CleverbotIoCreateResponse>(str);
 | 
			
		||||
        if (obj.Status != "success")
 | 
			
		||||
            throw new OperationCanceledException(obj.Status);
 | 
			
		||||
 | 
			
		||||
        return obj.Nick;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<string> Think(string input)
 | 
			
		||||
    {
 | 
			
		||||
        using var http = _httpFactory.CreateClient();
 | 
			
		||||
        using var msg = new FormUrlEncodedContent(new[]
 | 
			
		||||
        {
 | 
			
		||||
            new KeyValuePair<string, string>("user", _user), new KeyValuePair<string, string>("key", _key),
 | 
			
		||||
            new KeyValuePair<string, string>("nick", await _nick), new KeyValuePair<string, string>("text", input)
 | 
			
		||||
        });
 | 
			
		||||
        using var data = await http.PostAsync(_askEndpoint, msg);
 | 
			
		||||
        var str = await data.Content.ReadAsStringAsync();
 | 
			
		||||
        var obj = JsonConvert.DeserializeObject<CleverbotIoAskResponse>(str);
 | 
			
		||||
        if (obj.Status != "success")
 | 
			
		||||
            throw new OperationCanceledException(obj.Status);
 | 
			
		||||
 | 
			
		||||
        return obj.Response;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -212,7 +212,7 @@ public partial class Help : NadekoModule<HelpService>
 | 
			
		||||
                cmds = cmds.Where(x => succ.Contains(x)).ToList();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var cmdsWithGroup = cmds.GroupBy(c => c.Module.Name.Replace("Commands", "", StringComparison.InvariantCulture))
 | 
			
		||||
        var cmdsWithGroup = cmds.GroupBy(c => c.Module.GetGroupName())
 | 
			
		||||
                                .OrderBy(x => x.Key == x.First().Module.Name ? int.MaxValue : x.Count())
 | 
			
		||||
                                .ToList();
 | 
			
		||||
 | 
			
		||||
@@ -294,7 +294,7 @@ public partial class Help : NadekoModule<HelpService>
 | 
			
		||||
 | 
			
		||||
        if (fail.StartsWith(prefix))
 | 
			
		||||
            fail = fail.Substring(prefix.Length);
 | 
			
		||||
        
 | 
			
		||||
 | 
			
		||||
        var group = _cmds.Modules
 | 
			
		||||
                         .SelectMany(x => x.Submodules)
 | 
			
		||||
                         .Where(x => !string.IsNullOrWhiteSpace(x.Group))
 | 
			
		||||
@@ -393,11 +393,6 @@ public partial class Help : NadekoModule<HelpService>
 | 
			
		||||
            };
 | 
			
		||||
 | 
			
		||||
            using var dlClient = new AmazonS3Client(accessKey, secretAcccessKey, config);
 | 
			
		||||
            using var oldVersionObject = await dlClient.GetObjectAsync(new()
 | 
			
		||||
            {
 | 
			
		||||
                BucketName = "nadeko-pictures",
 | 
			
		||||
                Key = "cmds/versions.json"
 | 
			
		||||
            });
 | 
			
		||||
 | 
			
		||||
            using (var client = new AmazonS3Client(accessKey, secretAcccessKey, config))
 | 
			
		||||
            {
 | 
			
		||||
@@ -407,14 +402,29 @@ public partial class Help : NadekoModule<HelpService>
 | 
			
		||||
                    ContentType = "application/json",
 | 
			
		||||
                    ContentBody = uploadData,
 | 
			
		||||
                    // either use a path provided in the argument or the default one for public nadeko, other/cmds.json
 | 
			
		||||
                    Key = $"cmds/{StatsService.BOT_VERSION}.json",
 | 
			
		||||
                    Key = $"cmds/v4/{StatsService.BOT_VERSION}.json",
 | 
			
		||||
                    CannedACL = S3CannedACL.PublicRead
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await using var ms = new MemoryStream();
 | 
			
		||||
            await oldVersionObject.ResponseStream.CopyToAsync(ms);
 | 
			
		||||
            var versionListString = Encoding.UTF8.GetString(ms.ToArray());
 | 
			
		||||
 | 
			
		||||
            var versionListString = "[]";
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                using var oldVersionObject = await dlClient.GetObjectAsync(new()
 | 
			
		||||
                {
 | 
			
		||||
                    BucketName = "nadeko-pictures",
 | 
			
		||||
                    Key = "cmds/v4/versions.json"
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
                await using var ms = new MemoryStream();
 | 
			
		||||
                await oldVersionObject.ResponseStream.CopyToAsync(ms);
 | 
			
		||||
                versionListString = Encoding.UTF8.GetString(ms.ToArray());
 | 
			
		||||
            }
 | 
			
		||||
            catch (Exception)
 | 
			
		||||
            {
 | 
			
		||||
                Log.Information("No old version list found. Creating a new one.");
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var versionList = JsonSerializer.Deserialize<List<string>>(versionListString);
 | 
			
		||||
            if (versionList is not null && !versionList.Contains(StatsService.BOT_VERSION))
 | 
			
		||||
@@ -435,7 +445,7 @@ public partial class Help : NadekoModule<HelpService>
 | 
			
		||||
                    ContentType = "application/json",
 | 
			
		||||
                    ContentBody = versionListString,
 | 
			
		||||
                    // either use a path provided in the argument or the default one for public nadeko, other/cmds.json
 | 
			
		||||
                    Key = "cmds/versions.json",
 | 
			
		||||
                    Key = "cmds/v4/versions.json",
 | 
			
		||||
                    CannedACL = S3CannedACL.PublicRead
 | 
			
		||||
                });
 | 
			
		||||
            }
 | 
			
		||||
@@ -455,9 +465,71 @@ public partial class Help : NadekoModule<HelpService>
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    public async partial Task Guide()
 | 
			
		||||
        => await ConfirmLocalizedAsync(strs.guide("https://nadeko.bot/commands",
 | 
			
		||||
            "http://nadekobot.readthedocs.io/en/latest/"));
 | 
			
		||||
            "https://nadekobot.readthedocs.io/en/latest/"));
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    private Task SelfhostAction(SocketMessageComponent smc)
 | 
			
		||||
        => smc.RespondConfirmAsync(_eb,
 | 
			
		||||
            @"- In case you don't want or cannot Donate to NadekoBot project, but you 
 | 
			
		||||
- NadekoBot is a completely free and fully [open source](https://gitlab.com/kwoth/nadekobot) project which means you can run your own ""selfhosted"" instance on your computer or server for free.
 | 
			
		||||
 | 
			
		||||
*Keep in mind that running the bot on your computer means that the bot will be offline when you turn off your computer*
 | 
			
		||||
 | 
			
		||||
- You can find the selfhosting guides by using the `.guide` command and clicking on the second link that pops up.
 | 
			
		||||
- If you decide to selfhost the bot, still consider [supporting the project](https://patreon.com/join/nadekobot) to keep the development going :)",
 | 
			
		||||
            true);
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    [OnlyPublicBot]
 | 
			
		||||
    public async partial Task Donate()
 | 
			
		||||
        => await ReplyConfirmLocalizedAsync(strs.donate(PATREON_URL, PAYPAL_URL));
 | 
			
		||||
    {
 | 
			
		||||
        var selfhostInter = new DonateSelfhostingInteraction(_client, ctx.User.Id, SelfhostAction);
 | 
			
		||||
        
 | 
			
		||||
        var eb = _eb.Create(ctx)
 | 
			
		||||
                    .WithOkColor()
 | 
			
		||||
                    .WithTitle("Thank you for considering to donate to the NadekoBot project!");
 | 
			
		||||
 | 
			
		||||
        eb
 | 
			
		||||
            .WithDescription("NadekoBot relies on donations to keep the servers, services and APIs running.\n"
 | 
			
		||||
                             + "Donating will give you access to some exclusive features. You can read about them on the [patreon page](https://patreon.com/join/nadekobot)")
 | 
			
		||||
            .AddField("Donation Instructions",
 | 
			
		||||
                $@"
 | 
			
		||||
🗒️ Before pledging it is recommended to open your DMs as Nadeko will send you a welcome message with instructions after you pledge has been processed and confirmed.
 | 
			
		||||
 | 
			
		||||
**Step 1:** ❤️ Pledge on Patreon ❤️
 | 
			
		||||
 | 
			
		||||
`1.` Go to <https://patreon.com/join/nadekobot> and choose a tier.
 | 
			
		||||
`2.` Make sure your payment is processed and accepted.
 | 
			
		||||
 | 
			
		||||
**Step 2** 🤝 Connect your Discord account 🤝
 | 
			
		||||
 | 
			
		||||
`1.` Go to your profile settings on Patreon and connect your Discord account to it.
 | 
			
		||||
*please make sure you're logged into the correct Discord account*
 | 
			
		||||
 | 
			
		||||
If you do not know how to do it, you may follow instructions in this link:
 | 
			
		||||
<https://support.patreon.com/hc/en-us/articles/212052266-How-do-I-connect-Discord-to-Patreon-Patron->
 | 
			
		||||
 | 
			
		||||
**Step 3** ⏰ Wait a short while (usually 1-3 minutes) ⏰
 | 
			
		||||
  
 | 
			
		||||
Nadeko will DM you the welcome instructions, and you may start using the patron-only commands and features!
 | 
			
		||||
🎉 **Enjoy!** 🎉
 | 
			
		||||
")
 | 
			
		||||
            .AddField("Troubleshooting",
 | 
			
		||||
                @"
 | 
			
		||||
*In case you didn't receive the rewards within 5 minutes:*
 | 
			
		||||
`1.` Make sure your DMs are open to everyone. Maybe your pledge was processed successfully but the bot was unable to DM you. Use the `.patron` command to check your status.
 | 
			
		||||
`2.` Make sure you've connected the CORRECT Discord account. Quite often users log in to different Discord accounts in their browser. You may also try disconnecting and reconnecting your account.
 | 
			
		||||
`3.` Make sure your payment has been processed and not declined by Patreon.
 | 
			
		||||
`4.` If any of the previous steps don't help, you can join the nadeko support server <https://discord.nadeko.bot> and ask for help in the #help channel");
 | 
			
		||||
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            await (await ctx.User.CreateDMChannelAsync()).EmbedAsync(eb, inter: selfhostInter.GetInteraction());
 | 
			
		||||
            _ = ctx.OkAsync();
 | 
			
		||||
        }
 | 
			
		||||
        catch
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.cant_dm);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -131,6 +131,25 @@ public class HelpService : IExecNoCommand, INService
 | 
			
		||||
 | 
			
		||||
        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 userPerm = (UserPermAttribute)cmd.Preconditions.FirstOrDefault(ca => ca is UserPermAttribute);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,12 @@
 | 
			
		||||
namespace NadekoBot.Modules.Help;
 | 
			
		||||
 | 
			
		||||
public class DonateSelfhostingInteraction : NInteraction
 | 
			
		||||
{
 | 
			
		||||
    protected override NadekoInteractionData Data
 | 
			
		||||
        => new NadekoInteractionData(new Emoji("🖥️"), "donate:selfhosting", "Selfhosting");
 | 
			
		||||
    
 | 
			
		||||
    public DonateSelfhostingInteraction(DiscordSocketClient client, ulong userId, Func<SocketMessageComponent, Task> action)
 | 
			
		||||
        : base(client, userId, action)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,12 @@
 | 
			
		||||
namespace NadekoBot.Modules.Help;
 | 
			
		||||
 | 
			
		||||
public class DonateTroubleshootInteraction : NInteraction
 | 
			
		||||
{
 | 
			
		||||
    protected override NadekoInteractionData Data
 | 
			
		||||
        => new NadekoInteractionData(new Emoji("❓"), "donate:troubleshoot", "Troubleshoot");
 | 
			
		||||
    
 | 
			
		||||
    public DonateTroubleshootInteraction(DiscordSocketClient client, ulong userId, Func<SocketMessageComponent, Task> action)
 | 
			
		||||
        : base(client, userId, action)
 | 
			
		||||
    {
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -119,7 +119,7 @@ public sealed partial class YtLoader
 | 
			
		||||
        var responseSpan = response.AsSpan()[140_000..];
 | 
			
		||||
        var startIndex = responseSpan.IndexOf(_ytResultInitialData);
 | 
			
		||||
        if (startIndex == -1)
 | 
			
		||||
            return null; // todo future try selecting html
 | 
			
		||||
            return null; // FUTURE try selecting html
 | 
			
		||||
        startIndex += _ytResultInitialData.Length;
 | 
			
		||||
 | 
			
		||||
        var endIndex =
 | 
			
		||||
 
 | 
			
		||||
@@ -219,7 +219,7 @@ public sealed class MusicPlayer : IMusicPlayer
 | 
			
		||||
                                errorCount = 0;
 | 
			
		||||
                            }
 | 
			
		||||
 | 
			
		||||
                            // todo future windows multimedia api
 | 
			
		||||
                            // FUTURE windows multimedia api
 | 
			
		||||
 | 
			
		||||
                            // wait for slightly less than the latency
 | 
			
		||||
                            Thread.Sleep(delay);
 | 
			
		||||
 
 | 
			
		||||
@@ -42,8 +42,6 @@ public class RadioResolver : IRadioResolver
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (query.Contains(".pls"))
 | 
			
		||||
            //File1=http://armitunes.com:8000/
 | 
			
		||||
            //Regex.Match(query)
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
@@ -59,11 +57,6 @@ public class RadioResolver : IRadioResolver
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (query.Contains(".m3u"))
 | 
			
		||||
            /* 
 | 
			
		||||
    # This is a comment
 | 
			
		||||
                   C:\xxx4xx\xxxxxx3x\xx2xxxx\xx.mp3
 | 
			
		||||
                   C:\xxx5xx\x6xxxxxx\x7xxxxx\xx.mp3
 | 
			
		||||
                */
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
@@ -79,7 +72,6 @@ public class RadioResolver : IRadioResolver
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (query.Contains(".asx"))
 | 
			
		||||
            //<ref href="http://armitunes.com:8000"/>
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
@@ -95,12 +87,6 @@ public class RadioResolver : IRadioResolver
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if (query.Contains(".xspf"))
 | 
			
		||||
            /*
 | 
			
		||||
                <?xml version="1.0" encoding="UTF-8"?>
 | 
			
		||||
                    <playlist version="1" xmlns="http://xspf.org/ns/0/">
 | 
			
		||||
                        <trackList>
 | 
			
		||||
                            <track><location>file:///mp3s/song_1.mp3</location></track>
 | 
			
		||||
                */
 | 
			
		||||
        {
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,10 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using System.Runtime.InteropServices;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Permissions;
 | 
			
		||||
 | 
			
		||||
[StructLayout(LayoutKind.Sequential, Size = 1)]
 | 
			
		||||
public readonly struct CleverBotResponseStr
 | 
			
		||||
{
 | 
			
		||||
    public const string CLEVERBOT_RESPONSE = "cleverbot:response";
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,15 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using NadekoBot.Common.TypeReaders;
 | 
			
		||||
using static NadekoBot.Common.TypeReaders.TypeReaderResult;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Permissions;
 | 
			
		||||
 | 
			
		||||
public class CleverbotResponseCmdCdTypeReader : NadekoTypeReader<CleverBotResponseStr>
 | 
			
		||||
{
 | 
			
		||||
    public override ValueTask<TypeReaderResult<CleverBotResponseStr>> ReadAsync(
 | 
			
		||||
        ICommandContext ctx,
 | 
			
		||||
        string input)
 | 
			
		||||
        => input.ToLowerInvariant() == CleverBotResponseStr.CLEVERBOT_RESPONSE
 | 
			
		||||
            ? new(FromSuccess(new CleverBotResponseStr()))
 | 
			
		||||
            : new(FromError<CleverBotResponseStr>(CommandError.ParseFailed, "Not a valid cleverbot"));
 | 
			
		||||
}
 | 
			
		||||
@@ -27,9 +27,7 @@ public partial class Permissions
 | 
			
		||||
            _db = db;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        public async partial Task CmdCooldown(CommandOrCrInfo command, int secs)
 | 
			
		||||
        private async Task CmdCooldownInternal(string cmdName, int secs)
 | 
			
		||||
        {
 | 
			
		||||
            var channel = (ITextChannel)ctx.Channel;
 | 
			
		||||
            if (secs is < 0 or > 3600)
 | 
			
		||||
@@ -38,7 +36,7 @@ public partial class Permissions
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var name = command.Name.ToLowerInvariant();
 | 
			
		||||
            var name = cmdName.ToLowerInvariant();
 | 
			
		||||
            await using (var uow = _db.GetDbContext())
 | 
			
		||||
            {
 | 
			
		||||
                var config = uow.GuildConfigsForId(channel.Guild.Id, set => set.Include(gc => gc.CommandCooldowns));
 | 
			
		||||
@@ -71,6 +69,18 @@ public partial class Permissions
 | 
			
		||||
            else
 | 
			
		||||
                await ReplyConfirmLocalizedAsync(strs.cmdcd_add(Format.Bold(name), Format.Bold(secs.ToString())));
 | 
			
		||||
        }
 | 
			
		||||
        
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [Priority(0)]
 | 
			
		||||
        public partial Task CmdCooldown(CleverBotResponseStr command, int secs)
 | 
			
		||||
            => CmdCooldownInternal(CleverBotResponseStr.CLEVERBOT_RESPONSE, secs);
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
        [Priority(1)]
 | 
			
		||||
        public partial Task CmdCooldown(CommandOrCrInfo command, int secs)
 | 
			
		||||
            => CmdCooldownInternal(command.Name, secs);
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        [RequireContext(ContextType.Guild)]
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,8 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using CodeHollow.FeedReader;
 | 
			
		||||
using CodeHollow.FeedReader.Feeds;
 | 
			
		||||
using LinqToDB;
 | 
			
		||||
using LinqToDB.EntityFrameworkCore;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Db;
 | 
			
		||||
using NadekoBot.Services.Database.Models;
 | 
			
		||||
@@ -10,11 +12,12 @@ namespace NadekoBot.Modules.Searches.Services;
 | 
			
		||||
public class FeedsService : INService
 | 
			
		||||
{
 | 
			
		||||
    private readonly DbService _db;
 | 
			
		||||
    private readonly ConcurrentDictionary<string, HashSet<FeedSub>> _subs;
 | 
			
		||||
    private readonly ConcurrentDictionary<string, List<FeedSub>> _subs;
 | 
			
		||||
    private readonly DiscordSocketClient _client;
 | 
			
		||||
    private readonly IEmbedBuilderService _eb;
 | 
			
		||||
 | 
			
		||||
    private readonly ConcurrentDictionary<string, DateTime> _lastPosts = new();
 | 
			
		||||
    private readonly Dictionary<string, uint> _errorCounters = new();
 | 
			
		||||
 | 
			
		||||
    public FeedsService(
 | 
			
		||||
        Bot bot,
 | 
			
		||||
@@ -33,7 +36,7 @@ public class FeedsService : INService
 | 
			
		||||
                       .ToList()
 | 
			
		||||
                       .SelectMany(x => x.FeedSubs)
 | 
			
		||||
                       .GroupBy(x => x.Url.ToLower())
 | 
			
		||||
                       .ToDictionary(x => x.Key, x => x.ToHashSet())
 | 
			
		||||
                       .ToDictionary(x => x.Key, x => x.ToList())
 | 
			
		||||
                       .ToConcurrent();
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -43,6 +46,35 @@ public class FeedsService : INService
 | 
			
		||||
        _ = Task.Run(TrackFeeds);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private void ClearErrors(string url)
 | 
			
		||||
        => _errorCounters.Remove(url);
 | 
			
		||||
 | 
			
		||||
    private async Task AddError(string url, List<int> ids)
 | 
			
		||||
    {
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var newValue = _errorCounters[url] = _errorCounters.GetValueOrDefault(url) + 1;
 | 
			
		||||
 | 
			
		||||
            if (newValue >= 100)
 | 
			
		||||
            {
 | 
			
		||||
                // remove from db
 | 
			
		||||
                await using var ctx = _db.GetDbContext();
 | 
			
		||||
                await ctx.GetTable<FeedSub>()
 | 
			
		||||
                         .DeleteAsync(x => ids.Contains(x.Id));
 | 
			
		||||
                
 | 
			
		||||
                // remove from the local cache
 | 
			
		||||
                _subs.TryRemove(url, out _);
 | 
			
		||||
 | 
			
		||||
                // reset the error counter
 | 
			
		||||
                ClearErrors(url);
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
        catch (Exception ex)
 | 
			
		||||
        {
 | 
			
		||||
            Log.Error(ex, "Error adding rss errors...");
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<EmbedBuilder> TrackFeeds()
 | 
			
		||||
    {
 | 
			
		||||
        while (true)
 | 
			
		||||
@@ -134,13 +166,17 @@ public class FeedsService : INService
 | 
			
		||||
                            embed.WithDescription(desc.TrimTo(2048));
 | 
			
		||||
 | 
			
		||||
                        //send the created embed to all subscribed channels
 | 
			
		||||
                        var feedSendTasks = kvp.Value.Where(x => x.GuildConfig is not null)
 | 
			
		||||
                        var feedSendTasks = kvp.Value
 | 
			
		||||
                                               .Where(x => x.GuildConfig is not null)
 | 
			
		||||
                                               .Select(x => _client.GetGuild(x.GuildConfig.GuildId)
 | 
			
		||||
                                                                   ?.GetTextChannel(x.ChannelId))
 | 
			
		||||
                                               .Where(x => x is not null)
 | 
			
		||||
                                               .Select(x => x.EmbedAsync(embed));
 | 
			
		||||
 | 
			
		||||
                        allSendTasks.Add(feedSendTasks.WhenAll());
 | 
			
		||||
 | 
			
		||||
                        // as data retrieval was sucessful, reset error counter
 | 
			
		||||
                        ClearErrors(rssUrl);
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
                catch (Exception ex)
 | 
			
		||||
@@ -149,6 +185,8 @@ public class FeedsService : INService
 | 
			
		||||
                                + "\n {Message}",
 | 
			
		||||
                        rssUrl,
 | 
			
		||||
                        $"[{ex.GetType().Name}]: {ex.Message}");
 | 
			
		||||
 | 
			
		||||
                    await AddError(rssUrl, kvp.Value.Select(x => x.Id).ToList());
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
@@ -188,7 +226,7 @@ public class FeedsService : INService
 | 
			
		||||
        foreach (var feed in gc.FeedSubs)
 | 
			
		||||
        {
 | 
			
		||||
            _subs.AddOrUpdate(feed.Url.ToLower(),
 | 
			
		||||
                new HashSet<FeedSub>
 | 
			
		||||
                new List<FeedSub>
 | 
			
		||||
                {
 | 
			
		||||
                    feed
 | 
			
		||||
                },
 | 
			
		||||
@@ -216,7 +254,7 @@ public class FeedsService : INService
 | 
			
		||||
            return false;
 | 
			
		||||
        var toRemove = items[index];
 | 
			
		||||
        _subs.AddOrUpdate(toRemove.Url.ToLower(),
 | 
			
		||||
            new HashSet<FeedSub>(),
 | 
			
		||||
            new List<FeedSub>(),
 | 
			
		||||
            (_, old) =>
 | 
			
		||||
            {
 | 
			
		||||
                old.Remove(toRemove);
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,60 @@
 | 
			
		||||
using NadekoBot.Modules.Searches.Youtube;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public sealed class DefaultSearchServiceFactory : ISearchServiceFactory, INService
 | 
			
		||||
{
 | 
			
		||||
    private readonly SearchesConfigService _scs;
 | 
			
		||||
    private readonly SearxSearchService _sss;
 | 
			
		||||
    private readonly GoogleSearchService _gss;
 | 
			
		||||
 | 
			
		||||
    private readonly YtdlpYoutubeSearchService _ytdlp;
 | 
			
		||||
    private readonly YtdlYoutubeSearchService _ytdl;
 | 
			
		||||
    private readonly YoutubeDataApiSearchService _ytdata;
 | 
			
		||||
    private readonly InvidiousYtSearchService _iYtSs;
 | 
			
		||||
 | 
			
		||||
    public DefaultSearchServiceFactory(
 | 
			
		||||
        SearchesConfigService scs,
 | 
			
		||||
        GoogleSearchService gss,
 | 
			
		||||
        SearxSearchService sss,
 | 
			
		||||
        YtdlpYoutubeSearchService ytdlp,
 | 
			
		||||
        YtdlYoutubeSearchService ytdl,
 | 
			
		||||
        YoutubeDataApiSearchService ytdata,
 | 
			
		||||
        InvidiousYtSearchService iYtSs)
 | 
			
		||||
    {
 | 
			
		||||
        _scs = scs;
 | 
			
		||||
        _sss = sss;
 | 
			
		||||
        _gss = gss;
 | 
			
		||||
        _iYtSs = iYtSs;
 | 
			
		||||
 | 
			
		||||
        _ytdlp = ytdlp;
 | 
			
		||||
        _ytdl = ytdl;
 | 
			
		||||
        _ytdata = ytdata;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public ISearchService GetSearchService(string? hint = null)
 | 
			
		||||
        => _scs.Data.WebSearchEngine switch
 | 
			
		||||
        {
 | 
			
		||||
            WebSearchEngine.Google => _gss,
 | 
			
		||||
            WebSearchEngine.Searx => _sss,
 | 
			
		||||
            _ => _gss
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
    public ISearchService GetImageSearchService(string? hint = null)
 | 
			
		||||
        => _scs.Data.ImgSearchEngine switch
 | 
			
		||||
        {
 | 
			
		||||
            ImgSearchEngine.Google => _gss,
 | 
			
		||||
            ImgSearchEngine.Searx => _sss,
 | 
			
		||||
            _ => _gss
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
    public IYoutubeSearchService GetYoutubeSearchService(string? hint = null)
 | 
			
		||||
        => _scs.Data.YtProvider switch
 | 
			
		||||
        {
 | 
			
		||||
            YoutubeSearcher.YtDataApiv3 => _ytdata,
 | 
			
		||||
            YoutubeSearcher.Ytdlp => _ytdlp,
 | 
			
		||||
            YoutubeSearcher.Ytdl => _ytdl,
 | 
			
		||||
            YoutubeSearcher.Invidious => _iYtSs,
 | 
			
		||||
            _ => _ytdl
 | 
			
		||||
        };
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,65 @@
 | 
			
		||||
// using AngleSharp.Html.Dom;
 | 
			
		||||
// using MorseCode.ITask;
 | 
			
		||||
// using NadekoBot.Modules.Searches.Common;
 | 
			
		||||
// using System.Net;
 | 
			
		||||
//
 | 
			
		||||
// namespace NadekoBot.Modules.Searches.DuckDuckGo;
 | 
			
		||||
//
 | 
			
		||||
// public sealed class DuckDuckGoSeachService : SearchServiceBase
 | 
			
		||||
// {
 | 
			
		||||
//     private static readonly HtmlParser _googleParser = new(new()
 | 
			
		||||
//     {
 | 
			
		||||
//         IsScripting = false,
 | 
			
		||||
//         IsEmbedded = false,
 | 
			
		||||
//         IsSupportingProcessingInstructions = false,
 | 
			
		||||
//         IsKeepingSourceReferences = false,
 | 
			
		||||
//         IsNotSupportingFrames = true
 | 
			
		||||
//     });
 | 
			
		||||
//     
 | 
			
		||||
//     public override async ITask<SearchResultData> SearchAsync(string query)
 | 
			
		||||
//     {
 | 
			
		||||
//         query = WebUtility.UrlEncode(query)?.Replace(' ', '+');
 | 
			
		||||
//     
 | 
			
		||||
//         var fullQueryLink = "https://html.duckduckgo.com/html";
 | 
			
		||||
//     
 | 
			
		||||
//         using var http = _httpFactory.CreateClient();
 | 
			
		||||
//         http.DefaultRequestHeaders.Clear();
 | 
			
		||||
//         http.DefaultRequestHeaders.Add("User-Agent",
 | 
			
		||||
//             "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36");
 | 
			
		||||
//     
 | 
			
		||||
//         using var formData = new MultipartFormDataContent();
 | 
			
		||||
//         formData.Add(new StringContent(query), "q");
 | 
			
		||||
//         using var response = await http.PostAsync(fullQueryLink, formData);
 | 
			
		||||
//         var content = await response.Content.ReadAsStringAsync();
 | 
			
		||||
//     
 | 
			
		||||
//         using var document = await _googleParser.ParseDocumentAsync(content);
 | 
			
		||||
//         var searchResults = document.QuerySelector(".results");
 | 
			
		||||
//         var elems = searchResults.QuerySelectorAll(".result");
 | 
			
		||||
//     
 | 
			
		||||
//         if (!elems.Any())
 | 
			
		||||
//             return default;
 | 
			
		||||
//     
 | 
			
		||||
//         var results = elems.Select(elem =>
 | 
			
		||||
//                            {
 | 
			
		||||
//                                if (elem.QuerySelector(".result__a") is not IHtmlAnchorElement anchor)
 | 
			
		||||
//                                    return null;
 | 
			
		||||
//     
 | 
			
		||||
//                                var href = anchor.Href;
 | 
			
		||||
//                                var name = anchor.TextContent;
 | 
			
		||||
//     
 | 
			
		||||
//                                if (string.IsNullOrWhiteSpace(href) || string.IsNullOrWhiteSpace(name))
 | 
			
		||||
//                                    return null;
 | 
			
		||||
//     
 | 
			
		||||
//                                var txt = elem.QuerySelector(".result__snippet")?.TextContent;
 | 
			
		||||
//     
 | 
			
		||||
//                                if (string.IsNullOrWhiteSpace(txt))
 | 
			
		||||
//                                    return null;
 | 
			
		||||
//     
 | 
			
		||||
//                                return new GoogleSearchResult(name, href, txt);
 | 
			
		||||
//                            })
 | 
			
		||||
//                            .Where(x => x is not null)
 | 
			
		||||
//                            .ToList();
 | 
			
		||||
//     
 | 
			
		||||
//         return new(results.AsReadOnly(), fullQueryLink, "0");
 | 
			
		||||
//     }
 | 
			
		||||
// }
 | 
			
		||||
@@ -0,0 +1,22 @@
 | 
			
		||||
using NadekoBot.Modules.Searches;
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Services;
 | 
			
		||||
 | 
			
		||||
public sealed class GoogleCustomSearchResult : ISearchResult
 | 
			
		||||
{
 | 
			
		||||
    ISearchResultInformation ISearchResult.Info
 | 
			
		||||
        => Info;
 | 
			
		||||
 | 
			
		||||
    public string? Answer
 | 
			
		||||
        => null;
 | 
			
		||||
 | 
			
		||||
    IReadOnlyCollection<ISearchResultEntry> ISearchResult.Entries
 | 
			
		||||
        => Entries ?? Array.Empty<OfficialGoogleSearchResultEntry>();
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("searchInformation")]
 | 
			
		||||
    public GoogleSearchResultInformation Info { get; init; } = null!;
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("items")]
 | 
			
		||||
    public IReadOnlyCollection<OfficialGoogleSearchResultEntry>? Entries { get; init; }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,12 @@
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Services;
 | 
			
		||||
 | 
			
		||||
public sealed class GoogleImageData
 | 
			
		||||
{
 | 
			
		||||
    [JsonPropertyName("contextLink")]
 | 
			
		||||
    public string ContextLink { get; init; } = null!;
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("thumbnailLink")]
 | 
			
		||||
    public string ThumbnailLink { get; init; } = null!;
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,19 @@
 | 
			
		||||
using NadekoBot.Modules.Searches;
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Services;
 | 
			
		||||
 | 
			
		||||
public sealed class GoogleImageResult : IImageSearchResult
 | 
			
		||||
{
 | 
			
		||||
    ISearchResultInformation IImageSearchResult.Info
 | 
			
		||||
        => Info;
 | 
			
		||||
 | 
			
		||||
    IReadOnlyCollection<IImageSearchResultEntry> IImageSearchResult.Entries
 | 
			
		||||
        => Entries ?? Array.Empty<GoogleImageResultEntry>();
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("searchInformation")]
 | 
			
		||||
    public GoogleSearchResultInformation Info { get; init; } = null!;
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("items")]
 | 
			
		||||
    public IReadOnlyCollection<GoogleImageResultEntry>? Entries { get; init; }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,13 @@
 | 
			
		||||
using NadekoBot.Modules.Searches;
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Services;
 | 
			
		||||
 | 
			
		||||
public sealed class GoogleImageResultEntry : IImageSearchResultEntry
 | 
			
		||||
{
 | 
			
		||||
    [JsonPropertyName("link")]
 | 
			
		||||
    public string Link { get; init; } = null!;
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("image")]
 | 
			
		||||
    public GoogleImageData Image { get; init; } = null!;
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,13 @@
 | 
			
		||||
using NadekoBot.Modules.Searches;
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Services;
 | 
			
		||||
 | 
			
		||||
public sealed class GoogleSearchResultInformation : ISearchResultInformation
 | 
			
		||||
{
 | 
			
		||||
    [JsonPropertyName("formattedTotalResults")]
 | 
			
		||||
    public string TotalResults { get; init; } = null!;
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("formattedSearchTime")]
 | 
			
		||||
    public string SearchTime { get; init; } = null!;
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,66 @@
 | 
			
		||||
using MorseCode.ITask;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public sealed class GoogleSearchService : SearchServiceBase, INService
 | 
			
		||||
{
 | 
			
		||||
    private readonly IBotCredsProvider _creds;
 | 
			
		||||
    private readonly IHttpClientFactory _httpFactory;
 | 
			
		||||
 | 
			
		||||
    public GoogleSearchService(IBotCredsProvider creds, IHttpClientFactory httpFactory)
 | 
			
		||||
    {
 | 
			
		||||
        _creds = creds;
 | 
			
		||||
        _httpFactory = httpFactory;
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    public override async ITask<GoogleImageResult?> SearchImagesAsync(string query)
 | 
			
		||||
    {
 | 
			
		||||
        ArgumentNullException.ThrowIfNull(query);
 | 
			
		||||
 | 
			
		||||
        var creds = _creds.GetCreds();
 | 
			
		||||
        var key = creds.Google.ImageSearchId;
 | 
			
		||||
        var cx = string.IsNullOrWhiteSpace(key)
 | 
			
		||||
            ? "c3f56de3be2034c07"
 | 
			
		||||
            : key;
 | 
			
		||||
        
 | 
			
		||||
        using var http = _httpFactory.CreateClient("google:search");
 | 
			
		||||
        http.DefaultRequestHeaders.Add("Accept-Encoding", "gzip");
 | 
			
		||||
        await using var stream = await http.GetStreamAsync(
 | 
			
		||||
            $"https://customsearch.googleapis.com/customsearch/v1"
 | 
			
		||||
            + $"?cx={cx}"
 | 
			
		||||
            + $"&q={Uri.EscapeDataString(query)}"
 | 
			
		||||
            + $"&fields=items(image(contextLink%2CthumbnailLink)%2Clink)%2CsearchInformation"
 | 
			
		||||
            + $"&key={creds.GoogleApiKey}"
 | 
			
		||||
            + $"&searchType=image"
 | 
			
		||||
            + $"&safe=active");
 | 
			
		||||
        
 | 
			
		||||
        var result = await System.Text.Json.JsonSerializer.DeserializeAsync<GoogleImageResult>(stream);
 | 
			
		||||
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override async ITask<GoogleCustomSearchResult?> SearchAsync(string query)
 | 
			
		||||
    {
 | 
			
		||||
        ArgumentNullException.ThrowIfNull(query);
 | 
			
		||||
 | 
			
		||||
        var creds = _creds.GetCreds();
 | 
			
		||||
        var key = creds.Google.SearchId;
 | 
			
		||||
        var cx = string.IsNullOrWhiteSpace(key)
 | 
			
		||||
            ? "c7f1dac95987d4571"
 | 
			
		||||
            : key;
 | 
			
		||||
        
 | 
			
		||||
        using var http = _httpFactory.CreateClient("google:search");
 | 
			
		||||
        http.DefaultRequestHeaders.Add("Accept-Encoding", "gzip");
 | 
			
		||||
        await using var stream = await http.GetStreamAsync(
 | 
			
		||||
            $"https://customsearch.googleapis.com/customsearch/v1"
 | 
			
		||||
            + $"?cx={cx}"
 | 
			
		||||
            + $"&q={Uri.EscapeDataString(query)}"
 | 
			
		||||
            + $"&fields=items(title%2Clink%2CdisplayLink%2Csnippet)%2CsearchInformation"
 | 
			
		||||
            + $"&key={creds.GoogleApiKey}"
 | 
			
		||||
            + $"&safe=active");
 | 
			
		||||
        
 | 
			
		||||
        var result = await System.Text.Json.JsonSerializer.DeserializeAsync<GoogleCustomSearchResult>(stream);
 | 
			
		||||
 | 
			
		||||
        return result;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,19 @@
 | 
			
		||||
using NadekoBot.Modules.Searches;
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Services;
 | 
			
		||||
 | 
			
		||||
public sealed class OfficialGoogleSearchResultEntry : ISearchResultEntry
 | 
			
		||||
{
 | 
			
		||||
    [JsonPropertyName("title")]
 | 
			
		||||
    public string Title { get; init; } = null!;
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("link")]
 | 
			
		||||
    public string Url { get; init; } = null!;
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("displayLink")]
 | 
			
		||||
    public string DisplayUrl { get; init; } = null!;
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("snippet")]
 | 
			
		||||
    public string Description { get; init; } = null!;
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,62 @@
 | 
			
		||||
// using AngleSharp.Html.Dom;
 | 
			
		||||
// using MorseCode.ITask;
 | 
			
		||||
// using NadekoBot.Modules.Searches.Common;
 | 
			
		||||
//
 | 
			
		||||
// namespace NadekoBot.Modules.Searches.GoogleScrape;
 | 
			
		||||
//
 | 
			
		||||
// public sealed class GoogleScrapeService : SearchServiceBase
 | 
			
		||||
// {
 | 
			
		||||
//     public override async ITask<GoogleSearchResultData> SearchAsync(string query)
 | 
			
		||||
//     {
 | 
			
		||||
//         ArgumentNullException.ThrowIfNull(query);
 | 
			
		||||
//         
 | 
			
		||||
//         query = Uri.EscapeDataString(query)?.Replace(' ', '+');
 | 
			
		||||
//
 | 
			
		||||
//         var fullQueryLink = $"https://www.google.ca/search?q={query}&safe=on&lr=lang_eng&hl=en&ie=utf-8&oe=utf-8";
 | 
			
		||||
//
 | 
			
		||||
//         using var msg = new HttpRequestMessage(HttpMethod.Get, fullQueryLink);
 | 
			
		||||
//         msg.Headers.Add("User-Agent",
 | 
			
		||||
//             "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36");
 | 
			
		||||
//         msg.Headers.Add("Cookie", "CONSENT=YES+shp.gws-20210601-0-RC2.en+FX+423;");
 | 
			
		||||
//
 | 
			
		||||
//         using var http = _httpFactory.CreateClient();
 | 
			
		||||
//         http.DefaultRequestHeaders.Clear();
 | 
			
		||||
//
 | 
			
		||||
//         using var response = await http.SendAsync(msg);
 | 
			
		||||
//         await using var content = await response.Content.ReadAsStreamAsync();
 | 
			
		||||
//
 | 
			
		||||
//         using var document = await _googleParser.ParseDocumentAsync(content);
 | 
			
		||||
//         var elems = document.QuerySelectorAll("div.g > div > div");
 | 
			
		||||
//
 | 
			
		||||
//         var resultsElem = document.QuerySelectorAll("#resultStats").FirstOrDefault();
 | 
			
		||||
//         var totalResults = resultsElem?.TextContent;
 | 
			
		||||
//         //var time = resultsElem.Children.FirstOrDefault()?.TextContent
 | 
			
		||||
//         //^ this doesn't work for some reason, <nobr> is completely missing in parsed collection
 | 
			
		||||
//         if (!elems.Any())
 | 
			
		||||
//             return default;
 | 
			
		||||
//
 | 
			
		||||
//         var results = elems.Select(elem =>
 | 
			
		||||
//                            {
 | 
			
		||||
//                                var children = elem.Children.ToList();
 | 
			
		||||
//                                if (children.Count < 2)
 | 
			
		||||
//                                    return null;
 | 
			
		||||
//
 | 
			
		||||
//                                var href = (children[0].QuerySelector("a") as IHtmlAnchorElement)?.Href;
 | 
			
		||||
//                                var name = children[0].QuerySelector("h3")?.TextContent;
 | 
			
		||||
//
 | 
			
		||||
//                                if (href is null || name is null)
 | 
			
		||||
//                                    return null;
 | 
			
		||||
//
 | 
			
		||||
//                                var txt = children[1].TextContent;
 | 
			
		||||
//
 | 
			
		||||
//                                if (string.IsNullOrWhiteSpace(txt))
 | 
			
		||||
//                                    return null;
 | 
			
		||||
//
 | 
			
		||||
//                                return new GoogleSearchResult(name, href, txt);
 | 
			
		||||
//                            })
 | 
			
		||||
//                            .Where(x => x is not null)
 | 
			
		||||
//                            .ToList();
 | 
			
		||||
//
 | 
			
		||||
//         return new(results.AsReadOnly(), fullQueryLink, totalResults);
 | 
			
		||||
//     }
 | 
			
		||||
// }
 | 
			
		||||
							
								
								
									
										13
									
								
								src/NadekoBot/Modules/Searches/Search/IImageSearchResult.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								src/NadekoBot/Modules/Searches/Search/IImageSearchResult.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,13 @@
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public interface IImageSearchResult
 | 
			
		||||
{
 | 
			
		||||
    ISearchResultInformation Info { get; }
 | 
			
		||||
    
 | 
			
		||||
    IReadOnlyCollection<IImageSearchResultEntry> Entries { get; }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public interface IImageSearchResultEntry
 | 
			
		||||
{
 | 
			
		||||
    string Link { get; }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										8
									
								
								src/NadekoBot/Modules/Searches/Search/ISearchResult.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								src/NadekoBot/Modules/Searches/Search/ISearchResult.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,8 @@
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public interface ISearchResult
 | 
			
		||||
{
 | 
			
		||||
    string? Answer { get; }
 | 
			
		||||
    IReadOnlyCollection<ISearchResultEntry> Entries { get; }
 | 
			
		||||
    ISearchResultInformation Info { get; }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,9 @@
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public interface ISearchResultEntry
 | 
			
		||||
{
 | 
			
		||||
    string Title { get; }
 | 
			
		||||
    string Url { get; }
 | 
			
		||||
    string DisplayUrl { get; }
 | 
			
		||||
    string? Description { get; }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,7 @@
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public interface ISearchResultInformation
 | 
			
		||||
{
 | 
			
		||||
    string TotalResults { get; }
 | 
			
		||||
    string SearchTime { get; }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										9
									
								
								src/NadekoBot/Modules/Searches/Search/ISearchService.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								src/NadekoBot/Modules/Searches/Search/ISearchService.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,9 @@
 | 
			
		||||
using MorseCode.ITask;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public interface ISearchService
 | 
			
		||||
{
 | 
			
		||||
    ITask<ISearchResult?> SearchAsync(string query);
 | 
			
		||||
    ITask<IImageSearchResult?> SearchImagesAsync(string query);
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,10 @@
 | 
			
		||||
using NadekoBot.Modules.Searches.Youtube;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public interface ISearchServiceFactory
 | 
			
		||||
{
 | 
			
		||||
    public ISearchService GetSearchService(string? hint = null);
 | 
			
		||||
    public ISearchService GetImageSearchService(string? hint = null);
 | 
			
		||||
    public IYoutubeSearchService GetYoutubeSearchService(string? hint = null);
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										206
									
								
								src/NadekoBot/Modules/Searches/Search/SearchCommands.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										206
									
								
								src/NadekoBot/Modules/Searches/Search/SearchCommands.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,206 @@
 | 
			
		||||
using NadekoBot.Modules.Searches.Youtube;
 | 
			
		||||
using StackExchange.Redis;
 | 
			
		||||
using System.Net.Http.Json;
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public partial class Searches
 | 
			
		||||
{
 | 
			
		||||
    public partial class SearchCommands : NadekoModule
 | 
			
		||||
    {
 | 
			
		||||
        private readonly ISearchServiceFactory _searchFactory;
 | 
			
		||||
        private readonly ConnectionMultiplexer _redis;
 | 
			
		||||
 | 
			
		||||
        public SearchCommands(
 | 
			
		||||
            ISearchServiceFactory searchFactory,
 | 
			
		||||
            ConnectionMultiplexer redis)
 | 
			
		||||
        {
 | 
			
		||||
            _searchFactory = searchFactory;
 | 
			
		||||
            _redis = redis;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        public async partial Task Google([Leftover] string? query = null)
 | 
			
		||||
        {
 | 
			
		||||
            query = query?.Trim();
 | 
			
		||||
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(query))
 | 
			
		||||
            {
 | 
			
		||||
                await ErrorLocalizedAsync(strs.specify_search_params);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            _ = ctx.Channel.TriggerTypingAsync();
 | 
			
		||||
 | 
			
		||||
            var search = _searchFactory.GetSearchService();
 | 
			
		||||
            var data = await search.SearchAsync(query);
 | 
			
		||||
 | 
			
		||||
            if (data is null or { Entries: null or { Count: 0 } })
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.no_results);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // 3 with an answer
 | 
			
		||||
            // 4 without an answer
 | 
			
		||||
            // 5 is ideal but it lookes horrible on mobile
 | 
			
		||||
 | 
			
		||||
            var takeCount = string.IsNullOrWhiteSpace(data.Answer)
 | 
			
		||||
                ? 4
 | 
			
		||||
                : 3;
 | 
			
		||||
 | 
			
		||||
            var descStr = data.Entries
 | 
			
		||||
                              .Take(takeCount)
 | 
			
		||||
                              .Select(static res => $@"**[{Format.Sanitize(res.Title)}]({res.Url})**
 | 
			
		||||
*{Format.EscapeUrl(res.DisplayUrl)}*
 | 
			
		||||
{Format.Sanitize(res.Description ?? "-")}")
 | 
			
		||||
                              .Join("\n\n");
 | 
			
		||||
 | 
			
		||||
            if (!string.IsNullOrWhiteSpace(data.Answer))
 | 
			
		||||
                descStr = Format.Code(data.Answer) + "\n\n" + descStr;
 | 
			
		||||
 | 
			
		||||
            descStr = descStr.TrimTo(4096);
 | 
			
		||||
 | 
			
		||||
            var embed = _eb.Create()
 | 
			
		||||
                           .WithOkColor()
 | 
			
		||||
                           .WithAuthor(ctx.User)
 | 
			
		||||
                           .WithTitle(query.TrimTo(64)!)
 | 
			
		||||
                           .WithDescription(descStr)
 | 
			
		||||
                           .WithFooter(
 | 
			
		||||
                               GetText(strs.results_in(data.Info.TotalResults, data.Info.SearchTime)),
 | 
			
		||||
                               "https://i.imgur.com/G46fm8J.png");
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.EmbedAsync(embed);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        public async partial Task Image([Leftover] string? query = null)
 | 
			
		||||
        {
 | 
			
		||||
            query = query?.Trim();
 | 
			
		||||
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(query))
 | 
			
		||||
            {
 | 
			
		||||
                await ErrorLocalizedAsync(strs.specify_search_params);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            _ = ctx.Channel.TriggerTypingAsync();
 | 
			
		||||
 | 
			
		||||
            var search = _searchFactory.GetImageSearchService();
 | 
			
		||||
            var data = await search.SearchImagesAsync(query);
 | 
			
		||||
 | 
			
		||||
            if (data is null or { Entries: null or { Count: 0 } })
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.no_search_results);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            var embeds = new List<IEmbedBuilder>(4);
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
            IEmbedBuilder CreateEmbed(IImageSearchResultEntry entry)
 | 
			
		||||
            {
 | 
			
		||||
                return _eb.Create(ctx)
 | 
			
		||||
                          .WithOkColor()
 | 
			
		||||
                          .WithAuthor(ctx.User)
 | 
			
		||||
                          .WithTitle(query)
 | 
			
		||||
                          .WithUrl("https://google.com")
 | 
			
		||||
                          .WithImageUrl(entry.Link);
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            embeds.Add(CreateEmbed(data.Entries.First())
 | 
			
		||||
                .WithFooter(
 | 
			
		||||
                    GetText(strs.results_in(data.Info.TotalResults, data.Info.SearchTime)),
 | 
			
		||||
                    "https://i.imgur.com/G46fm8J.png"));
 | 
			
		||||
 | 
			
		||||
            var random = data.Entries.Skip(1)
 | 
			
		||||
                             .Shuffle()
 | 
			
		||||
                             .Take(3)
 | 
			
		||||
                             .ToArray();
 | 
			
		||||
 | 
			
		||||
            foreach (var entry in random)
 | 
			
		||||
            {
 | 
			
		||||
                embeds.Add(CreateEmbed(entry));
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await ctx.Channel.EmbedAsync(null, embeds: embeds);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task AddYoutubeUrlToCacheAsync(string query, string url)
 | 
			
		||||
        {
 | 
			
		||||
            var db = _redis.GetDatabase();
 | 
			
		||||
            await db.StringSetAsync($"search:youtube:{query}", url, expiry: 1.Hours());
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        private async Task<VideoInfo?> GetYoutubeUrlFromCacheAsync(string query)
 | 
			
		||||
        {
 | 
			
		||||
            var db = _redis.GetDatabase();
 | 
			
		||||
            var url = await db.StringGetAsync($"search:youtube:{query}");
 | 
			
		||||
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(url))
 | 
			
		||||
                return null;
 | 
			
		||||
 | 
			
		||||
            return new VideoInfo()
 | 
			
		||||
            {
 | 
			
		||||
                Url = url
 | 
			
		||||
            };
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        [Cmd]
 | 
			
		||||
        public async partial Task Youtube([Leftover] string? query = null)
 | 
			
		||||
        {
 | 
			
		||||
            query = query?.Trim();
 | 
			
		||||
 | 
			
		||||
            if (string.IsNullOrWhiteSpace(query))
 | 
			
		||||
            {
 | 
			
		||||
                await ErrorLocalizedAsync(strs.specify_search_params);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            _ = ctx.Channel.TriggerTypingAsync();
 | 
			
		||||
 | 
			
		||||
            var maybeResult = await GetYoutubeUrlFromCacheAsync(query)
 | 
			
		||||
                              ?? await _searchFactory.GetYoutubeSearchService().SearchAsync(query);
 | 
			
		||||
            if (maybeResult is not {} result || result is {Url: null})
 | 
			
		||||
            {
 | 
			
		||||
                await ReplyErrorLocalizedAsync(strs.no_results);
 | 
			
		||||
                return;
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            await AddYoutubeUrlToCacheAsync(query, result.Url);
 | 
			
		||||
            await ctx.Channel.SendMessageAsync(result.Url);
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
//     [Cmd]
 | 
			
		||||
//     public async partial Task DuckDuckGo([Leftover] string query = null)
 | 
			
		||||
//     {
 | 
			
		||||
//         query = query?.Trim();
 | 
			
		||||
//         if (!await ValidateQuery(query))
 | 
			
		||||
//             return;
 | 
			
		||||
//
 | 
			
		||||
//         _ = ctx.Channel.TriggerTypingAsync();
 | 
			
		||||
//
 | 
			
		||||
//         var data = await _service.DuckDuckGoSearchAsync(query);
 | 
			
		||||
//         if (data is null)
 | 
			
		||||
//         {
 | 
			
		||||
//             await ReplyErrorLocalizedAsync(strs.no_results);
 | 
			
		||||
//             return;
 | 
			
		||||
//         }
 | 
			
		||||
//
 | 
			
		||||
//         var desc = data.Results.Take(5)
 | 
			
		||||
//                        .Select(res => $@"[**{res.Title}**]({res.Link})
 | 
			
		||||
// {res.Text.TrimTo(380 - res.Title.Length - res.Link.Length)}");
 | 
			
		||||
//
 | 
			
		||||
//         var descStr = string.Join("\n\n", desc);
 | 
			
		||||
//
 | 
			
		||||
//         var embed = _eb.Create()
 | 
			
		||||
//                        .WithAuthor(ctx.User.ToString(),
 | 
			
		||||
//                            "https://upload.wikimedia.org/wikipedia/en/9/90/The_DuckDuckGo_Duck.png")
 | 
			
		||||
//                        .WithDescription($"{GetText(strs.search_for)} **{query}**\n\n" + descStr)
 | 
			
		||||
//                        .WithOkColor();
 | 
			
		||||
//
 | 
			
		||||
//         await ctx.Channel.EmbedAsync(embed);
 | 
			
		||||
//     }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,9 @@
 | 
			
		||||
using MorseCode.ITask;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public abstract class SearchServiceBase : ISearchService
 | 
			
		||||
{
 | 
			
		||||
    public abstract ITask<ISearchResult?> SearchAsync(string query);
 | 
			
		||||
    public abstract ITask<IImageSearchResult?> SearchImagesAsync(string query);
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,28 @@
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public sealed class SearxImageSearchResult : IImageSearchResult
 | 
			
		||||
{
 | 
			
		||||
    public string SearchTime { get; set; } = null!;
 | 
			
		||||
 | 
			
		||||
    public ISearchResultInformation Info
 | 
			
		||||
        => new SearxSearchResultInformation()
 | 
			
		||||
        {
 | 
			
		||||
            SearchTime = SearchTime,
 | 
			
		||||
            TotalResults = NumberOfResults.ToString("N", CultureInfo.InvariantCulture)
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
    public IReadOnlyCollection<IImageSearchResultEntry> Entries
 | 
			
		||||
        => Results;
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("results")]
 | 
			
		||||
    public List<SearxImageSearchResultEntry> Results { get; set; } = new List<SearxImageSearchResultEntry>();
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("query")]
 | 
			
		||||
    public string Query { get; set; } = null!;
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("number_of_results")]
 | 
			
		||||
    public double NumberOfResults { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,14 @@
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public sealed class SearxImageSearchResultEntry : IImageSearchResultEntry
 | 
			
		||||
{
 | 
			
		||||
    public string Link
 | 
			
		||||
        => ImageSource.StartsWith("//")
 | 
			
		||||
            ? "https:" + ImageSource
 | 
			
		||||
            : ImageSource;
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("img_src")]
 | 
			
		||||
    public string ImageSource { get; set; } = string.Empty;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										30
									
								
								src/NadekoBot/Modules/Searches/Search/Searx/SearxInfobox.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/NadekoBot/Modules/Searches/Search/Searx/SearxInfobox.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
// using System.Text.Json.Serialization;
 | 
			
		||||
//
 | 
			
		||||
// namespace NadekoBot.Modules.Searches;
 | 
			
		||||
//
 | 
			
		||||
// public sealed class SearxInfobox
 | 
			
		||||
// {
 | 
			
		||||
//     [JsonPropertyName("infobox")]
 | 
			
		||||
//     public string Infobox { get; set; }
 | 
			
		||||
//
 | 
			
		||||
//     [JsonPropertyName("id")]
 | 
			
		||||
//     public string Id { get; set; }
 | 
			
		||||
//
 | 
			
		||||
//     [JsonPropertyName("content")]
 | 
			
		||||
//     public string Content { get; set; }
 | 
			
		||||
//
 | 
			
		||||
//     [JsonPropertyName("img_src")]
 | 
			
		||||
//     public string ImgSrc { get; set; }
 | 
			
		||||
//
 | 
			
		||||
//     [JsonPropertyName("urls")]
 | 
			
		||||
//     public List<SearxUrlData> Urls { get; } = new List<SearxUrlData>();
 | 
			
		||||
//
 | 
			
		||||
//     [JsonPropertyName("engine")]
 | 
			
		||||
//     public string Engine { get; set; }
 | 
			
		||||
//
 | 
			
		||||
//     [JsonPropertyName("engines")]
 | 
			
		||||
//     public List<string> Engines { get; } = new List<string>();
 | 
			
		||||
//
 | 
			
		||||
//     [JsonPropertyName("attributes")]
 | 
			
		||||
//     public List<SearxSearchAttribute> Attributes { get; } = new List<SearxSearchAttribute>();
 | 
			
		||||
// }
 | 
			
		||||
@@ -0,0 +1,15 @@
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public sealed class SearxSearchAttribute
 | 
			
		||||
{
 | 
			
		||||
    [JsonPropertyName("label")]
 | 
			
		||||
    public string? Label { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("value")]
 | 
			
		||||
    public string? Value { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("entity")]
 | 
			
		||||
    public string? Entity { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,47 @@
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public sealed class SearxSearchResult : ISearchResult
 | 
			
		||||
{
 | 
			
		||||
    [JsonPropertyName("query")]
 | 
			
		||||
    public string Query { get; set; } = null!;
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("number_of_results")]
 | 
			
		||||
    public double NumberOfResults { get; set; }
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("results")]
 | 
			
		||||
    public List<SearxSearchResultEntry> Results { get; set; } = new List<SearxSearchResultEntry>();
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("answers")]
 | 
			
		||||
    public List<string> Answers { get; set; } = new List<string>();
 | 
			
		||||
    //
 | 
			
		||||
    // [JsonPropertyName("corrections")]
 | 
			
		||||
    // public List<object> Corrections { get; } = new List<object>();
 | 
			
		||||
 | 
			
		||||
    // [JsonPropertyName("infoboxes")]
 | 
			
		||||
    // public List<InfoboxModel> Infoboxes { get; } = new List<InfoboxModel>();
 | 
			
		||||
    //
 | 
			
		||||
    // [JsonPropertyName("suggestions")]
 | 
			
		||||
    // public List<string> Suggestions { get; } = new List<string>();
 | 
			
		||||
 | 
			
		||||
    // [JsonPropertyName("unresponsive_engines")]
 | 
			
		||||
    // public List<object> UnresponsiveEngines { get; } = new List<object>();
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
    public string SearchTime { get; set; } = null!;
 | 
			
		||||
 | 
			
		||||
    public IReadOnlyCollection<ISearchResultEntry> Entries
 | 
			
		||||
        => Results;
 | 
			
		||||
 | 
			
		||||
    public ISearchResultInformation Info
 | 
			
		||||
        => new SearxSearchResultInformation()
 | 
			
		||||
        {
 | 
			
		||||
            SearchTime = SearchTime,
 | 
			
		||||
            TotalResults = NumberOfResults.ToString("N", CultureInfo.InvariantCulture)
 | 
			
		||||
        };
 | 
			
		||||
 | 
			
		||||
    public string? Answer
 | 
			
		||||
        => Answers.FirstOrDefault();
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,51 @@
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public sealed class SearxSearchResultEntry : ISearchResultEntry
 | 
			
		||||
{
 | 
			
		||||
    public string DisplayUrl
 | 
			
		||||
        => Url;
 | 
			
		||||
 | 
			
		||||
    public string Description
 | 
			
		||||
        => Content.TrimTo(768)!;
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("url")]
 | 
			
		||||
    public string Url { get; set; } = null!;
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("title")]
 | 
			
		||||
    public string Title { get; set; } = null!;
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("content")]
 | 
			
		||||
    public string? Content { get; set; }
 | 
			
		||||
 | 
			
		||||
    // [JsonPropertyName("engine")]
 | 
			
		||||
    // public string Engine { get; set; }
 | 
			
		||||
    //
 | 
			
		||||
    // [JsonPropertyName("parsed_url")]
 | 
			
		||||
    // public List<string> ParsedUrl { get; } = new List<string>();
 | 
			
		||||
    //
 | 
			
		||||
    // [JsonPropertyName("template")]
 | 
			
		||||
    // public string Template { get; set; }
 | 
			
		||||
    //
 | 
			
		||||
    // [JsonPropertyName("engines")]
 | 
			
		||||
    // public List<string> Engines { get; } = new List<string>();
 | 
			
		||||
    //
 | 
			
		||||
    // [JsonPropertyName("positions")]
 | 
			
		||||
    // public List<int> Positions { get; } = new List<int>();
 | 
			
		||||
    //
 | 
			
		||||
    // [JsonPropertyName("score")]
 | 
			
		||||
    // public double Score { get; set; }
 | 
			
		||||
    //
 | 
			
		||||
    // [JsonPropertyName("category")]
 | 
			
		||||
    // public string Category { get; set; }
 | 
			
		||||
    //
 | 
			
		||||
    // [JsonPropertyName("pretty_url")]
 | 
			
		||||
    // public string PrettyUrl { get; set; }
 | 
			
		||||
    //
 | 
			
		||||
    // [JsonPropertyName("open_group")]
 | 
			
		||||
    // public bool OpenGroup { get; set; }
 | 
			
		||||
    //
 | 
			
		||||
    // [JsonPropertyName("close_group")]
 | 
			
		||||
    // public bool? CloseGroup { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,7 @@
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public sealed class SearxSearchResultInformation : ISearchResultInformation
 | 
			
		||||
{
 | 
			
		||||
    public string TotalResults { get; init; } = string.Empty;
 | 
			
		||||
    public string SearchTime { get; init; } = string.Empty;
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,76 @@
 | 
			
		||||
using MorseCode.ITask;
 | 
			
		||||
using System.Diagnostics;
 | 
			
		||||
using System.Globalization;
 | 
			
		||||
using System.Text.Json;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public sealed class SearxSearchService : SearchServiceBase, INService
 | 
			
		||||
{
 | 
			
		||||
    private readonly IHttpClientFactory _http;
 | 
			
		||||
    private readonly SearchesConfigService _scs;
 | 
			
		||||
    
 | 
			
		||||
    private static readonly Random _rng = new NadekoRandom();
 | 
			
		||||
 | 
			
		||||
    public SearxSearchService(IHttpClientFactory http, SearchesConfigService scs)
 | 
			
		||||
        => (_http, _scs) = (http, scs);
 | 
			
		||||
 | 
			
		||||
    private string GetRandomInstance()
 | 
			
		||||
    {
 | 
			
		||||
        var instances = _scs.Data.SearxInstances;
 | 
			
		||||
 | 
			
		||||
        if (instances is null or { Count: 0 })
 | 
			
		||||
            throw new InvalidOperationException("No searx instances specified in searches.yml");
 | 
			
		||||
 | 
			
		||||
        return instances[_rng.Next(0, instances.Count)];
 | 
			
		||||
    }
 | 
			
		||||
    
 | 
			
		||||
    public override async ITask<SearxSearchResult> SearchAsync(string query)
 | 
			
		||||
    {
 | 
			
		||||
        ArgumentNullException.ThrowIfNull(query);
 | 
			
		||||
 | 
			
		||||
        var instanceUrl = GetRandomInstance();
 | 
			
		||||
        
 | 
			
		||||
        Log.Information("Using {Instance} instance for web search...", instanceUrl);
 | 
			
		||||
        var sw = Stopwatch.StartNew();
 | 
			
		||||
        using var http = _http.CreateClient();
 | 
			
		||||
        await using var res = await http.GetStreamAsync($"{instanceUrl}"
 | 
			
		||||
                                                        + $"?q={Uri.EscapeDataString(query)}"
 | 
			
		||||
                                                        + $"&format=json"
 | 
			
		||||
                                                        + $"&strict=2");
 | 
			
		||||
 | 
			
		||||
        sw.Stop();
 | 
			
		||||
        var dat = await JsonSerializer.DeserializeAsync<SearxSearchResult>(res);
 | 
			
		||||
 | 
			
		||||
        if (dat is null)
 | 
			
		||||
            return new SearxSearchResult();
 | 
			
		||||
        
 | 
			
		||||
        dat.SearchTime = sw.Elapsed.TotalSeconds.ToString("N2", CultureInfo.InvariantCulture); 
 | 
			
		||||
        return dat;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public override async ITask<SearxImageSearchResult> SearchImagesAsync(string query)
 | 
			
		||||
    {
 | 
			
		||||
        ArgumentNullException.ThrowIfNull(query);
 | 
			
		||||
        
 | 
			
		||||
        var instanceUrl = GetRandomInstance();
 | 
			
		||||
        
 | 
			
		||||
        Log.Information("Using {Instance} instance for img search...", instanceUrl);
 | 
			
		||||
        var sw = Stopwatch.StartNew();
 | 
			
		||||
        using var http = _http.CreateClient();
 | 
			
		||||
        await using var res = await http.GetStreamAsync($"{instanceUrl}"
 | 
			
		||||
                                                        + $"?q={Uri.EscapeDataString(query)}"
 | 
			
		||||
                                                        + $"&format=json"
 | 
			
		||||
                                                        + $"&category_images=on"
 | 
			
		||||
                                                        + $"&strict=2");
 | 
			
		||||
 | 
			
		||||
        sw.Stop();
 | 
			
		||||
        var dat = await JsonSerializer.DeserializeAsync<SearxImageSearchResult>(res);
 | 
			
		||||
 | 
			
		||||
        if (dat is null)
 | 
			
		||||
            return new SearxImageSearchResult();
 | 
			
		||||
        
 | 
			
		||||
        dat.SearchTime = sw.Elapsed.TotalSeconds.ToString("N2", CultureInfo.InvariantCulture); 
 | 
			
		||||
        return dat;
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										15
									
								
								src/NadekoBot/Modules/Searches/Search/Searx/SearxUrlData.cs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								src/NadekoBot/Modules/Searches/Search/Searx/SearxUrlData.cs
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public sealed class SearxUrlData
 | 
			
		||||
{
 | 
			
		||||
    [JsonPropertyName("title")]
 | 
			
		||||
    public string Title { get; set; } = null!;
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("url")]
 | 
			
		||||
    public string Url { get; set; } = null!;
 | 
			
		||||
 | 
			
		||||
    [JsonPropertyName("official")]
 | 
			
		||||
    public bool? Official { get; set; }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,6 @@
 | 
			
		||||
namespace NadekoBot.Modules.Searches.Youtube;
 | 
			
		||||
 | 
			
		||||
public interface IYoutubeSearchService
 | 
			
		||||
{
 | 
			
		||||
    Task<VideoInfo?> SearchAsync(string query);
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,9 @@
 | 
			
		||||
using System.Text.Json.Serialization;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public sealed class InvidiousSearchResponse
 | 
			
		||||
{
 | 
			
		||||
    [JsonPropertyName("videoId")]
 | 
			
		||||
    public string VideoId { get; set; } = null!;
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,46 @@
 | 
			
		||||
using NadekoBot.Modules.Searches.Youtube;
 | 
			
		||||
using System.Net.Http.Json;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public sealed class InvidiousYtSearchService : IYoutubeSearchService, INService
 | 
			
		||||
{
 | 
			
		||||
    private readonly IHttpClientFactory _http;
 | 
			
		||||
    private readonly SearchesConfigService _scs;
 | 
			
		||||
    private readonly NadekoRandom _rng;
 | 
			
		||||
 | 
			
		||||
    public InvidiousYtSearchService(
 | 
			
		||||
        IHttpClientFactory http,
 | 
			
		||||
        SearchesConfigService scs)
 | 
			
		||||
    {
 | 
			
		||||
        _http = http;
 | 
			
		||||
        _scs = scs;
 | 
			
		||||
        _rng = new();
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<VideoInfo?> SearchAsync(string query)
 | 
			
		||||
    {
 | 
			
		||||
        ArgumentNullException.ThrowIfNull(query);
 | 
			
		||||
 | 
			
		||||
        var instances = _scs.Data.InvidiousInstances;
 | 
			
		||||
        if (instances is null or { Count: 0 })
 | 
			
		||||
        {
 | 
			
		||||
            Log.Warning("Attempted to use Invidious as the .youtube provider but there are no 'invidiousInstances' "
 | 
			
		||||
                        + "specified in `data/searches.yml`");
 | 
			
		||||
            return null;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var instance = instances[_rng.Next(0, instances.Count)];
 | 
			
		||||
 | 
			
		||||
        using var http = _http.CreateClient();
 | 
			
		||||
        var res = await http.GetFromJsonAsync<List<InvidiousSearchResponse>>(
 | 
			
		||||
            $"{instance}/api/v1/search"
 | 
			
		||||
            + $"?q={query}"
 | 
			
		||||
            + $"&type=video");
 | 
			
		||||
 | 
			
		||||
        if (res is null or {Count: 0})
 | 
			
		||||
            return null;
 | 
			
		||||
 | 
			
		||||
        return new VideoInfo(res[0].VideoId);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,9 @@
 | 
			
		||||
namespace NadekoBot.Modules.Searches.Youtube;
 | 
			
		||||
 | 
			
		||||
public readonly struct VideoInfo
 | 
			
		||||
{
 | 
			
		||||
    public VideoInfo(string videoId)
 | 
			
		||||
        => Url = $"https://youtube.com/watch?v={videoId}";
 | 
			
		||||
 | 
			
		||||
    public string Url { get; init; }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,26 @@
 | 
			
		||||
namespace NadekoBot.Modules.Searches.Youtube;
 | 
			
		||||
 | 
			
		||||
public sealed class YoutubeDataApiSearchService : IYoutubeSearchService, INService
 | 
			
		||||
{
 | 
			
		||||
    private readonly IGoogleApiService _gapi;
 | 
			
		||||
 | 
			
		||||
    public YoutubeDataApiSearchService(IGoogleApiService gapi)
 | 
			
		||||
    {
 | 
			
		||||
        _gapi = gapi;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<VideoInfo?> SearchAsync(string query)
 | 
			
		||||
    {
 | 
			
		||||
        ArgumentNullException.ThrowIfNull(query);
 | 
			
		||||
 | 
			
		||||
        var results = await _gapi.GetVideoLinksByKeywordAsync(query);
 | 
			
		||||
        var first = results.FirstOrDefault();
 | 
			
		||||
        if (first is null)
 | 
			
		||||
            return null;
 | 
			
		||||
 | 
			
		||||
        return new()
 | 
			
		||||
        {
 | 
			
		||||
            Url = first
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,7 @@
 | 
			
		||||
namespace NadekoBot.Modules.Searches.Youtube;
 | 
			
		||||
 | 
			
		||||
public sealed class YtdlYoutubeSearchService : YoutubedlxServiceBase, INService
 | 
			
		||||
{
 | 
			
		||||
    public override async Task<VideoInfo?> SearchAsync(string query)
 | 
			
		||||
        => await InternalGetInfoAsync(query, false);
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,7 @@
 | 
			
		||||
namespace NadekoBot.Modules.Searches.Youtube;
 | 
			
		||||
 | 
			
		||||
public sealed class YtdlpYoutubeSearchService : YoutubedlxServiceBase, INService
 | 
			
		||||
{
 | 
			
		||||
    public override async Task<VideoInfo?> SearchAsync(string query)
 | 
			
		||||
        => await InternalGetInfoAsync(query, true);
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,34 @@
 | 
			
		||||
namespace NadekoBot.Modules.Searches.Youtube;
 | 
			
		||||
 | 
			
		||||
public abstract class YoutubedlxServiceBase : IYoutubeSearchService
 | 
			
		||||
{
 | 
			
		||||
    private YtdlOperation CreateYtdlOp(bool isYtDlp)
 | 
			
		||||
        => new YtdlOperation("-4 "
 | 
			
		||||
                             + "--geo-bypass "
 | 
			
		||||
                             + "--encoding UTF8 "
 | 
			
		||||
                             + "--get-id "
 | 
			
		||||
                             + "--no-check-certificate "
 | 
			
		||||
                             + "--default-search "
 | 
			
		||||
                             + "\"ytsearch:\" -- \"{0}\"",
 | 
			
		||||
            isYtDlp: isYtDlp);
 | 
			
		||||
 | 
			
		||||
    protected async Task<VideoInfo?> InternalGetInfoAsync(string query, bool isYtDlp)
 | 
			
		||||
    {
 | 
			
		||||
        var op = CreateYtdlOp(isYtDlp);
 | 
			
		||||
        var data = await op.GetDataAsync(query);
 | 
			
		||||
        var items = data?.Split('\n');
 | 
			
		||||
        if (items is null or { Length: 0 })
 | 
			
		||||
            return null;
 | 
			
		||||
 | 
			
		||||
        var id = items.FirstOrDefault(x => x.Length is > 5 and < 15);
 | 
			
		||||
        if (id is null)
 | 
			
		||||
            return null;
 | 
			
		||||
 | 
			
		||||
        return new VideoInfo()
 | 
			
		||||
        {
 | 
			
		||||
            Url = $"https://youtube.com/watch?v={id}"
 | 
			
		||||
        };
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public abstract Task<VideoInfo?> SearchAsync(string query);
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,4 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using AngleSharp;
 | 
			
		||||
using AngleSharp.Html.Dom;
 | 
			
		||||
using Microsoft.Extensions.Caching.Memory;
 | 
			
		||||
using NadekoBot.Modules.Administration.Services;
 | 
			
		||||
using NadekoBot.Modules.Searches.Common;
 | 
			
		||||
@@ -11,9 +9,9 @@ using SixLabors.ImageSharp;
 | 
			
		||||
using SixLabors.ImageSharp.Drawing.Processing;
 | 
			
		||||
using SixLabors.ImageSharp.PixelFormats;
 | 
			
		||||
using SixLabors.ImageSharp.Processing;
 | 
			
		||||
using System.Diagnostics.CodeAnalysis;
 | 
			
		||||
using System.Net;
 | 
			
		||||
using Color = SixLabors.ImageSharp.Color;
 | 
			
		||||
using Configuration = AngleSharp.Configuration;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
@@ -92,7 +90,7 @@ public partial class Searches : NadekoModule<SearchesService>
 | 
			
		||||
                .AddField("🌇 " + Format.Bold(GetText(strs.sunset)), $"{sunset:HH:mm} {timezone}", true)
 | 
			
		||||
                .WithOkColor()
 | 
			
		||||
                .WithFooter("Powered by openweathermap.org",
 | 
			
		||||
                    $"http://openweathermap.org/img/w/{data.Weather[0].Icon}.png");
 | 
			
		||||
                    $"https://openweathermap.org/img/w/{data.Weather[0].Icon}.png");
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await ctx.Channel.EmbedAsync(embed);
 | 
			
		||||
@@ -146,22 +144,6 @@ public partial class Searches : NadekoModule<SearchesService>
 | 
			
		||||
        await ctx.Channel.SendMessageAsync(embed: eb.Build());
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    public async partial Task Youtube([Leftover] string query = null)
 | 
			
		||||
    {
 | 
			
		||||
        if (!await ValidateQuery(query))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        var result = (await _google.GetVideoLinksByKeywordAsync(query)).FirstOrDefault();
 | 
			
		||||
        if (string.IsNullOrWhiteSpace(result))
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.no_results);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        await ctx.Channel.SendMessageAsync(result);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    public async partial Task Movie([Leftover] string query = null)
 | 
			
		||||
    {
 | 
			
		||||
@@ -180,7 +162,7 @@ public partial class Searches : NadekoModule<SearchesService>
 | 
			
		||||
        await ctx.Channel.EmbedAsync(_eb.Create()
 | 
			
		||||
                                        .WithOkColor()
 | 
			
		||||
                                        .WithTitle(movie.Title)
 | 
			
		||||
                                        .WithUrl($"http://www.imdb.com/title/{movie.ImdbId}/")
 | 
			
		||||
                                        .WithUrl($"https://www.imdb.com/title/{movie.ImdbId}/")
 | 
			
		||||
                                        .WithDescription(movie.Plot.TrimTo(1000))
 | 
			
		||||
                                        .AddField("Rating", movie.ImdbRating, true)
 | 
			
		||||
                                        .AddField("Genre", movie.Genre, true)
 | 
			
		||||
@@ -210,67 +192,13 @@ public partial class Searches : NadekoModule<SearchesService>
 | 
			
		||||
        return ctx.Channel.EmbedAsync(_eb.Create().WithOkColor().WithImageUrl(url));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    public async partial Task Image([Leftover] string query = null)
 | 
			
		||||
    {
 | 
			
		||||
        var oterms = query?.Trim();
 | 
			
		||||
        if (!await ValidateQuery(query))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        query = WebUtility.UrlEncode(oterms)?.Replace(' ', '+');
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var res = await _google.GetImageAsync(oterms);
 | 
			
		||||
            var embed = _eb.Create()
 | 
			
		||||
                           .WithOkColor()
 | 
			
		||||
                           .WithAuthor(GetText(strs.image_search_for) + " " + oterms.TrimTo(50),
 | 
			
		||||
                               "http://i.imgur.com/G46fm8J.png",
 | 
			
		||||
                               $"https://www.google.rs/search?q={query}&source=lnms&tbm=isch")
 | 
			
		||||
                           .WithDescription(res.Link)
 | 
			
		||||
                           .WithImageUrl(res.Link)
 | 
			
		||||
                           .WithTitle(ctx.User.ToString());
 | 
			
		||||
            await ctx.Channel.EmbedAsync(embed);
 | 
			
		||||
        }
 | 
			
		||||
        catch
 | 
			
		||||
        {
 | 
			
		||||
            Log.Warning("Falling back to Imgur");
 | 
			
		||||
 | 
			
		||||
            var fullQueryLink = $"http://imgur.com/search?q={query}";
 | 
			
		||||
            var config = Configuration.Default.WithDefaultLoader();
 | 
			
		||||
            using var document = await BrowsingContext.New(config).OpenAsync(fullQueryLink);
 | 
			
		||||
            var elems = document.QuerySelectorAll("a.image-list-link").ToList();
 | 
			
		||||
 | 
			
		||||
            if (!elems.Any())
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var img =
 | 
			
		||||
                elems.ElementAtOrDefault(new NadekoRandom().Next(0, elems.Count))?.Children?.FirstOrDefault() as
 | 
			
		||||
                    IHtmlImageElement;
 | 
			
		||||
 | 
			
		||||
            if (img?.Source is null)
 | 
			
		||||
                return;
 | 
			
		||||
 | 
			
		||||
            var source = img.Source.Replace("b.", ".", StringComparison.InvariantCulture);
 | 
			
		||||
 | 
			
		||||
            var embed = _eb.Create()
 | 
			
		||||
                           .WithOkColor()
 | 
			
		||||
                           .WithAuthor(GetText(strs.image_search_for) + " " + oterms.TrimTo(50),
 | 
			
		||||
                               "http://s.imgur.com/images/logo-1200-630.jpg?",
 | 
			
		||||
                               fullQueryLink)
 | 
			
		||||
                           .WithDescription(source)
 | 
			
		||||
                           .WithImageUrl(source)
 | 
			
		||||
                           .WithTitle(ctx.User.ToString());
 | 
			
		||||
            await ctx.Channel.EmbedAsync(embed);
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    public async partial Task Lmgtfy([Leftover] string ffs = null)
 | 
			
		||||
    {
 | 
			
		||||
        if (!await ValidateQuery(ffs))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        var shortenedUrl = await _google.ShortenUrl($"http://lmgtfy.com/?q={Uri.EscapeDataString(ffs)}");
 | 
			
		||||
        var shortenedUrl = await _google.ShortenUrl($"https://lmgtfy.com/?q={Uri.EscapeDataString(ffs)}");
 | 
			
		||||
        await SendConfirmAsync($"<{shortenedUrl}>");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -317,69 +245,6 @@ public partial class Searches : NadekoModule<SearchesService>
 | 
			
		||||
                                        .AddField(GetText(strs.short_url), $"<{shortLink}>"));
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    public async partial Task Google([Leftover] string query = null)
 | 
			
		||||
    {
 | 
			
		||||
        query = query?.Trim();
 | 
			
		||||
        if (!await ValidateQuery(query))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        _ = ctx.Channel.TriggerTypingAsync();
 | 
			
		||||
 | 
			
		||||
        var data = await _service.GoogleSearchAsync(query);
 | 
			
		||||
        if (data is null)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.no_results);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var desc = data.Results.Take(5)
 | 
			
		||||
                       .Select(res => $@"[**{res.Title}**]({res.Link})
 | 
			
		||||
{res.Text.TrimTo(400 - res.Title.Length - res.Link.Length)}");
 | 
			
		||||
 | 
			
		||||
        var descStr = string.Join("\n\n", desc);
 | 
			
		||||
 | 
			
		||||
        var embed = _eb.Create()
 | 
			
		||||
                       .WithAuthor(ctx.User.ToString(), "http://i.imgur.com/G46fm8J.png")
 | 
			
		||||
                       .WithTitle(ctx.User.ToString())
 | 
			
		||||
                       .WithFooter(data.TotalResults)
 | 
			
		||||
                       .WithDescription($"{GetText(strs.search_for)} **{query}**\n\n" + descStr)
 | 
			
		||||
                       .WithOkColor();
 | 
			
		||||
 | 
			
		||||
        await ctx.Channel.EmbedAsync(embed);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    public async partial Task DuckDuckGo([Leftover] string query = null)
 | 
			
		||||
    {
 | 
			
		||||
        query = query?.Trim();
 | 
			
		||||
        if (!await ValidateQuery(query))
 | 
			
		||||
            return;
 | 
			
		||||
 | 
			
		||||
        _ = ctx.Channel.TriggerTypingAsync();
 | 
			
		||||
 | 
			
		||||
        var data = await _service.DuckDuckGoSearchAsync(query);
 | 
			
		||||
        if (data is null)
 | 
			
		||||
        {
 | 
			
		||||
            await ReplyErrorLocalizedAsync(strs.no_results);
 | 
			
		||||
            return;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        var desc = data.Results.Take(5)
 | 
			
		||||
                       .Select(res => $@"[**{res.Title}**]({res.Link})
 | 
			
		||||
{res.Text.TrimTo(380 - res.Title.Length - res.Link.Length)}");
 | 
			
		||||
 | 
			
		||||
        var descStr = string.Join("\n\n", desc);
 | 
			
		||||
 | 
			
		||||
        var embed = _eb.Create()
 | 
			
		||||
                       .WithAuthor(ctx.User.ToString(),
 | 
			
		||||
                           "https://upload.wikimedia.org/wikipedia/en/9/90/The_DuckDuckGo_Duck.png")
 | 
			
		||||
                       .WithDescription($"{GetText(strs.search_for)} **{query}**\n\n" + descStr)
 | 
			
		||||
                       .WithOkColor();
 | 
			
		||||
 | 
			
		||||
        await ctx.Channel.EmbedAsync(embed);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    [Cmd]
 | 
			
		||||
    public async partial Task MagicTheGathering([Leftover] string search)
 | 
			
		||||
    {
 | 
			
		||||
@@ -446,7 +311,7 @@ public partial class Searches : NadekoModule<SearchesService>
 | 
			
		||||
        using (var http = _httpFactory.CreateClient())
 | 
			
		||||
        {
 | 
			
		||||
            var res = await http.GetStringAsync(
 | 
			
		||||
                $"http://api.urbandictionary.com/v0/define?term={Uri.EscapeDataString(query)}");
 | 
			
		||||
                $"https://api.urbandictionary.com/v0/define?term={Uri.EscapeDataString(query)}");
 | 
			
		||||
            try
 | 
			
		||||
            {
 | 
			
		||||
                var items = JsonConvert.DeserializeObject<UrbanResponse>(res).List;
 | 
			
		||||
@@ -732,7 +597,7 @@ public partial class Searches : NadekoModule<SearchesService>
 | 
			
		||||
        await ctx.Channel.SendMessageAsync($"https://store.steampowered.com/app/{appId}");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private async Task<bool> ValidateQuery(string query)
 | 
			
		||||
    private async Task<bool> ValidateQuery([MaybeNullWhen(false)] string query)
 | 
			
		||||
    {
 | 
			
		||||
        if (!string.IsNullOrWhiteSpace(query))
 | 
			
		||||
            return true;
 | 
			
		||||
 
 | 
			
		||||
@@ -26,15 +26,6 @@ public class SearchesService : INService
 | 
			
		||||
        Birds
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private static readonly HtmlParser _googleParser = new(new()
 | 
			
		||||
    {
 | 
			
		||||
        IsScripting = false,
 | 
			
		||||
        IsEmbedded = false,
 | 
			
		||||
        IsSupportingProcessingInstructions = false,
 | 
			
		||||
        IsKeepingSourceReferences = false,
 | 
			
		||||
        IsNotSupportingFrames = true
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    public List<WoWJoke> WowJokes { get; } = new();
 | 
			
		||||
    public List<MagicItem> MagicItems { get; } = new();
 | 
			
		||||
    private readonly IHttpClientFactory _httpFactory;
 | 
			
		||||
@@ -161,7 +152,7 @@ public class SearchesService : INService
 | 
			
		||||
        using var http = _httpFactory.CreateClient();
 | 
			
		||||
        try
 | 
			
		||||
        {
 | 
			
		||||
            var data = await http.GetStringAsync("http://api.openweathermap.org/data/2.5/weather?"
 | 
			
		||||
            var data = await http.GetStringAsync("https://api.openweathermap.org/data/2.5/weather?"
 | 
			
		||||
                                                 + $"q={query}&"
 | 
			
		||||
                                                 + "appid=42cd627dd60debf25a5739e50a217d74&"
 | 
			
		||||
                                                 + "units=metric");
 | 
			
		||||
@@ -440,22 +431,6 @@ public class SearchesService : INService
 | 
			
		||||
    public async Task<int> GetSteamAppIdByName(string query)
 | 
			
		||||
    {
 | 
			
		||||
        const string steamGameIdsKey = "steam_names_to_appid";
 | 
			
		||||
        // var exists = await db.KeyExistsAsync(steamGameIdsKey);
 | 
			
		||||
 | 
			
		||||
        // if we didn't get steam name to id map already, get it
 | 
			
		||||
        //if (!exists)
 | 
			
		||||
        //{
 | 
			
		||||
        //    using (var http = _httpFactory.CreateClient())
 | 
			
		||||
        //    {
 | 
			
		||||
        //        // https://api.steampowered.com/ISteamApps/GetAppList/v2/
 | 
			
		||||
        //        var gamesStr = await http.GetStringAsync("https://api.steampowered.com/ISteamApps/GetAppList/v2/");
 | 
			
		||||
        //        var apps = JsonConvert.DeserializeAnonymousType(gamesStr, new { applist = new { apps = new List<SteamGameId>() } }).applist.apps;
 | 
			
		||||
 | 
			
		||||
        //        //await db.HashSetAsync("steam_game_ids", apps.Select(app => new HashEntry(app.Name.Trim().ToLowerInvariant(), app.AppId)).ToArray());
 | 
			
		||||
        //        await db.StringSetAsync("steam_game_ids", gamesStr, TimeSpan.FromHours(24));
 | 
			
		||||
        //        //await db.KeyExpireAsync("steam_game_ids", TimeSpan.FromHours(24), CommandFlags.FireAndForget);
 | 
			
		||||
        //    }
 | 
			
		||||
        //}
 | 
			
		||||
 | 
			
		||||
        var gamesMap = await _cache.GetOrAddCachedDataAsync(steamGameIdsKey,
 | 
			
		||||
            async _ =>
 | 
			
		||||
@@ -502,150 +477,5 @@ public class SearchesService : INService
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        return gamesMap[key];
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
        //// try finding the game id
 | 
			
		||||
        //var val = db.HashGet(STEAM_GAME_IDS_KEY, query);
 | 
			
		||||
        //if (val == default)
 | 
			
		||||
        //    return -1; // not found
 | 
			
		||||
 | 
			
		||||
        //var appid = (int)val;
 | 
			
		||||
        //return appid;
 | 
			
		||||
 | 
			
		||||
        // now that we have appid, get the game info with that appid
 | 
			
		||||
        //var gameData = await _cache.GetOrAddCachedDataAsync($"steam_game:{appid}", SteamGameDataFactory, appid, TimeSpan.FromHours(12))
 | 
			
		||||
        //;
 | 
			
		||||
 | 
			
		||||
        //return gameData;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<GoogleSearchResultData> GoogleSearchAsync(string query)
 | 
			
		||||
    {
 | 
			
		||||
        query = WebUtility.UrlEncode(query)?.Replace(' ', '+');
 | 
			
		||||
 | 
			
		||||
        var fullQueryLink = $"https://www.google.ca/search?q={query}&safe=on&lr=lang_eng&hl=en&ie=utf-8&oe=utf-8";
 | 
			
		||||
 | 
			
		||||
        using var msg = new HttpRequestMessage(HttpMethod.Get, fullQueryLink);
 | 
			
		||||
        msg.Headers.Add("User-Agent",
 | 
			
		||||
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36");
 | 
			
		||||
        msg.Headers.Add("Cookie", "CONSENT=YES+shp.gws-20210601-0-RC2.en+FX+423;");
 | 
			
		||||
 | 
			
		||||
        using var http = _httpFactory.CreateClient();
 | 
			
		||||
        http.DefaultRequestHeaders.Clear();
 | 
			
		||||
 | 
			
		||||
        using var response = await http.SendAsync(msg);
 | 
			
		||||
        await using var content = await response.Content.ReadAsStreamAsync();
 | 
			
		||||
 | 
			
		||||
        using var document = await _googleParser.ParseDocumentAsync(content);
 | 
			
		||||
        var elems = document.QuerySelectorAll("div.g > div > div");
 | 
			
		||||
 | 
			
		||||
        var resultsElem = document.QuerySelectorAll("#resultStats").FirstOrDefault();
 | 
			
		||||
        var totalResults = resultsElem?.TextContent;
 | 
			
		||||
        //var time = resultsElem.Children.FirstOrDefault()?.TextContent
 | 
			
		||||
        //^ this doesn't work for some reason, <nobr> is completely missing in parsed collection
 | 
			
		||||
        if (!elems.Any())
 | 
			
		||||
            return default;
 | 
			
		||||
 | 
			
		||||
        var results = elems.Select(elem =>
 | 
			
		||||
                           {
 | 
			
		||||
                               var children = elem.Children.ToList();
 | 
			
		||||
                               if (children.Count < 2)
 | 
			
		||||
                                   return null;
 | 
			
		||||
 | 
			
		||||
                               var href = (children[0].QuerySelector("a") as IHtmlAnchorElement)?.Href;
 | 
			
		||||
                               var name = children[0].QuerySelector("h3")?.TextContent;
 | 
			
		||||
 | 
			
		||||
                               if (href is null || name is null)
 | 
			
		||||
                                   return null;
 | 
			
		||||
 | 
			
		||||
                               var txt = children[1].TextContent;
 | 
			
		||||
 | 
			
		||||
                               if (string.IsNullOrWhiteSpace(txt))
 | 
			
		||||
                                   return null;
 | 
			
		||||
 | 
			
		||||
                               return new GoogleSearchResult(name, href, txt);
 | 
			
		||||
                           })
 | 
			
		||||
                           .Where(x => x is not null)
 | 
			
		||||
                           .ToList();
 | 
			
		||||
 | 
			
		||||
        return new(results.AsReadOnly(), fullQueryLink, totalResults);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    public async Task<GoogleSearchResultData> DuckDuckGoSearchAsync(string query)
 | 
			
		||||
    {
 | 
			
		||||
        query = WebUtility.UrlEncode(query)?.Replace(' ', '+');
 | 
			
		||||
 | 
			
		||||
        var fullQueryLink = "https://html.duckduckgo.com/html";
 | 
			
		||||
 | 
			
		||||
        using var http = _httpFactory.CreateClient();
 | 
			
		||||
        http.DefaultRequestHeaders.Clear();
 | 
			
		||||
        http.DefaultRequestHeaders.Add("User-Agent",
 | 
			
		||||
            "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.77 Safari/537.36");
 | 
			
		||||
 | 
			
		||||
        using var formData = new MultipartFormDataContent();
 | 
			
		||||
        formData.Add(new StringContent(query), "q");
 | 
			
		||||
        using var response = await http.PostAsync(fullQueryLink, formData);
 | 
			
		||||
        var content = await response.Content.ReadAsStringAsync();
 | 
			
		||||
 | 
			
		||||
        using var document = await _googleParser.ParseDocumentAsync(content);
 | 
			
		||||
        var searchResults = document.QuerySelector(".results");
 | 
			
		||||
        var elems = searchResults.QuerySelectorAll(".result");
 | 
			
		||||
 | 
			
		||||
        if (!elems.Any())
 | 
			
		||||
            return default;
 | 
			
		||||
 | 
			
		||||
        var results = elems.Select(elem =>
 | 
			
		||||
                           {
 | 
			
		||||
                               if (elem.QuerySelector(".result__a") is not IHtmlAnchorElement anchor)
 | 
			
		||||
                                   return null;
 | 
			
		||||
 | 
			
		||||
                               var href = anchor.Href;
 | 
			
		||||
                               var name = anchor.TextContent;
 | 
			
		||||
 | 
			
		||||
                               if (string.IsNullOrWhiteSpace(href) || string.IsNullOrWhiteSpace(name))
 | 
			
		||||
                                   return null;
 | 
			
		||||
 | 
			
		||||
                               var txt = elem.QuerySelector(".result__snippet")?.TextContent;
 | 
			
		||||
 | 
			
		||||
                               if (string.IsNullOrWhiteSpace(txt))
 | 
			
		||||
                                   return null;
 | 
			
		||||
 | 
			
		||||
                               return new GoogleSearchResult(name, href, txt);
 | 
			
		||||
                           })
 | 
			
		||||
                           .Where(x => x is not null)
 | 
			
		||||
                           .ToList();
 | 
			
		||||
 | 
			
		||||
        return new(results.AsReadOnly(), fullQueryLink, "0");
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    //private async Task<SteamGameData> SteamGameDataFactory(int appid)
 | 
			
		||||
    //{
 | 
			
		||||
    //    using (var http = _httpFactory.CreateClient())
 | 
			
		||||
    //    {
 | 
			
		||||
    //        //  https://store.steampowered.com/api/appdetails?appids=
 | 
			
		||||
    //        var responseStr = await http.GetStringAsync($"https://store.steampowered.com/api/appdetails?appids={appid}");
 | 
			
		||||
    //        var data = JsonConvert.DeserializeObject<Dictionary<int, SteamGameData.Container>>(responseStr);
 | 
			
		||||
    //        if (!data.ContainsKey(appid) || !data[appid].Success)
 | 
			
		||||
    //            return null; // for some reason we can't get the game with valid appid. SHould never happen
 | 
			
		||||
 | 
			
		||||
    //        return data[appid].Data;
 | 
			
		||||
    //    }
 | 
			
		||||
    //}
 | 
			
		||||
 | 
			
		||||
    public class GoogleSearchResultData
 | 
			
		||||
    {
 | 
			
		||||
        public IReadOnlyList<GoogleSearchResult> Results { get; }
 | 
			
		||||
        public string FullQueryLink { get; }
 | 
			
		||||
        public string TotalResults { get; }
 | 
			
		||||
 | 
			
		||||
        public GoogleSearchResultData(
 | 
			
		||||
            IReadOnlyList<GoogleSearchResult> results,
 | 
			
		||||
            string fullQueryLink,
 | 
			
		||||
            string totalResults)
 | 
			
		||||
        {
 | 
			
		||||
            Results = results;
 | 
			
		||||
            FullQueryLink = fullQueryLink;
 | 
			
		||||
            TotalResults = totalResults;
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,4 @@
 | 
			
		||||
#nullable disable
 | 
			
		||||
using LinqToDB;
 | 
			
		||||
using LinqToDB.EntityFrameworkCore;
 | 
			
		||||
using Microsoft.EntityFrameworkCore;
 | 
			
		||||
using NadekoBot.Common.ModuleBehaviors;
 | 
			
		||||
using NadekoBot.Db;
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,7 @@
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public enum ImgSearchEngine
 | 
			
		||||
{
 | 
			
		||||
    Google,
 | 
			
		||||
    Searx,
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,63 @@
 | 
			
		||||
using Cloneable;
 | 
			
		||||
using NadekoBot.Common.Yml;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
[Cloneable]
 | 
			
		||||
public partial class SearchesConfig : ICloneable<SearchesConfig>
 | 
			
		||||
{
 | 
			
		||||
    [Comment(@"Which engine should .search command
 | 
			
		||||
'google' requires googleApiKey and google.searchId set in creds.yml
 | 
			
		||||
'searx' requires at least one searx instance specified in the 'searxInstances' property below")]
 | 
			
		||||
    public WebSearchEngine WebSearchEngine { get; set; } = WebSearchEngine.Google;
 | 
			
		||||
    
 | 
			
		||||
    [Comment(@"Which engine should .image command use
 | 
			
		||||
'google' requires googleApiKey and google.imageSearchId set in creds.yml
 | 
			
		||||
'searx' requires at least one searx instance specified in the 'searxInstances' property below")]
 | 
			
		||||
    public ImgSearchEngine ImgSearchEngine { get; set; } = ImgSearchEngine.Google;
 | 
			
		||||
    
 | 
			
		||||
 | 
			
		||||
    [Comment(@"Which search provider will be used for the `.youtube` command.
 | 
			
		||||
 | 
			
		||||
- `ytDataApiv3` - uses google's official youtube data api. Requires `GoogleApiKey` set in creds and youtube data api enabled in developers console
 | 
			
		||||
 | 
			
		||||
- `ytdl` - default, uses youtube-dl. Requires `youtube-dl` to be installed and it's path added to env variables. Slow.
 | 
			
		||||
 | 
			
		||||
- `ytdlp` - recommended easy, uses `yt-dlp`. Requires `yt-dlp` to be installed and it's path added to env variables
 | 
			
		||||
 | 
			
		||||
- `invidious` - recommended advanced, uses invidious api. Requires at least one invidious instance specified in the `invidiousInstances` property")]
 | 
			
		||||
    public YoutubeSearcher YtProvider { get; set; } = YoutubeSearcher.Ytdl;
 | 
			
		||||
 | 
			
		||||
    [Comment(@"Set the searx instance urls in case you want to use 'searx' for either img or web search.
 | 
			
		||||
Nadeko will use a random one for each request.
 | 
			
		||||
Use a fully qualified url. Example: `https://my-searx-instance.mydomain.com`
 | 
			
		||||
Instances specified must support 'format=json' query parameter.
 | 
			
		||||
- In case you're running your own searx instance, set 
 | 
			
		||||
 | 
			
		||||
search:
 | 
			
		||||
  formats:
 | 
			
		||||
    - json
 | 
			
		||||
 | 
			
		||||
in 'searxng/settings.yml' on your server 
 | 
			
		||||
 | 
			
		||||
- If you're using a public instance, make sure that the instance you're using supports it (they usually don't)")]
 | 
			
		||||
    public List<string> SearxInstances { get; set; } = new List<string>();
 | 
			
		||||
 | 
			
		||||
    [Comment(@"Set the invidious instance urls in case you want to use 'invidious' for `.youtube` search
 | 
			
		||||
Nadeko will use a random one for each request.
 | 
			
		||||
These instances may be used for music queue functionality in the future.
 | 
			
		||||
Use a fully qualified url. Example: https://my-invidious-instance.mydomain.com
 | 
			
		||||
 | 
			
		||||
Instances specified must have api available.
 | 
			
		||||
You check that by opening an api endpoint in your browser. For example: https://my-invidious-instance.mydomain.com/api/v1/trending")]
 | 
			
		||||
    public List<string> InvidiousInstances { get; set; } = new List<string>();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
public enum YoutubeSearcher
 | 
			
		||||
{
 | 
			
		||||
    YtDataApiv3,
 | 
			
		||||
    Ytdl,
 | 
			
		||||
    Ytdlp,
 | 
			
		||||
    Invid,
 | 
			
		||||
    Invidious = 3
 | 
			
		||||
}
 | 
			
		||||
@@ -0,0 +1,31 @@
 | 
			
		||||
using NadekoBot.Common.Configs;
 | 
			
		||||
 | 
			
		||||
namespace NadekoBot.Modules.Searches;
 | 
			
		||||
 | 
			
		||||
public class SearchesConfigService : ConfigServiceBase<SearchesConfig>
 | 
			
		||||
{
 | 
			
		||||
    private static string FILE_PATH = "data/searches.yml";
 | 
			
		||||
    private static readonly TypedKey<SearchesConfig> _changeKey = new("config.searches.updated");
 | 
			
		||||
 | 
			
		||||
    public override string Name
 | 
			
		||||
        => "searches";
 | 
			
		||||
 | 
			
		||||
    public SearchesConfigService(IConfigSeria serializer, IPubSub pubSub)
 | 
			
		||||
        : base(FILE_PATH, serializer, pubSub, _changeKey)
 | 
			
		||||
    {
 | 
			
		||||
        AddParsedProp("webEngine",
 | 
			
		||||
            sc => sc.WebSearchEngine,
 | 
			
		||||
            ConfigParsers.InsensitiveEnum,
 | 
			
		||||
            ConfigPrinters.ToString);
 | 
			
		||||
        
 | 
			
		||||
        AddParsedProp("imgEngine",
 | 
			
		||||
            sc => sc.ImgSearchEngine,
 | 
			
		||||
            ConfigParsers.InsensitiveEnum,
 | 
			
		||||
            ConfigPrinters.ToString);
 | 
			
		||||
        
 | 
			
		||||
        AddParsedProp("ytProvider",
 | 
			
		||||
            sc => sc.YtProvider,
 | 
			
		||||
            ConfigParsers.InsensitiveEnum,
 | 
			
		||||
            ConfigPrinters.ToString);
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user