mirror of
				https://gitlab.com/Kwoth/nadekobot.git
				synced 2025-11-03 16:24:27 -05:00 
			
		
		
		
	NadekoBot Patronage system, Search commands improvements + fixes
This commit is contained in:
		@@ -7,6 +7,7 @@ stages:
 | 
				
			|||||||
  - release
 | 
					  - release
 | 
				
			||||||
  - publish-windows
 | 
					  - publish-windows
 | 
				
			||||||
  - upload-windows-updater-release
 | 
					  - upload-windows-updater-release
 | 
				
			||||||
 | 
					  - publish-medusa-package
 | 
				
			||||||
 | 
					
 | 
				
			||||||
variables:
 | 
					variables:
 | 
				
			||||||
  project: "NadekoBot"
 | 
					  project: "NadekoBot"
 | 
				
			||||||
@@ -97,6 +98,16 @@ upload-windows-updater-release:
 | 
				
			|||||||
    - aws --endpoint-url $AWS_SERVICE_URL s3api put-object --bucket "$AWS_BUCKET_NAME" --key "dl/bot/$INSTALLER_FILE_NAME" --acl public-read --body "$INSTALLER_OUTPUT_DIR/$INSTALLER_FILE_NAME"
 | 
					    - aws --endpoint-url $AWS_SERVICE_URL s3api put-object --bucket "$AWS_BUCKET_NAME" --key "dl/bot/$INSTALLER_FILE_NAME" --acl public-read --body "$INSTALLER_OUTPUT_DIR/$INSTALLER_FILE_NAME"
 | 
				
			||||||
    - aws --endpoint-url $AWS_SERVICE_URL s3api put-object --bucket "$AWS_BUCKET_NAME" --key "dl/bot/releases-v3.json" --acl public-read --body "releases-v3.json"
 | 
					    - aws --endpoint-url $AWS_SERVICE_URL s3api put-object --bucket "$AWS_BUCKET_NAME" --key "dl/bot/releases-v3.json" --acl public-read --body "releases-v3.json"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					publish-medusa-package:
 | 
				
			||||||
 | 
					  stage: publish-medusa-package
 | 
				
			||||||
 | 
					  rules:
 | 
				
			||||||
 | 
					    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
 | 
				
			||||||
 | 
					  script:
 | 
				
			||||||
 | 
					    - LAST_TAG=$(git describe --tags --abbrev=0)
 | 
				
			||||||
 | 
					    - if [ $CI_COMMIT_TAG ]; then MEDUSA_VERSION=$CI_COMMIT_TAG; else MEDUSA_VERSION="$LAST_TAG-$CI_COMMIT_REF_SLUG"
 | 
				
			||||||
 | 
					    - dotnet pack -c Release /p:Version=$MEDUSA_VERSION -o bin/Release/packed
 | 
				
			||||||
 | 
					    - dotnet nuget push bin/Release/packed/ --api-key $MYGET_API_KEY --source https://www.myget.org/F/nadeko/api/v2/package
 | 
				
			||||||
 | 
					
 | 
				
			||||||
docker-build:
 | 
					docker-build:
 | 
				
			||||||
  # Use the official docker image.
 | 
					  # Use the official docker image.
 | 
				
			||||||
  image: docker:latest
 | 
					  image: docker:latest
 | 
				
			||||||
@@ -120,6 +131,6 @@ docker-build:
 | 
				
			|||||||
    - docker push "$CI_REGISTRY_IMAGE${tag}"
 | 
					    - docker push "$CI_REGISTRY_IMAGE${tag}"
 | 
				
			||||||
  # Run this job in a branch where a Dockerfile exists
 | 
					  # Run this job in a branch where a Dockerfile exists
 | 
				
			||||||
  rules:
 | 
					  rules:
 | 
				
			||||||
    - if: $CI_COMMIT_BRANCH
 | 
					    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
 | 
				
			||||||
      exists:
 | 
					      exists:
 | 
				
			||||||
        - Dockerfile
 | 
					        - Dockerfile
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										129
									
								
								CHANGELOG.md
									
									
									
									
									
								
							
							
						
						
									
										129
									
								
								CHANGELOG.md
									
									
									
									
									
								
							@@ -3,7 +3,134 @@
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
 | 
					Experimental changelog. Mostly based on [keepachangelog](https://keepachangelog.com/en/1.0.0/) except date format. a-c-f-r-o
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Unreleased
 | 
					## [4.2.0] - 14.06.2022
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Added
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Added `data/searches.yml` file which configures some of the new search functionality
 | 
				
			||||||
 | 
					  The file comments explaining what each property does.
 | 
				
			||||||
 | 
					  Explained briefly here:
 | 
				
			||||||
 | 
					  ```yml
 | 
				
			||||||
 | 
					  # what will be used for .google command. Either google (official api) or searx
 | 
				
			||||||
 | 
					  webSearchEngine: Google
 | 
				
			||||||
 | 
					  # what will be used for .img command. Either google (official api) or searx
 | 
				
			||||||
 | 
					  imgSearchEngine: Google
 | 
				
			||||||
 | 
					  # how will yt results be retrieved: ytdataapi or ytdl or ytdlp
 | 
				
			||||||
 | 
					  ytProvider: YtDataApiv3
 | 
				
			||||||
 | 
					  # in case web or img search is set to searx, the following instances will be used:
 | 
				
			||||||
 | 
					  searxInstances: []
 | 
				
			||||||
 | 
					  # in case ytProvider is set to invidious, the following instances will be used
 | 
				
			||||||
 | 
					  invidiousInstances: []
 | 
				
			||||||
 | 
					  ```
 | 
				
			||||||
 | 
					- Added new properties to `creds.yml`. google -> searchId and google -> searchImageId.
 | 
				
			||||||
 | 
					- These properties are used as `cx` (google api query parameter) in case you've setup your `data/searches.yml` to use the official google api.
 | 
				
			||||||
 | 
					  `searchId` is used for web search
 | 
				
			||||||
 | 
					  `searchimageId` is used for image search
 | 
				
			||||||
 | 
					  ```yml
 | 
				
			||||||
 | 
					  google:
 | 
				
			||||||
 | 
					      searchId: ""
 | 
				
			||||||
 | 
					      searchImageId: ""
 | 
				
			||||||
 | 
					  ```
 | 
				
			||||||
 | 
					- Check `creds_example.yml` for comments explaining how to obtain them.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#### Patronage system added
 | 
				
			||||||
 | 
					- Added `data/patron.yml` for configuration
 | 
				
			||||||
 | 
					- Implemented only for patreon so far
 | 
				
			||||||
 | 
					- Patreon subscription code completely rewritten
 | 
				
			||||||
 | 
					- Users who pledge on patreon get benefits based on the amount they pledged
 | 
				
			||||||
 | 
					- Public nadeko only. But selfhosters can adapt it to their own patreon pages by configuring their patreon credentials in `creds.yml` and enabling the system in `data/patron.yml` file.
 | 
				
			||||||
 | 
					  - Most of the patronage system strings are hardcoded atm, so if you wish to use this system on selfhosts, you will have to modify the source
 | 
				
			||||||
 | 
					- Pledge amounts are split into tiers. This is not configurable atm.
 | 
				
			||||||
 | 
					  - Tier I - 1$ - 4.99$ a month
 | 
				
			||||||
 | 
					  - Tier V - 5$ - 9.99$ a month
 | 
				
			||||||
 | 
					  - Tier X - 10$ - 19.99$ a month
 | 
				
			||||||
 | 
					  - Tier XX - 20$ - 49.99$ a month
 | 
				
			||||||
 | 
					  - Tier L - 50$ - 99.99$ a month
 | 
				
			||||||
 | 
					  - Tier C - 100$+ a month
 | 
				
			||||||
 | 
					- Rewards and command quotas for each of the tiers are configurable
 | 
				
			||||||
 | 
					- Limitations to certain features are also configurable. ex:
 | 
				
			||||||
 | 
					```yml
 | 
				
			||||||
 | 
					quotas:
 | 
				
			||||||
 | 
					    features:
 | 
				
			||||||
 | 
					        "rero:max_count":
 | 
				
			||||||
 | 
					            x: 50
 | 
				
			||||||
 | 
					```
 | 
				
			||||||
 | 
					- ^ this setting would set the maximum number of reaction roles to be 50 for a user who is in Patron Tier X
 | 
				
			||||||
 | 
					- Read the comments in the .yml file for (much) more info
 | 
				
			||||||
 | 
					- Quota system allows the owner to set up hourly, daily and monthly quota usage for each tier
 | 
				
			||||||
 | 
					- Quota system applies to entire server owner by a patron
 | 
				
			||||||
 | 
					  - Patron spends own quota by using the commands on any server
 | 
				
			||||||
 | 
					  - Any user on *any* server owned by a patron spends that patron's quota
 | 
				
			||||||
 | 
					- When users subscribe to patreon they will receive a welcome message
 | 
				
			||||||
 | 
					  - If you're enabling patron system for a selfhost, you will want to edit it
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Added `.patron` and `.patronmessage` commands
 | 
				
			||||||
 | 
					- `.patron` checks your patronage status, and quotas. Requires patron system to be enabled.
 | 
				
			||||||
 | 
					- `.patronmessage` (owner only) sends message to all patrons with the specified tier or higher. Supports embeds
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Added a fake `.cmdcd` command `cleverbot:response` which can be used to limit how often users can talk to the cleverbot.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Changed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- CurrencyReward now support adding additional flowers to patrons.
 | 
				
			||||||
 | 
					- `.donate` command completely reworked.
 | 
				
			||||||
 | 
					  - Works only on public bot (OnlyPublicBotAttribute)
 | 
				
			||||||
 | 
					  - Guides user on how to donate to support the project
 | 
				
			||||||
 | 
					  - Added interaction explaining selfhosting
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- `.google` reimplemented. It now has 2 modes configurable in `data/searches.yml` under the `webSearchengine` property
 | 
				
			||||||
 | 
					  - If set to `google`, official custom search api will be used. You will need to set googleapikey and google.searchId in `creds.yml`
 | 
				
			||||||
 | 
					  - if set to `searx` one of the instances specified in the `searxInstances:` property will be randomly chosen for each request
 | 
				
			||||||
 | 
					    - instances must have `format=json` allowed (public ones usually don't allow it)
 | 
				
			||||||
 | 
					    - instances are specified as a fully qualified url, example: `https://my.cool.searx.instance.io`
 | 
				
			||||||
 | 
					- `.image` reimplemented. Same as `.google` - it uses either `google` official api (in which case it uses `google.searchImageId` from `creds.yml`) or `searx`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- `.youtube` reimplemented. It will use a `ytProvider:` property from `data/searches.yml` to determine how to retrieve results
 | 
				
			||||||
 | 
					  - `ytdataapi` will use the official google api (requires `GoogleApiKey` specified in `creds.yml`) and YoutubeDataApi enabled in the dev console
 | 
				
			||||||
 | 
					  - `ytdl` will use `youtube-dl` program from the host machine. It must be downloaded and it's location must be added to path env variable.
 | 
				
			||||||
 | 
					  - `ytdlp` will use `yt-dlp` program from the host machine. Same as `youtube-dl` - must be in path env variable.
 | 
				
			||||||
 | 
					  - `invidious` will use one of invidious instances specified in the `invidiousInstances` property. Very good. 
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- `.google`, `.youtube` and `.image` moved to the new Search group
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Note: Results of each `.youtube` query will be cached for 1 hour to improve perfomance
 | 
				
			||||||
 | 
					- Removed 30 second `.ping` ratelimit on public nadeko
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- xp image generation changes
 | 
				
			||||||
 | 
					  - In case you have default settings, your xp image will look slightly different
 | 
				
			||||||
 | 
					  - If you've modified xp_template.json, your xp image might look broken. Your old template will be saved in xp_template.json.old
 | 
				
			||||||
 | 
					  - Xp number outline is now slightly thicker
 | 
				
			||||||
 | 
					  - Xp number will now have Center vertical and horizontal alignment
 | 
				
			||||||
 | 
					  - LastLevelUp no longer supported
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- Some commands will now use timestamp tags for better user experience
 | 
				
			||||||
 | 
					- `.prune` was slightly slowed down to avoid ratelimits
 | 
				
			||||||
 | 
					- `.wof` moved from it's own group to the default Gambling group
 | 
				
			||||||
 | 
					- `.feed` urls which error for more than 100 times will be automatically removed.
 | 
				
			||||||
 | 
					- `.ve` is now enabled by default
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- [dev] nadeko interaction slightly improved to make it less nonsense (they still don't make sense)  
 | 
				
			||||||
 | 
					- [dev] RewardedUsers table slightly changed to make it more general  
 | 
				
			||||||
 | 
					- [dev] renamed `// todo`s which aren't planned soon to `// FUTURE`  
 | 
				
			||||||
 | 
					- [dev] currency rewards have been reimplemented and moved to a separate service  
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Fixed
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
 | 
					- `.rh` no longer needs quotes for multi word roles
 | 
				
			||||||
 | 
					- `.deletexp` will now properly delete server xp too
 | 
				
			||||||
 | 
					- [dev] added support for configs to properly parse enums without case sensitivity (ConfigParsers.InsensitiveEnum)
 | 
				
			||||||
 | 
					- [dev] Fixed a bug in .gencmdlist  
 | 
				
			||||||
 | 
					- [dev] small fixes to creds provider  
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Removed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- `.ddg` removed.
 | 
				
			||||||
 | 
					- [dev] removed some dead code and comments
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					### Obsolete
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  
 | 
				
			||||||
 | 
					
 | 
				
			||||||
### Fixed
 | 
					### Fixed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,7 +5,7 @@ namespace NadekoBot;
 | 
				
			|||||||
public interface IEmbedBuilder
 | 
					public interface IEmbedBuilder
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    IEmbedBuilder WithDescription(string? desc);
 | 
					    IEmbedBuilder WithDescription(string? desc);
 | 
				
			||||||
    IEmbedBuilder WithTitle(string title);
 | 
					    IEmbedBuilder WithTitle(string? title);
 | 
				
			||||||
    IEmbedBuilder AddField(string title, object value, bool isInline = false);
 | 
					    IEmbedBuilder AddField(string title, object value, bool isInline = false);
 | 
				
			||||||
    IEmbedBuilder WithFooter(string text, string? iconUrl = null);
 | 
					    IEmbedBuilder WithFooter(string text, string? iconUrl = null);
 | 
				
			||||||
    IEmbedBuilder WithAuthor(string name, string? iconUrl = null, string? url = null);
 | 
					    IEmbedBuilder WithAuthor(string name, string? iconUrl = null, string? url = null);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,7 +9,6 @@
 | 
				
			|||||||
        <RootNamespace>Nadeko.Snake</RootNamespace>
 | 
					        <RootNamespace>Nadeko.Snake</RootNamespace>
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        <Authors>The NadekoBot Team</Authors>
 | 
					        <Authors>The NadekoBot Team</Authors>
 | 
				
			||||||
        <Version>1.0.3</Version>
 | 
					 | 
				
			||||||
    </PropertyGroup>
 | 
					    </PropertyGroup>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    <ItemGroup>
 | 
					    <ItemGroup>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,9 +4,11 @@ using NadekoBot.Common.Configs;
 | 
				
			|||||||
using NadekoBot.Common.ModuleBehaviors;
 | 
					using NadekoBot.Common.ModuleBehaviors;
 | 
				
			||||||
using NadekoBot.Db;
 | 
					using NadekoBot.Db;
 | 
				
			||||||
using NadekoBot.Modules.Administration;
 | 
					using NadekoBot.Modules.Administration;
 | 
				
			||||||
 | 
					using NadekoBot.Modules.Utility;
 | 
				
			||||||
using NadekoBot.Services.Database.Models;
 | 
					using NadekoBot.Services.Database.Models;
 | 
				
			||||||
using System.Collections.Immutable;
 | 
					using System.Collections.Immutable;
 | 
				
			||||||
using System.Diagnostics;
 | 
					using System.Diagnostics;
 | 
				
			||||||
 | 
					using System.Net;
 | 
				
			||||||
using System.Reflection;
 | 
					using System.Reflection;
 | 
				
			||||||
using RunMode = Discord.Commands.RunMode;
 | 
					using RunMode = Discord.Commands.RunMode;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -126,6 +128,12 @@ public sealed class Bot
 | 
				
			|||||||
                AllowAutoRedirect = false
 | 
					                AllowAutoRedirect = false
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
 | 
					        svcs.AddHttpClient("google:search")
 | 
				
			||||||
 | 
					            .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler()
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					                AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (Environment.GetEnvironmentVariable("NADEKOBOT_IS_COORDINATED") != "1")
 | 
					        if (Environment.GetEnvironmentVariable("NADEKOBOT_IS_COORDINATED") != "1")
 | 
				
			||||||
            svcs.AddSingleton<ICoordinator, SingleProcessCoordinator>();
 | 
					            svcs.AddSingleton<ICoordinator, SingleProcessCoordinator>();
 | 
				
			||||||
        else
 | 
					        else
 | 
				
			||||||
@@ -164,6 +172,7 @@ public sealed class Bot
 | 
				
			|||||||
        //initialize Services
 | 
					        //initialize Services
 | 
				
			||||||
        Services = svcs.BuildServiceProvider();
 | 
					        Services = svcs.BuildServiceProvider();
 | 
				
			||||||
        Services.GetRequiredService<IBehaviorHandler>().Initialize();
 | 
					        Services.GetRequiredService<IBehaviorHandler>().Initialize();
 | 
				
			||||||
 | 
					        Services.GetRequiredService<CurrencyRewardService>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (Client.ShardId == 0)
 | 
					        if (Client.ShardId == 0)
 | 
				
			||||||
            ApplyConfigMigrations();
 | 
					            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>
 | 
					[AttributeUsage(AttributeTargets.Method | AttributeTargets.Class)]
 | 
				
			||||||
/// Classed marked with this attribute will not be added to the service provider 
 | 
					[SuppressMessage("Style", "IDE0022:Use expression body for methods")]
 | 
				
			||||||
/// </summary>
 | 
					public sealed class OnlyPublicBotAttribute : PreconditionAttribute
 | 
				
			||||||
[AttributeUsage(AttributeTargets.Class)]
 | 
					 | 
				
			||||||
public class DontAddToIocContainerAttribute : Attribute
 | 
					 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    
 | 
					    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")]
 | 
					    [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; }
 | 
					    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.")]
 | 
					Leave at 1 if you don't know what you're doing.")]
 | 
				
			||||||
    public int TotalShards { get; set; }
 | 
					    public int TotalShards { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -28,6 +28,16 @@ Then, go to APIs and Services -> Credentials and click Create credentials -> API
 | 
				
			|||||||
Used only for Youtube Data Api (at the moment).")]
 | 
					Used only for Youtube Data Api (at the moment).")]
 | 
				
			||||||
    public string GoogleApiKey { get; set; }
 | 
					    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.")]
 | 
					    [Comment(@"Settings for voting system for discordbots. Meant for use on global Nadeko.")]
 | 
				
			||||||
    public VotesSettings Votes { get; set; }
 | 
					    public VotesSettings Votes { get; set; }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -119,6 +129,7 @@ Windows default
 | 
				
			|||||||
        CoordinatorUrl = "http://localhost:3442";
 | 
					        CoordinatorUrl = "http://localhost:3442";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        RestartCommand = new();
 | 
					        RestartCommand = new();
 | 
				
			||||||
 | 
					        Google = new();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -201,3 +212,9 @@ This should be equivalent to the DiscordsKey in your NadekoBot.Votes api appsett
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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 CoordinatorUrl { get; set; }
 | 
				
			||||||
    string TwitchClientId { get; set; }
 | 
					    string TwitchClientId { get; set; }
 | 
				
			||||||
    string TwitchClientSecret { get; set; }
 | 
					    string TwitchClientSecret { get; set; }
 | 
				
			||||||
 | 
					    GoogleApiConfig Google { get; set; }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public class RestartConfig
 | 
					public class RestartConfig
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,11 +1,11 @@
 | 
				
			|||||||
namespace NadekoBot;
 | 
					namespace NadekoBot;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public sealed class NadekoActionInteraction : NadekoOwnInteraction
 | 
					public sealed class NadekoButtonActionInteraction : NadekoButtonOwnInteraction
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    private readonly NadekoInteractionData _data;
 | 
					    private readonly NadekoInteractionData _data;
 | 
				
			||||||
    private readonly Func<SocketMessageComponent, Task> _action;
 | 
					    private readonly Func<SocketMessageComponent, Task> _action;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public NadekoActionInteraction(
 | 
					    public NadekoButtonActionInteraction(
 | 
				
			||||||
        DiscordSocketClient client,
 | 
					        DiscordSocketClient client,
 | 
				
			||||||
        ulong authorId,
 | 
					        ulong authorId,
 | 
				
			||||||
        NadekoInteractionData data,
 | 
					        NadekoInteractionData data,
 | 
				
			||||||
@@ -17,10 +17,12 @@ public sealed class NadekoActionInteraction : NadekoOwnInteraction
 | 
				
			|||||||
        _action = action;
 | 
					        _action = action;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public override string Name
 | 
					    protected override string Name
 | 
				
			||||||
        => _data.CustomId;
 | 
					        => _data.CustomId;
 | 
				
			||||||
    public override IEmote Emote
 | 
					    protected override IEmote Emote
 | 
				
			||||||
        => _data.Emote;
 | 
					        => _data.Emote;
 | 
				
			||||||
 | 
					    protected override string? Text
 | 
				
			||||||
 | 
					        => _data.Text;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public override Task ExecuteOnActionAsync(SocketMessageComponent smc)
 | 
					    public override Task ExecuteOnActionAsync(SocketMessageComponent smc)
 | 
				
			||||||
        => _action(smc);
 | 
					        => _action(smc);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,23 +1,24 @@
 | 
				
			|||||||
namespace NadekoBot;
 | 
					namespace NadekoBot;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public abstract class NadekoInteraction
 | 
					public abstract class NadekoButtonInteraction
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    // improvements:
 | 
					    // improvements:
 | 
				
			||||||
    //  - state in OnAction
 | 
					    //  - state in OnAction
 | 
				
			||||||
    //  - configurable delay
 | 
					    //  - configurable delay
 | 
				
			||||||
    //  - 
 | 
					    //  - 
 | 
				
			||||||
    public abstract string Name { get; }
 | 
					    protected abstract string Name { get; }
 | 
				
			||||||
    public abstract IEmote Emote { 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 readonly TaskCompletionSource<bool> _interactionCompletedSource;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected IUserMessage message = null!;
 | 
					    protected IUserMessage message = null!;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected NadekoInteraction(DiscordSocketClient client)
 | 
					    protected NadekoButtonInteraction(DiscordSocketClient client)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        _client = client;
 | 
					        Client = client;
 | 
				
			||||||
        _interactionCompletedSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
 | 
					        _interactionCompletedSource = new(TaskCreationOptions.RunContinuationsAsynchronously);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -25,9 +26,9 @@ public abstract class NadekoInteraction
 | 
				
			|||||||
    {
 | 
					    {
 | 
				
			||||||
        message = msg;
 | 
					        message = msg;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        _client.InteractionCreated += OnInteraction;
 | 
					        Client.InteractionCreated += OnInteraction;
 | 
				
			||||||
        await Task.WhenAny(Task.Delay(10_000), _interactionCompletedSource.Task);
 | 
					        await Task.WhenAny(Task.Delay(10_000), _interactionCompletedSource.Task);
 | 
				
			||||||
        _client.InteractionCreated -= OnInteraction;
 | 
					        Client.InteractionCreated -= OnInteraction;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        await msg.ModifyAsync(m => m.Components = new ComponentBuilder().Build());
 | 
					        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()
 | 
					        var comp = new ComponentBuilder()
 | 
				
			||||||
            .WithButton(new ButtonBuilder(style: ButtonStyle.Secondary, emote: Emote, customId: Name));
 | 
					            .WithButton(GetButtonBuilder());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return comp.Build();
 | 
					        return comp.Build();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    public ButtonBuilder GetButtonBuilder()
 | 
				
			||||||
 | 
					        => new ButtonBuilder(style: ButtonStyle.Secondary, emote: Emote, customId: Name, label: Text);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public abstract Task ExecuteOnActionAsync(SocketMessageComponent smc);
 | 
					    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;
 | 
					    //     this.isOwn = isOwn;
 | 
				
			||||||
    //     return this;
 | 
					    //     return this;
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    // }
 | 
					    // }
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    public NadekoInteractionBuilder WithAction(in Func<SocketMessageComponent, Task> fn)
 | 
					    public NadekoInteractionBuilder WithAction(in Func<SocketMessageComponent, Task> fn)
 | 
				
			||||||
@@ -28,7 +29,7 @@ public class NadekoInteractionBuilder
 | 
				
			|||||||
        return this;
 | 
					        return this;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public NadekoActionInteraction Build(DiscordSocketClient client, ulong userId)
 | 
					    public NadekoButtonActionInteraction Build(DiscordSocketClient client, ulong userId)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        if (iData is null)
 | 
					        if (iData is null)
 | 
				
			||||||
            throw new InvalidOperationException("You have to specify the data before building the interaction");
 | 
					            throw new InvalidOperationException("You have to specify the data before building the interaction");
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,4 +5,4 @@
 | 
				
			|||||||
/// </summary>
 | 
					/// </summary>
 | 
				
			||||||
/// <param name="Emote">Emote which will show on a button</param>
 | 
					/// <param name="Emote">Emote which will show on a button</param>
 | 
				
			||||||
/// <param name="CustomId">Custom interaction id</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>
 | 
					/// <summary>
 | 
				
			||||||
/// Interaction which only the author can use
 | 
					/// Interaction which only the author can use
 | 
				
			||||||
/// </summary>
 | 
					/// </summary>
 | 
				
			||||||
public abstract class NadekoOwnInteraction : NadekoInteraction
 | 
					public abstract class NadekoButtonOwnInteraction : NadekoButtonInteraction
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    protected readonly ulong _authorId;
 | 
					    protected readonly ulong _authorId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected NadekoOwnInteraction(DiscordSocketClient client, ulong authorId) : base(client)
 | 
					    protected NadekoButtonOwnInteraction(DiscordSocketClient client, ulong authorId) : base(client)
 | 
				
			||||||
        => _authorId = authorId;
 | 
					        => _authorId = authorId;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected override ValueTask<bool> Validate(SocketMessageComponent smc)
 | 
					    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 error,
 | 
				
			||||||
        string url = null,
 | 
					        string url = null,
 | 
				
			||||||
        string footer = null, 
 | 
					        string footer = null, 
 | 
				
			||||||
        NadekoInteraction inter = null)
 | 
					        NadekoButtonInteraction inter = null)
 | 
				
			||||||
        => ctx.Channel.SendErrorAsync(_eb, title, error, url, footer);
 | 
					        => ctx.Channel.SendErrorAsync(_eb, title, error, url, footer);
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    public Task<IUserMessage> SendConfirmAsync(
 | 
					    public Task<IUserMessage> SendConfirmAsync(
 | 
				
			||||||
@@ -47,32 +47,32 @@ public abstract class NadekoModule : ModuleBase
 | 
				
			|||||||
        => ctx.Channel.SendConfirmAsync(_eb, title, text, url, footer);
 | 
					        => 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);
 | 
					        => 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);
 | 
					        => 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);
 | 
					        => ctx.Channel.SendAsync(_eb, text, MessageType.Pending, inter);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    // localized normal
 | 
					    // localized normal
 | 
				
			||||||
    public Task<IUserMessage> ErrorLocalizedAsync(LocStr str, NadekoInteraction inter = null)
 | 
					    public Task<IUserMessage> ErrorLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null)
 | 
				
			||||||
        => SendErrorAsync(GetText(str), inter);
 | 
					        => 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);
 | 
					        => 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);
 | 
					        => SendConfirmAsync(GetText(str), inter);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // localized replies
 | 
					    // 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)}");
 | 
					        => 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)}");
 | 
					        => 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)}");
 | 
					        => SendConfirmAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public async Task<bool> PromptUserConfirmAsync(IEmbedBuilder embed)
 | 
					    public async Task<bool> PromptUserConfirmAsync(IEmbedBuilder embed)
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,4 @@
 | 
				
			|||||||
#nullable disable
 | 
					#nullable disable
 | 
				
			||||||
using Newtonsoft.Json;
 | 
					 | 
				
			||||||
using Newtonsoft.Json.Linq;
 | 
					using Newtonsoft.Json.Linq;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace NadekoBot;
 | 
					namespace NadekoBot;
 | 
				
			||||||
@@ -45,7 +44,6 @@ public abstract record SmartText
 | 
				
			|||||||
            _ => throw new ArgumentOutOfRangeException(nameof(text))
 | 
					            _ => throw new ArgumentOutOfRangeException(nameof(text))
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    [CanBeNull]
 | 
					 | 
				
			||||||
    public static SmartText CreateFrom(string input)
 | 
					    public static SmartText CreateFrom(string input)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        if (string.IsNullOrWhiteSpace(input))
 | 
					        if (string.IsNullOrWhiteSpace(input))
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,8 @@ using NadekoBot.Services.Database.Models;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
namespace NadekoBot.Db.Models;
 | 
					namespace NadekoBot.Db.Models;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// FUTURE remove LastLevelUp from here and UserXpStats
 | 
				
			||||||
public class DiscordUser : DbEntity
 | 
					public class DiscordUser : DbEntity
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    public ulong UserId { get; set; }
 | 
					    public ulong UserId { get; set; }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -84,7 +84,7 @@ public class GuildConfig : DbEntity
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public List<ShopEntry> ShopEntries { get; set; }
 | 
					    public List<ShopEntry> ShopEntries { get; set; }
 | 
				
			||||||
    public ulong? GameVoiceChannel { get; set; }
 | 
					    public ulong? GameVoiceChannel { get; set; }
 | 
				
			||||||
    public bool VerboseErrors { get; set; }
 | 
					    public bool VerboseErrors { get; set; } = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public StreamRoleSettings StreamRole { get; set; }
 | 
					    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 class RewardedUser : DbEntity
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    public ulong UserId { get; set; }
 | 
					    public ulong UserId { get; set; }
 | 
				
			||||||
    public string PatreonUserId { get; set; }
 | 
					    public string PlatformUserId { get; set; }
 | 
				
			||||||
    public int AmountRewardedThisMonth { get; set; }
 | 
					    public long AmountRewardedThisMonth { get; set; }
 | 
				
			||||||
    public DateTime LastReward { get; set; }
 | 
					    public DateTime LastReward { get; set; }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1,6 +1,5 @@
 | 
				
			|||||||
#nullable disable
 | 
					#nullable disable
 | 
				
			||||||
using Microsoft.EntityFrameworkCore;
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore.Design;
 | 
					 | 
				
			||||||
using Microsoft.Extensions.Logging;
 | 
					using Microsoft.Extensions.Logging;
 | 
				
			||||||
using NadekoBot.Db.Models;
 | 
					using NadekoBot.Db.Models;
 | 
				
			||||||
using NadekoBot.Services.Database.Models;
 | 
					using NadekoBot.Services.Database.Models;
 | 
				
			||||||
@@ -56,6 +55,10 @@ public abstract class NadekoContext : DbContext
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    public DbSet<ReactionRoleV2> ReactionRoles { 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
 | 
					    #region Mandatory Provider-Specific Values
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    protected abstract string CurrencyTransactionOtherIdDefaultValue { get; }
 | 
					    protected abstract string CurrencyTransactionOtherIdDefaultValue { get; }
 | 
				
			||||||
@@ -77,7 +80,11 @@ public abstract class NadekoContext : DbContext
 | 
				
			|||||||
        #region GuildConfig
 | 
					        #region GuildConfig
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var configEntity = modelBuilder.Entity<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);
 | 
					        modelBuilder.Entity<AntiSpamSetting>().HasOne(x => x.GuildConfig).WithOne(x => x.AntiSpamSetting);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -193,13 +200,6 @@ public abstract class NadekoContext : DbContext
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        #endregion
 | 
					        #endregion
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        #region PatreonRewards
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        var pr = modelBuilder.Entity<RewardedUser>();
 | 
					 | 
				
			||||||
        pr.HasIndex(x => x.PatreonUserId).IsUnique();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        #endregion
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        #region XpStats
 | 
					        #region XpStats
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var xps = modelBuilder.Entity<UserXpStats>();
 | 
					        var xps = modelBuilder.Entity<UserXpStats>();
 | 
				
			||||||
@@ -369,10 +369,11 @@ public abstract class NadekoContext : DbContext
 | 
				
			|||||||
               .IsUnique(false);
 | 
					               .IsUnique(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            rr2.HasIndex(x => new
 | 
					            rr2.HasIndex(x => new
 | 
				
			||||||
            {
 | 
					               {
 | 
				
			||||||
                x.MessageId,
 | 
					                   x.MessageId,
 | 
				
			||||||
                x.Emote
 | 
					                   x.Emote
 | 
				
			||||||
            }).IsUnique();
 | 
					               })
 | 
				
			||||||
 | 
					               .IsUnique();
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        #endregion
 | 
					        #endregion
 | 
				
			||||||
@@ -420,6 +421,36 @@ public abstract class NadekoContext : DbContext
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        #endregion
 | 
					        #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
 | 
					#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
 | 
					#pragma warning disable 612, 618
 | 
				
			||||||
            modelBuilder
 | 
					            modelBuilder
 | 
				
			||||||
                .HasAnnotation("ProductVersion", "6.0.4")
 | 
					                .HasAnnotation("ProductVersion", "6.0.5")
 | 
				
			||||||
                .HasAnnotation("Relational:MaxIdentifierLength", 64);
 | 
					                .HasAnnotation("Relational:MaxIdentifierLength", 64);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            modelBuilder.Entity("NadekoBot.Db.Models.BankUser", b =>
 | 
					            modelBuilder.Entity("NadekoBot.Db.Models.BankUser", b =>
 | 
				
			||||||
@@ -186,10 +186,10 @@ namespace NadekoBot.Migrations.Mysql
 | 
				
			|||||||
                        .HasDefaultValue(0)
 | 
					                        .HasDefaultValue(0)
 | 
				
			||||||
                        .HasColumnName("notifyonlevelup");
 | 
					                        .HasColumnName("notifyonlevelup");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<int>("TotalXp")
 | 
					                    b.Property<long>("TotalXp")
 | 
				
			||||||
                        .ValueGeneratedOnAdd()
 | 
					                        .ValueGeneratedOnAdd()
 | 
				
			||||||
                        .HasColumnType("int")
 | 
					                        .HasColumnType("bigint")
 | 
				
			||||||
                        .HasDefaultValue(0)
 | 
					                        .HasDefaultValue(0L)
 | 
				
			||||||
                        .HasColumnName("totalxp");
 | 
					                        .HasColumnName("totalxp");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<ulong>("UserId")
 | 
					                    b.Property<ulong>("UserId")
 | 
				
			||||||
@@ -265,6 +265,74 @@ namespace NadekoBot.Migrations.Mysql
 | 
				
			|||||||
                    b.ToTable("followedstream", (string)null);
 | 
					                    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 =>
 | 
					            modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiAltSetting", b =>
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    b.Property<int>("Id")
 | 
					                    b.Property<int>("Id")
 | 
				
			||||||
@@ -1138,7 +1206,9 @@ namespace NadekoBot.Migrations.Mysql
 | 
				
			|||||||
                        .HasColumnName("timezoneid");
 | 
					                        .HasColumnName("timezoneid");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<bool>("VerboseErrors")
 | 
					                    b.Property<bool>("VerboseErrors")
 | 
				
			||||||
 | 
					                        .ValueGeneratedOnAdd()
 | 
				
			||||||
                        .HasColumnType("tinyint(1)")
 | 
					                        .HasColumnType("tinyint(1)")
 | 
				
			||||||
 | 
					                        .HasDefaultValue(true)
 | 
				
			||||||
                        .HasColumnName("verboseerrors");
 | 
					                        .HasColumnName("verboseerrors");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<bool>("VerbosePermissions")
 | 
					                    b.Property<bool>("VerbosePermissions")
 | 
				
			||||||
@@ -1962,8 +2032,8 @@ namespace NadekoBot.Migrations.Mysql
 | 
				
			|||||||
                        .HasColumnType("int")
 | 
					                        .HasColumnType("int")
 | 
				
			||||||
                        .HasColumnName("id");
 | 
					                        .HasColumnName("id");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<int>("AmountRewardedThisMonth")
 | 
					                    b.Property<long>("AmountRewardedThisMonth")
 | 
				
			||||||
                        .HasColumnType("int")
 | 
					                        .HasColumnType("bigint")
 | 
				
			||||||
                        .HasColumnName("amountrewardedthismonth");
 | 
					                        .HasColumnName("amountrewardedthismonth");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<DateTime?>("DateAdded")
 | 
					                    b.Property<DateTime?>("DateAdded")
 | 
				
			||||||
@@ -1974,9 +2044,9 @@ namespace NadekoBot.Migrations.Mysql
 | 
				
			|||||||
                        .HasColumnType("datetime(6)")
 | 
					                        .HasColumnType("datetime(6)")
 | 
				
			||||||
                        .HasColumnName("lastreward");
 | 
					                        .HasColumnName("lastreward");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<string>("PatreonUserId")
 | 
					                    b.Property<string>("PlatformUserId")
 | 
				
			||||||
                        .HasColumnType("varchar(255)")
 | 
					                        .HasColumnType("varchar(255)")
 | 
				
			||||||
                        .HasColumnName("patreonuserid");
 | 
					                        .HasColumnName("platformuserid");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<ulong>("UserId")
 | 
					                    b.Property<ulong>("UserId")
 | 
				
			||||||
                        .HasColumnType("bigint unsigned")
 | 
					                        .HasColumnType("bigint unsigned")
 | 
				
			||||||
@@ -1985,9 +2055,9 @@ namespace NadekoBot.Migrations.Mysql
 | 
				
			|||||||
                    b.HasKey("Id")
 | 
					                    b.HasKey("Id")
 | 
				
			||||||
                        .HasName("pk_rewardedusers");
 | 
					                        .HasName("pk_rewardedusers");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.HasIndex("PatreonUserId")
 | 
					                    b.HasIndex("PlatformUserId")
 | 
				
			||||||
                        .IsUnique()
 | 
					                        .IsUnique()
 | 
				
			||||||
                        .HasDatabaseName("ix_rewardedusers_patreonuserid");
 | 
					                        .HasDatabaseName("ix_rewardedusers_platformuserid");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.ToTable("rewardedusers", (string)null);
 | 
					                    b.ToTable("rewardedusers", (string)null);
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
@@ -2404,8 +2474,8 @@ namespace NadekoBot.Migrations.Mysql
 | 
				
			|||||||
                        .HasColumnType("int")
 | 
					                        .HasColumnType("int")
 | 
				
			||||||
                        .HasColumnName("id");
 | 
					                        .HasColumnName("id");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<int>("AwardedXp")
 | 
					                    b.Property<long>("AwardedXp")
 | 
				
			||||||
                        .HasColumnType("int")
 | 
					                        .HasColumnType("bigint")
 | 
				
			||||||
                        .HasColumnName("awardedxp");
 | 
					                        .HasColumnName("awardedxp");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<DateTime?>("DateAdded")
 | 
					                    b.Property<DateTime?>("DateAdded")
 | 
				
			||||||
@@ -2430,8 +2500,8 @@ namespace NadekoBot.Migrations.Mysql
 | 
				
			|||||||
                        .HasColumnType("bigint unsigned")
 | 
					                        .HasColumnType("bigint unsigned")
 | 
				
			||||||
                        .HasColumnName("userid");
 | 
					                        .HasColumnName("userid");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<int>("Xp")
 | 
					                    b.Property<long>("Xp")
 | 
				
			||||||
                        .HasColumnType("int")
 | 
					                        .HasColumnType("bigint")
 | 
				
			||||||
                        .HasColumnName("xp");
 | 
					                        .HasColumnName("xp");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.HasKey("Id")
 | 
					                    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
 | 
					#pragma warning disable 612, 618
 | 
				
			||||||
            modelBuilder
 | 
					            modelBuilder
 | 
				
			||||||
                .HasAnnotation("ProductVersion", "6.0.4")
 | 
					                .HasAnnotation("ProductVersion", "6.0.5")
 | 
				
			||||||
                .HasAnnotation("Relational:MaxIdentifierLength", 63);
 | 
					                .HasAnnotation("Relational:MaxIdentifierLength", 63);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
 | 
					            NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder);
 | 
				
			||||||
@@ -194,10 +194,10 @@ namespace NadekoBot.Migrations.PostgreSql
 | 
				
			|||||||
                        .HasDefaultValue(0)
 | 
					                        .HasDefaultValue(0)
 | 
				
			||||||
                        .HasColumnName("notifyonlevelup");
 | 
					                        .HasColumnName("notifyonlevelup");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<int>("TotalXp")
 | 
					                    b.Property<long>("TotalXp")
 | 
				
			||||||
                        .ValueGeneratedOnAdd()
 | 
					                        .ValueGeneratedOnAdd()
 | 
				
			||||||
                        .HasColumnType("integer")
 | 
					                        .HasColumnType("bigint")
 | 
				
			||||||
                        .HasDefaultValue(0)
 | 
					                        .HasDefaultValue(0L)
 | 
				
			||||||
                        .HasColumnName("totalxp");
 | 
					                        .HasColumnName("totalxp");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<decimal>("UserId")
 | 
					                    b.Property<decimal>("UserId")
 | 
				
			||||||
@@ -275,6 +275,74 @@ namespace NadekoBot.Migrations.PostgreSql
 | 
				
			|||||||
                    b.ToTable("followedstream", (string)null);
 | 
					                    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 =>
 | 
					            modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiAltSetting", b =>
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    b.Property<int>("Id")
 | 
					                    b.Property<int>("Id")
 | 
				
			||||||
@@ -1194,7 +1262,9 @@ namespace NadekoBot.Migrations.PostgreSql
 | 
				
			|||||||
                        .HasColumnName("timezoneid");
 | 
					                        .HasColumnName("timezoneid");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<bool>("VerboseErrors")
 | 
					                    b.Property<bool>("VerboseErrors")
 | 
				
			||||||
 | 
					                        .ValueGeneratedOnAdd()
 | 
				
			||||||
                        .HasColumnType("boolean")
 | 
					                        .HasColumnType("boolean")
 | 
				
			||||||
 | 
					                        .HasDefaultValue(true)
 | 
				
			||||||
                        .HasColumnName("verboseerrors");
 | 
					                        .HasColumnName("verboseerrors");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<bool>("VerbosePermissions")
 | 
					                    b.Property<bool>("VerbosePermissions")
 | 
				
			||||||
@@ -2058,8 +2128,8 @@ namespace NadekoBot.Migrations.PostgreSql
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
 | 
					                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<int>("AmountRewardedThisMonth")
 | 
					                    b.Property<long>("AmountRewardedThisMonth")
 | 
				
			||||||
                        .HasColumnType("integer")
 | 
					                        .HasColumnType("bigint")
 | 
				
			||||||
                        .HasColumnName("amountrewardedthismonth");
 | 
					                        .HasColumnName("amountrewardedthismonth");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<DateTime?>("DateAdded")
 | 
					                    b.Property<DateTime?>("DateAdded")
 | 
				
			||||||
@@ -2070,9 +2140,9 @@ namespace NadekoBot.Migrations.PostgreSql
 | 
				
			|||||||
                        .HasColumnType("timestamp with time zone")
 | 
					                        .HasColumnType("timestamp with time zone")
 | 
				
			||||||
                        .HasColumnName("lastreward");
 | 
					                        .HasColumnName("lastreward");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<string>("PatreonUserId")
 | 
					                    b.Property<string>("PlatformUserId")
 | 
				
			||||||
                        .HasColumnType("text")
 | 
					                        .HasColumnType("text")
 | 
				
			||||||
                        .HasColumnName("patreonuserid");
 | 
					                        .HasColumnName("platformuserid");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<decimal>("UserId")
 | 
					                    b.Property<decimal>("UserId")
 | 
				
			||||||
                        .HasColumnType("numeric(20,0)")
 | 
					                        .HasColumnType("numeric(20,0)")
 | 
				
			||||||
@@ -2081,9 +2151,9 @@ namespace NadekoBot.Migrations.PostgreSql
 | 
				
			|||||||
                    b.HasKey("Id")
 | 
					                    b.HasKey("Id")
 | 
				
			||||||
                        .HasName("pk_rewardedusers");
 | 
					                        .HasName("pk_rewardedusers");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.HasIndex("PatreonUserId")
 | 
					                    b.HasIndex("PlatformUserId")
 | 
				
			||||||
                        .IsUnique()
 | 
					                        .IsUnique()
 | 
				
			||||||
                        .HasDatabaseName("ix_rewardedusers_patreonuserid");
 | 
					                        .HasDatabaseName("ix_rewardedusers_platformuserid");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.ToTable("rewardedusers", (string)null);
 | 
					                    b.ToTable("rewardedusers", (string)null);
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
@@ -2526,8 +2596,8 @@ namespace NadekoBot.Migrations.PostgreSql
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
 | 
					                    NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property<int>("Id"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<int>("AwardedXp")
 | 
					                    b.Property<long>("AwardedXp")
 | 
				
			||||||
                        .HasColumnType("integer")
 | 
					                        .HasColumnType("bigint")
 | 
				
			||||||
                        .HasColumnName("awardedxp");
 | 
					                        .HasColumnName("awardedxp");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<DateTime?>("DateAdded")
 | 
					                    b.Property<DateTime?>("DateAdded")
 | 
				
			||||||
@@ -2552,8 +2622,8 @@ namespace NadekoBot.Migrations.PostgreSql
 | 
				
			|||||||
                        .HasColumnType("numeric(20,0)")
 | 
					                        .HasColumnType("numeric(20,0)")
 | 
				
			||||||
                        .HasColumnName("userid");
 | 
					                        .HasColumnName("userid");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<int>("Xp")
 | 
					                    b.Property<long>("Xp")
 | 
				
			||||||
                        .HasColumnType("integer")
 | 
					                        .HasColumnType("bigint")
 | 
				
			||||||
                        .HasColumnName("xp");
 | 
					                        .HasColumnName("xp");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.HasKey("Id")
 | 
					                    b.HasKey("Id")
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -87,7 +87,7 @@ namespace NadekoBot.Migrations
 | 
				
			|||||||
                name: "VoicePresenceChannelId",
 | 
					                name: "VoicePresenceChannelId",
 | 
				
			||||||
                table: "LogSettings");
 | 
					                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.Sql("UPDATE GuildConfigs SET LogSettingId = null WHERE LogSettingId NOT IN (SELECT Id from LogSettings)");
 | 
				
			||||||
            
 | 
					            
 | 
				
			||||||
            migrationBuilder.DropTable(
 | 
					            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)
 | 
					        protected override void BuildModel(ModelBuilder modelBuilder)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
#pragma warning disable 612, 618
 | 
					#pragma warning disable 612, 618
 | 
				
			||||||
            modelBuilder.HasAnnotation("ProductVersion", "6.0.3");
 | 
					            modelBuilder.HasAnnotation("ProductVersion", "6.0.5");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            modelBuilder.Entity("NadekoBot.Db.Models.BankUser", b =>
 | 
					            modelBuilder.Entity("NadekoBot.Db.Models.BankUser", b =>
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
@@ -149,10 +149,10 @@ namespace NadekoBot.Migrations
 | 
				
			|||||||
                        .HasColumnType("INTEGER")
 | 
					                        .HasColumnType("INTEGER")
 | 
				
			||||||
                        .HasDefaultValue(0);
 | 
					                        .HasDefaultValue(0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<int>("TotalXp")
 | 
					                    b.Property<long>("TotalXp")
 | 
				
			||||||
                        .ValueGeneratedOnAdd()
 | 
					                        .ValueGeneratedOnAdd()
 | 
				
			||||||
                        .HasColumnType("INTEGER")
 | 
					                        .HasColumnType("INTEGER")
 | 
				
			||||||
                        .HasDefaultValue(0);
 | 
					                        .HasDefaultValue(0L);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<ulong>("UserId")
 | 
					                    b.Property<ulong>("UserId")
 | 
				
			||||||
                        .HasColumnType("INTEGER");
 | 
					                        .HasColumnType("INTEGER");
 | 
				
			||||||
@@ -209,6 +209,59 @@ namespace NadekoBot.Migrations
 | 
				
			|||||||
                    b.ToTable("FollowedStream");
 | 
					                    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 =>
 | 
					            modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiAltSetting", b =>
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    b.Property<int>("Id")
 | 
					                    b.Property<int>("Id")
 | 
				
			||||||
@@ -890,7 +943,9 @@ namespace NadekoBot.Migrations
 | 
				
			|||||||
                        .HasColumnType("TEXT");
 | 
					                        .HasColumnType("TEXT");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<bool>("VerboseErrors")
 | 
					                    b.Property<bool>("VerboseErrors")
 | 
				
			||||||
                        .HasColumnType("INTEGER");
 | 
					                        .ValueGeneratedOnAdd()
 | 
				
			||||||
 | 
					                        .HasColumnType("INTEGER")
 | 
				
			||||||
 | 
					                        .HasDefaultValue(true);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<bool>("VerbosePermissions")
 | 
					                    b.Property<bool>("VerbosePermissions")
 | 
				
			||||||
                        .HasColumnType("INTEGER");
 | 
					                        .HasColumnType("INTEGER");
 | 
				
			||||||
@@ -1531,7 +1586,7 @@ namespace NadekoBot.Migrations
 | 
				
			|||||||
                        .ValueGeneratedOnAdd()
 | 
					                        .ValueGeneratedOnAdd()
 | 
				
			||||||
                        .HasColumnType("INTEGER");
 | 
					                        .HasColumnType("INTEGER");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<int>("AmountRewardedThisMonth")
 | 
					                    b.Property<long>("AmountRewardedThisMonth")
 | 
				
			||||||
                        .HasColumnType("INTEGER");
 | 
					                        .HasColumnType("INTEGER");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<DateTime?>("DateAdded")
 | 
					                    b.Property<DateTime?>("DateAdded")
 | 
				
			||||||
@@ -1540,7 +1595,7 @@ namespace NadekoBot.Migrations
 | 
				
			|||||||
                    b.Property<DateTime>("LastReward")
 | 
					                    b.Property<DateTime>("LastReward")
 | 
				
			||||||
                        .HasColumnType("TEXT");
 | 
					                        .HasColumnType("TEXT");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<string>("PatreonUserId")
 | 
					                    b.Property<string>("PlatformUserId")
 | 
				
			||||||
                        .HasColumnType("TEXT");
 | 
					                        .HasColumnType("TEXT");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<ulong>("UserId")
 | 
					                    b.Property<ulong>("UserId")
 | 
				
			||||||
@@ -1548,7 +1603,7 @@ namespace NadekoBot.Migrations
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
                    b.HasKey("Id");
 | 
					                    b.HasKey("Id");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.HasIndex("PatreonUserId")
 | 
					                    b.HasIndex("PlatformUserId")
 | 
				
			||||||
                        .IsUnique();
 | 
					                        .IsUnique();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.ToTable("RewardedUsers");
 | 
					                    b.ToTable("RewardedUsers");
 | 
				
			||||||
@@ -1877,7 +1932,7 @@ namespace NadekoBot.Migrations
 | 
				
			|||||||
                        .ValueGeneratedOnAdd()
 | 
					                        .ValueGeneratedOnAdd()
 | 
				
			||||||
                        .HasColumnType("INTEGER");
 | 
					                        .HasColumnType("INTEGER");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<int>("AwardedXp")
 | 
					                    b.Property<long>("AwardedXp")
 | 
				
			||||||
                        .HasColumnType("INTEGER");
 | 
					                        .HasColumnType("INTEGER");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<DateTime?>("DateAdded")
 | 
					                    b.Property<DateTime?>("DateAdded")
 | 
				
			||||||
@@ -1897,7 +1952,7 @@ namespace NadekoBot.Migrations
 | 
				
			|||||||
                    b.Property<ulong>("UserId")
 | 
					                    b.Property<ulong>("UserId")
 | 
				
			||||||
                        .HasColumnType("INTEGER");
 | 
					                        .HasColumnType("INTEGER");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.Property<int>("Xp")
 | 
					                    b.Property<long>("Xp")
 | 
				
			||||||
                        .HasColumnType("INTEGER");
 | 
					                        .HasColumnType("INTEGER");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    b.HasKey("Id");
 | 
					                    b.HasKey("Id");
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,6 +23,8 @@ public class DangerousCommandsService : INService
 | 
				
			|||||||
            // IsClubAdmin = false,
 | 
					            // IsClubAdmin = false,
 | 
				
			||||||
            TotalXp = 0
 | 
					            TotalXp = 0
 | 
				
			||||||
        });
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await ctx.UserXpStats.DeleteAsync();
 | 
				
			||||||
        await ctx.ClubApplicants.DeleteAsync();
 | 
					        await ctx.ClubApplicants.DeleteAsync();
 | 
				
			||||||
        await ctx.ClubBans.DeleteAsync();
 | 
					        await ctx.ClubBans.DeleteAsync();
 | 
				
			||||||
        await ctx.Clubs.DeleteAsync();
 | 
					        await ctx.Clubs.DeleteAsync();
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -23,12 +23,13 @@ public class PruneService : INService
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        try
 | 
					        try
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
 | 
					            var now = DateTime.UtcNow;
 | 
				
			||||||
            IMessage[] msgs;
 | 
					            IMessage[] msgs;
 | 
				
			||||||
            IMessage lastMessage = null;
 | 
					            IMessage lastMessage = null;
 | 
				
			||||||
            msgs = (await channel.GetMessagesAsync(50).FlattenAsync()).Where(predicate).Take(amount).ToArray();
 | 
					            msgs = (await channel.GetMessagesAsync(50).FlattenAsync()).Where(predicate).Take(amount).ToArray();
 | 
				
			||||||
            while (amount > 0 && msgs.Any())
 | 
					            while (amount > 0 && msgs.Any())
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                lastMessage = msgs[msgs.Length - 1];
 | 
					                lastMessage = msgs[^1];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                var bulkDeletable = new List<IMessage>();
 | 
					                var bulkDeletable = new List<IMessage>();
 | 
				
			||||||
                var singleDeletable = new List<IMessage>();
 | 
					                var singleDeletable = new List<IMessage>();
 | 
				
			||||||
@@ -36,17 +37,23 @@ public class PruneService : INService
 | 
				
			|||||||
                {
 | 
					                {
 | 
				
			||||||
                    _logService.AddDeleteIgnore(x.Id);
 | 
					                    _logService.AddDeleteIgnore(x.Id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                    if (DateTime.UtcNow - x.CreatedAt < _twoWeeks)
 | 
					                    if (now - x.CreatedAt < _twoWeeks)
 | 
				
			||||||
                        bulkDeletable.Add(x);
 | 
					                        bulkDeletable.Add(x);
 | 
				
			||||||
                    else
 | 
					                    else
 | 
				
			||||||
                        singleDeletable.Add(x);
 | 
					                        singleDeletable.Add(x);
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                if (bulkDeletable.Count > 0)
 | 
					                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))
 | 
					                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
 | 
					                //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
 | 
					                //100 messages, Maybe this needs to be reduced by msgs.Length instead of 100
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,8 @@
 | 
				
			|||||||
#nullable disable
 | 
					#nullable disable
 | 
				
			||||||
 | 
					using NadekoBot.Modules.Utility.Patronage;
 | 
				
			||||||
using NadekoBot.Services.Database.Models;
 | 
					using NadekoBot.Services.Database.Models;
 | 
				
			||||||
using System.Collections;
 | 
					using OneOf;
 | 
				
			||||||
 | 
					using OneOf.Types;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace NadekoBot.Modules.Administration.Services;
 | 
					namespace NadekoBot.Modules.Administration.Services;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -9,18 +11,16 @@ public interface IReactionRoleService
 | 
				
			|||||||
    /// <summary>
 | 
					    /// <summary>
 | 
				
			||||||
    /// Adds a single reaction role
 | 
					    /// Adds a single reaction role
 | 
				
			||||||
    /// </summary>
 | 
					    /// </summary>
 | 
				
			||||||
    /// <param name="guildId"></param>
 | 
					    /// <param name="guild">Guild where to add a reaction role</param>
 | 
				
			||||||
    /// <param name="msg"></param>
 | 
					    /// <param name="msg">Message to which to add a reaction role</param>
 | 
				
			||||||
    /// <param name="channel"></param>
 | 
					 | 
				
			||||||
    /// <param name="emote"></param>
 | 
					    /// <param name="emote"></param>
 | 
				
			||||||
    /// <param name="role"></param>
 | 
					    /// <param name="role"></param>
 | 
				
			||||||
    /// <param name="group"></param>
 | 
					    /// <param name="group"></param>
 | 
				
			||||||
    /// <param name="levelReq"></param>
 | 
					    /// <param name="levelReq"></param>
 | 
				
			||||||
    /// <returns></returns>
 | 
					    /// <returns>The result of the operation</returns>
 | 
				
			||||||
    Task<bool> AddReactionRole(
 | 
					    Task<OneOf<Success, FeatureLimit>> AddReactionRole(
 | 
				
			||||||
        ulong guildId,
 | 
					        IGuild guild,
 | 
				
			||||||
        IMessage msg,
 | 
					        IMessage msg,
 | 
				
			||||||
        ITextChannel channel,
 | 
					 | 
				
			||||||
        string emote,
 | 
					        string emote,
 | 
				
			||||||
        IRole role,
 | 
					        IRole role,
 | 
				
			||||||
        int group = 0,
 | 
					        int group = 0,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,7 +15,6 @@ public partial class Administration
 | 
				
			|||||||
        
 | 
					        
 | 
				
			||||||
        [Cmd]
 | 
					        [Cmd]
 | 
				
			||||||
        [RequireContext(ContextType.Guild)]
 | 
					        [RequireContext(ContextType.Guild)]
 | 
				
			||||||
        [NoPublicBot]
 | 
					 | 
				
			||||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
					        [UserPerm(GuildPerm.ManageRoles)]
 | 
				
			||||||
        [BotPerm(GuildPerm.ManageRoles)]
 | 
					        [BotPerm(GuildPerm.ManageRoles)]
 | 
				
			||||||
        public async partial Task ReactionRoleAdd(
 | 
					        public async partial Task ReactionRoleAdd(
 | 
				
			||||||
@@ -46,27 +45,26 @@ public partial class Administration
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
            var emote = emoteStr.ToIEmote();
 | 
					            var emote = emoteStr.ToIEmote();
 | 
				
			||||||
            await msg.AddReactionAsync(emote);
 | 
					            await msg.AddReactionAsync(emote);
 | 
				
			||||||
            var succ = await _rero.AddReactionRole(ctx.Guild.Id,
 | 
					            var res = await _rero.AddReactionRole(ctx.Guild,
 | 
				
			||||||
                msg,
 | 
					                msg,
 | 
				
			||||||
                (ITextChannel)ctx.Channel,
 | 
					 | 
				
			||||||
                emoteStr,
 | 
					                emoteStr,
 | 
				
			||||||
                role,
 | 
					                role,
 | 
				
			||||||
                group,
 | 
					                group,
 | 
				
			||||||
                levelReq);
 | 
					                levelReq);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if (succ)
 | 
					            await res.Match(
 | 
				
			||||||
            {
 | 
					                _ => ctx.OkAsync(),
 | 
				
			||||||
                await ctx.OkAsync();
 | 
					                fl =>
 | 
				
			||||||
            }
 | 
					                {
 | 
				
			||||||
            else
 | 
					                    _ = msg.RemoveReactionAsync(emote, ctx.Client.CurrentUser);
 | 
				
			||||||
            {
 | 
					                    return !fl.IsPatronLimit
 | 
				
			||||||
                await ctx.ErrorAsync();
 | 
					                        ? ReplyErrorLocalizedAsync(strs.limit_reached(fl.Quota))
 | 
				
			||||||
            }
 | 
					                        : ReplyPendingLocalizedAsync(strs.feature_limit_reached_owner(fl.Quota, fl.Name));
 | 
				
			||||||
 | 
					                });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [Cmd]
 | 
					        [Cmd]
 | 
				
			||||||
        [RequireContext(ContextType.Guild)]
 | 
					        [RequireContext(ContextType.Guild)]
 | 
				
			||||||
        [NoPublicBot]
 | 
					 | 
				
			||||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
					        [UserPerm(GuildPerm.ManageRoles)]
 | 
				
			||||||
        [BotPerm(GuildPerm.ManageRoles)]
 | 
					        [BotPerm(GuildPerm.ManageRoles)]
 | 
				
			||||||
        public async partial Task ReactionRolesList()
 | 
					        public async partial Task ReactionRolesList()
 | 
				
			||||||
@@ -109,7 +107,6 @@ public partial class Administration
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        [Cmd]
 | 
					        [Cmd]
 | 
				
			||||||
        [RequireContext(ContextType.Guild)]
 | 
					        [RequireContext(ContextType.Guild)]
 | 
				
			||||||
        [NoPublicBot]
 | 
					 | 
				
			||||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
					        [UserPerm(GuildPerm.ManageRoles)]
 | 
				
			||||||
        [BotPerm(GuildPerm.ManageRoles)]
 | 
					        [BotPerm(GuildPerm.ManageRoles)]
 | 
				
			||||||
        public async partial Task ReactionRolesRemove(ulong messageId)
 | 
					        public async partial Task ReactionRolesRemove(ulong messageId)
 | 
				
			||||||
@@ -123,7 +120,6 @@ public partial class Administration
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        [Cmd]
 | 
					        [Cmd]
 | 
				
			||||||
        [RequireContext(ContextType.Guild)]
 | 
					        [RequireContext(ContextType.Guild)]
 | 
				
			||||||
        [NoPublicBot]
 | 
					 | 
				
			||||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
					        [UserPerm(GuildPerm.ManageRoles)]
 | 
				
			||||||
        [BotPerm(GuildPerm.ManageRoles)]
 | 
					        [BotPerm(GuildPerm.ManageRoles)]
 | 
				
			||||||
        public async partial Task ReactionRolesDeleteAll()
 | 
					        public async partial Task ReactionRolesDeleteAll()
 | 
				
			||||||
@@ -134,7 +130,6 @@ public partial class Administration
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        [Cmd]
 | 
					        [Cmd]
 | 
				
			||||||
        [RequireContext(ContextType.Guild)]
 | 
					        [RequireContext(ContextType.Guild)]
 | 
				
			||||||
        [NoPublicBot]
 | 
					 | 
				
			||||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
					        [UserPerm(GuildPerm.ManageRoles)]
 | 
				
			||||||
        [BotPerm(GuildPerm.ManageRoles)]
 | 
					        [BotPerm(GuildPerm.ManageRoles)]
 | 
				
			||||||
        [Ratelimit(60)]
 | 
					        [Ratelimit(60)]
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,8 +2,11 @@
 | 
				
			|||||||
using LinqToDB;
 | 
					using LinqToDB;
 | 
				
			||||||
using LinqToDB.EntityFrameworkCore;
 | 
					using LinqToDB.EntityFrameworkCore;
 | 
				
			||||||
using NadekoBot.Common.ModuleBehaviors;
 | 
					using NadekoBot.Common.ModuleBehaviors;
 | 
				
			||||||
 | 
					using NadekoBot.Modules.Utility.Patronage;
 | 
				
			||||||
using NadekoBot.Modules.Xp.Extensions;
 | 
					using NadekoBot.Modules.Xp.Extensions;
 | 
				
			||||||
using NadekoBot.Services.Database.Models;
 | 
					using NadekoBot.Services.Database.Models;
 | 
				
			||||||
 | 
					using OneOf.Types;
 | 
				
			||||||
 | 
					using OneOf;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace NadekoBot.Modules.Administration.Services;
 | 
					namespace NadekoBot.Modules.Administration.Services;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -16,10 +19,22 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
 | 
				
			|||||||
    private ConcurrentDictionary<ulong, List<ReactionRoleV2>> _cache;
 | 
					    private ConcurrentDictionary<ulong, List<ReactionRoleV2>> _cache;
 | 
				
			||||||
    private readonly object _cacheLock = new();
 | 
					    private readonly object _cacheLock = new();
 | 
				
			||||||
    private readonly SemaphoreSlim _assignementLock = new(1, 1);
 | 
					    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;
 | 
					        _db = db;
 | 
				
			||||||
 | 
					        _ps = ps;
 | 
				
			||||||
        _client = client;
 | 
					        _client = client;
 | 
				
			||||||
        _creds = creds;
 | 
					        _creds = creds;
 | 
				
			||||||
        _cache = new();
 | 
					        _cache = new();
 | 
				
			||||||
@@ -29,7 +44,8 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
 | 
				
			|||||||
    {
 | 
					    {
 | 
				
			||||||
        await using var uow = _db.GetDbContext();
 | 
					        await using var uow = _db.GetDbContext();
 | 
				
			||||||
        var reros = await uow.GetTable<ReactionRoleV2>()
 | 
					        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();
 | 
					                             .ToListAsyncLinqToDB();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        foreach (var group in reros.GroupBy(x => x.MessageId))
 | 
					        foreach (var group in reros.GroupBy(x => x.MessageId))
 | 
				
			||||||
@@ -126,7 +142,7 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
 | 
				
			|||||||
                    {
 | 
					                    {
 | 
				
			||||||
                        await using var ctx = _db.GetDbContext();
 | 
					                        await using var ctx = _db.GetDbContext();
 | 
				
			||||||
                        var levelData = await ctx.GetTable<UserXpStats>()
 | 
					                        var levelData = await ctx.GetTable<UserXpStats>()
 | 
				
			||||||
                                 .GetLevelDataFor(user.GuildId, user.Id);
 | 
					                                                 .GetLevelDataFor(user.GuildId, user.Id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        if (levelData.Level < rero.LevelReq)
 | 
					                        if (levelData.Level < rero.LevelReq)
 | 
				
			||||||
                            return;
 | 
					                            return;
 | 
				
			||||||
@@ -181,18 +197,16 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
 | 
				
			|||||||
    /// <summary>
 | 
					    /// <summary>
 | 
				
			||||||
    /// Adds a single reaction role
 | 
					    /// Adds a single reaction role
 | 
				
			||||||
    /// </summary>
 | 
					    /// </summary>
 | 
				
			||||||
    /// <param name="guildId"></param>
 | 
					    /// <param name="guild">Guild where to add a reaction role</param>
 | 
				
			||||||
    /// <param name="msg"></param>
 | 
					    /// <param name="msg">Message to which to add a reaction role</param>
 | 
				
			||||||
    /// <param name="channel"></param>
 | 
					 | 
				
			||||||
    /// <param name="emote"></param>
 | 
					    /// <param name="emote"></param>
 | 
				
			||||||
    /// <param name="role"></param>
 | 
					    /// <param name="role"></param>
 | 
				
			||||||
    /// <param name="group"></param>
 | 
					    /// <param name="group"></param>
 | 
				
			||||||
    /// <param name="levelReq"></param>
 | 
					    /// <param name="levelReq"></param>
 | 
				
			||||||
    /// <returns></returns>
 | 
					    /// <returns>The result of the operation</returns>
 | 
				
			||||||
    public async Task<bool> AddReactionRole(
 | 
					    public async Task<OneOf<Success, FeatureLimit>> AddReactionRole(
 | 
				
			||||||
        ulong guildId,
 | 
					        IGuild guild,
 | 
				
			||||||
        IMessage msg,
 | 
					        IMessage msg,
 | 
				
			||||||
        ITextChannel channel,
 | 
					 | 
				
			||||||
        string emote,
 | 
					        string emote,
 | 
				
			||||||
        IRole role,
 | 
					        IRole role,
 | 
				
			||||||
        int group = 0,
 | 
					        int group = 0,
 | 
				
			||||||
@@ -205,44 +219,46 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
 | 
				
			|||||||
            throw new ArgumentOutOfRangeException(nameof(group));
 | 
					            throw new ArgumentOutOfRangeException(nameof(group));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        await using var ctx = _db.GetDbContext();
 | 
					        await using var ctx = _db.GetDbContext();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        await using var tran = await ctx.Database.BeginTransactionAsync();
 | 
				
			||||||
        var activeReactionRoles = await ctx.GetTable<ReactionRoleV2>()
 | 
					        var activeReactionRoles = await ctx.GetTable<ReactionRoleV2>()
 | 
				
			||||||
                                           .Where(x => x.GuildId == guildId)
 | 
					                                           .Where(x => x.GuildId == guild.Id)
 | 
				
			||||||
                                           .CountAsync();
 | 
					                                           .CountAsync();
 | 
				
			||||||
        
 | 
					        
 | 
				
			||||||
        if (activeReactionRoles >= 50)
 | 
					        var result = await _ps.TryGetFeatureLimitAsync(_reroFLKey, guild.OwnerId, 50);
 | 
				
			||||||
            return false;
 | 
					        if (result.Quota != -1 && activeReactionRoles >= result.Quota)
 | 
				
			||||||
 | 
					            return result;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var changed = await ctx.GetTable<ReactionRoleV2>()
 | 
					        await ctx.GetTable<ReactionRoleV2>()
 | 
				
			||||||
                               .InsertOrUpdateAsync(() => new()
 | 
					                 .InsertOrUpdateAsync(() => new()
 | 
				
			||||||
                                   {
 | 
					                     {
 | 
				
			||||||
                                       GuildId = guildId,
 | 
					                         GuildId = guild.Id,
 | 
				
			||||||
                                       ChannelId = channel.Id,
 | 
					                         ChannelId = msg.Channel.Id,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                                       MessageId = msg.Id,
 | 
					                         MessageId = msg.Id,
 | 
				
			||||||
                                       Emote = emote,
 | 
					                         Emote = emote,
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                                       RoleId = role.Id,
 | 
					                         RoleId = role.Id,
 | 
				
			||||||
                                       Group = group,
 | 
					                         Group = group,
 | 
				
			||||||
                                       LevelReq = levelReq
 | 
					                         LevelReq = levelReq
 | 
				
			||||||
                                   },
 | 
					                     },
 | 
				
			||||||
                                   (old) => new()
 | 
					                     (old) => new()
 | 
				
			||||||
                                   {
 | 
					                     {
 | 
				
			||||||
                                       RoleId = role.Id,
 | 
					                         RoleId = role.Id,
 | 
				
			||||||
                                       Group = group,
 | 
					                         Group = group,
 | 
				
			||||||
                                       LevelReq = levelReq
 | 
					                         LevelReq = levelReq
 | 
				
			||||||
                                   },
 | 
					                     },
 | 
				
			||||||
                                   () => new()
 | 
					                     () => new()
 | 
				
			||||||
                                   {
 | 
					                     {
 | 
				
			||||||
                                       MessageId = msg.Id,
 | 
					                         MessageId = msg.Id,
 | 
				
			||||||
                                       Emote = emote,
 | 
					                         Emote = emote,
 | 
				
			||||||
                                   });
 | 
					                     });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (changed == 0)
 | 
					        await tran.CommitAsync();
 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var obj = new ReactionRoleV2()
 | 
					        var obj = new ReactionRoleV2()
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            GuildId = guildId,
 | 
					            GuildId = guild.Id,
 | 
				
			||||||
            MessageId = msg.Id,
 | 
					            MessageId = msg.Id,
 | 
				
			||||||
            Emote = emote,
 | 
					            Emote = emote,
 | 
				
			||||||
            RoleId = role.Id,
 | 
					            RoleId = role.Id,
 | 
				
			||||||
@@ -265,7 +281,7 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
 | 
				
			|||||||
                });
 | 
					                });
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return true;
 | 
					        return new Success();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    /// <summary>
 | 
					    /// <summary>
 | 
				
			||||||
@@ -326,7 +342,10 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
 | 
				
			|||||||
        return output.Length;
 | 
					        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();
 | 
					        await using var ctx = _db.GetDbContext();
 | 
				
			||||||
        var updated = ctx.GetTable<ReactionRoleV2>()
 | 
					        var updated = ctx.GetTable<ReactionRoleV2>()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -149,7 +149,7 @@ public partial class Administration
 | 
				
			|||||||
        [RequireContext(ContextType.Guild)]
 | 
					        [RequireContext(ContextType.Guild)]
 | 
				
			||||||
        [UserPerm(GuildPerm.ManageRoles)]
 | 
					        [UserPerm(GuildPerm.ManageRoles)]
 | 
				
			||||||
        [BotPerm(GuildPerm.ManageRoles)]
 | 
					        [BotPerm(GuildPerm.ManageRoles)]
 | 
				
			||||||
        public async partial Task RoleHoist(IRole role)
 | 
					        public async partial Task RoleHoist([Leftover] IRole role)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var newHoisted = !role.IsHoisted;
 | 
					            var newHoisted = !role.IsHoisted;
 | 
				
			||||||
            await role.ModifyAsync(r => r.Hoist = newHoisted);
 | 
					            await role.ModifyAsync(r => r.Hoist = newHoisted);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -65,7 +65,7 @@ public partial class Gambling
 | 
				
			|||||||
            }
 | 
					            }
 | 
				
			||||||
            catch
 | 
					            catch
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                await ReplyErrorLocalizedAsync(strs.unable_to_dm_user);
 | 
					                await ReplyErrorLocalizedAsync(strs.cant_dm);
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,6 @@
 | 
				
			|||||||
using LinqToDB;
 | 
					using LinqToDB;
 | 
				
			||||||
using LinqToDB.EntityFrameworkCore;
 | 
					using LinqToDB.EntityFrameworkCore;
 | 
				
			||||||
 | 
					using NadekoBot.Db.Models;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace NadekoBot.Modules.Gambling.Bank;
 | 
					namespace NadekoBot.Modules.Gambling.Bank;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -74,4 +75,19 @@ public sealed class BankService : IBankService, INService
 | 
				
			|||||||
               ?.Balance
 | 
					               ?.Balance
 | 
				
			||||||
               ?? 0;
 | 
					               ?? 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> DepositAsync(ulong userId, long amount);
 | 
				
			||||||
    Task<bool> WithdrawAsync(ulong userId, long amount);
 | 
					    Task<bool> WithdrawAsync(ulong userId, long amount);
 | 
				
			||||||
    Task<long> GetBalanceAsync(ulong userId);
 | 
					    Task<long> GetBalanceAsync(ulong userId);
 | 
				
			||||||
 | 
					    Task<long> BurnAllAsync(ulong userId);
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -1,17 +1,13 @@
 | 
				
			|||||||
#nullable disable
 | 
					#nullable disable
 | 
				
			||||||
namespace NadekoBot.Modules.Gambling;
 | 
					namespace NadekoBot.Modules.Gambling;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
public class CashInteraction
 | 
					public class CashInteraction : NInteraction
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    public static NadekoInteractionData Data =
 | 
					    protected override NadekoInteractionData Data
 | 
				
			||||||
        new NadekoInteractionData(new Emoji("🏦"), "cash:bank_show_balance");
 | 
					        => new NadekoInteractionData(new Emoji("🏦"), "cash:bank_show_balance");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public static NadekoInteraction CreateInstance(
 | 
					    public CashInteraction(DiscordSocketClient client, ulong userId, Func<SocketMessageComponent, Task> action)
 | 
				
			||||||
        DiscordSocketClient client,
 | 
					        : base(client, userId, action)
 | 
				
			||||||
        ulong userId,
 | 
					    {
 | 
				
			||||||
        Func<SocketMessageComponent, Task> action)
 | 
					    }
 | 
				
			||||||
        => new NadekoInteractionBuilder()
 | 
					 | 
				
			||||||
           .WithData(Data)
 | 
					 | 
				
			||||||
           .WithAction(action)
 | 
					 | 
				
			||||||
           .Build(client, userId);
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -3,11 +3,13 @@ using LinqToDB;
 | 
				
			|||||||
using LinqToDB.EntityFrameworkCore;
 | 
					using LinqToDB.EntityFrameworkCore;
 | 
				
			||||||
using NadekoBot.Db;
 | 
					using NadekoBot.Db;
 | 
				
			||||||
using NadekoBot.Db.Models;
 | 
					using NadekoBot.Db.Models;
 | 
				
			||||||
 | 
					using NadekoBot.Modules.Utility.Patronage;
 | 
				
			||||||
using NadekoBot.Modules.Gambling.Bank;
 | 
					using NadekoBot.Modules.Gambling.Bank;
 | 
				
			||||||
using NadekoBot.Modules.Gambling.Common;
 | 
					using NadekoBot.Modules.Gambling.Common;
 | 
				
			||||||
using NadekoBot.Modules.Gambling.Services;
 | 
					using NadekoBot.Modules.Gambling.Services;
 | 
				
			||||||
using NadekoBot.Services.Currency;
 | 
					using NadekoBot.Services.Currency;
 | 
				
			||||||
using NadekoBot.Services.Database.Models;
 | 
					using NadekoBot.Services.Database.Models;
 | 
				
			||||||
 | 
					using System.Collections.Immutable;
 | 
				
			||||||
using System.Globalization;
 | 
					using System.Globalization;
 | 
				
			||||||
using System.Text;
 | 
					using System.Text;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -42,6 +44,7 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
				
			|||||||
    private readonly DownloadTracker _tracker;
 | 
					    private readonly DownloadTracker _tracker;
 | 
				
			||||||
    private readonly GamblingConfigService _configService;
 | 
					    private readonly GamblingConfigService _configService;
 | 
				
			||||||
    private readonly IBankService _bank;
 | 
					    private readonly IBankService _bank;
 | 
				
			||||||
 | 
					    private readonly IPatronageService _ps;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private IUserMessage rdMsg;
 | 
					    private IUserMessage rdMsg;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -52,7 +55,8 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
				
			|||||||
        DiscordSocketClient client,
 | 
					        DiscordSocketClient client,
 | 
				
			||||||
        DownloadTracker tracker,
 | 
					        DownloadTracker tracker,
 | 
				
			||||||
        GamblingConfigService configService,
 | 
					        GamblingConfigService configService,
 | 
				
			||||||
        IBankService bank)
 | 
					        IBankService bank,
 | 
				
			||||||
 | 
					        IPatronageService ps)
 | 
				
			||||||
        : base(configService)
 | 
					        : base(configService)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        _db = db;
 | 
					        _db = db;
 | 
				
			||||||
@@ -60,6 +64,7 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
				
			|||||||
        _cache = cache;
 | 
					        _cache = cache;
 | 
				
			||||||
        _client = client;
 | 
					        _client = client;
 | 
				
			||||||
        _bank = bank;
 | 
					        _bank = bank;
 | 
				
			||||||
 | 
					        _ps = ps;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        _enUsCulture = new CultureInfo("en-US", false).NumberFormat;
 | 
					        _enUsCulture = new CultureInfo("en-US", false).NumberFormat;
 | 
				
			||||||
        _enUsCulture.NumberDecimalDigits = 0;
 | 
					        _enUsCulture.NumberDecimalDigits = 0;
 | 
				
			||||||
@@ -102,6 +107,12 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
				
			|||||||
        await ctx.Channel.EmbedAsync(embed);
 | 
					        await ctx.Channel.EmbedAsync(embed);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private static readonly FeatureLimitKey _timelyKey = new FeatureLimitKey()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Key = "timely:extra_percent",
 | 
				
			||||||
 | 
					        PrettyName = "Timely"
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
    [Cmd]
 | 
					    [Cmd]
 | 
				
			||||||
    public async partial Task Timely()
 | 
					    public async partial Task Timely()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
@@ -119,6 +130,10 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
				
			|||||||
            return;
 | 
					            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 _cs.AddAsync(ctx.User.Id, val, new("timely", "claim"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        await ReplyConfirmLocalizedAsync(strs.timely(N(val), period));
 | 
					        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));
 | 
					              .Pipe(text => smc.RespondConfirmAsync(_eb, text, ephemeral: true));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private NadekoInteraction CreateCashInteraction()
 | 
					    private NadekoButtonInteraction CreateCashInteraction()
 | 
				
			||||||
        => CashInteraction.CreateInstance(_client, ctx.User.Id, BankAction);
 | 
					        => new CashInteraction(_client, ctx.User.Id, BankAction).GetInteraction();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    [Cmd]
 | 
					    [Cmd]
 | 
				
			||||||
    [Priority(1)]
 | 
					    [Priority(1)]
 | 
				
			||||||
@@ -780,4 +795,31 @@ public partial class Gambling : GamblingModule<GamblingService>
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        await ctx.Channel.EmbedAsync(embed);
 | 
					        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
 | 
					#nullable disable
 | 
				
			||||||
using NadekoBot.Common.ModuleBehaviors;
 | 
					using NadekoBot.Common.ModuleBehaviors;
 | 
				
			||||||
 | 
					using NadekoBot.Db.Models;
 | 
				
			||||||
using NadekoBot.Modules.Games.Common.ChatterBot;
 | 
					using NadekoBot.Modules.Games.Common.ChatterBot;
 | 
				
			||||||
 | 
					using NadekoBot.Modules.Permissions;
 | 
				
			||||||
using NadekoBot.Modules.Permissions.Common;
 | 
					using NadekoBot.Modules.Permissions.Common;
 | 
				
			||||||
using NadekoBot.Modules.Permissions.Services;
 | 
					using NadekoBot.Modules.Permissions.Services;
 | 
				
			||||||
 | 
					using NadekoBot.Modules.Utility.Patronage;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace NadekoBot.Modules.Games.Services;
 | 
					namespace NadekoBot.Modules.Games.Services;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -13,6 +16,8 @@ public class ChatterBotService : IExecOnMessage
 | 
				
			|||||||
    public int Priority
 | 
					    public int Priority
 | 
				
			||||||
        => 1;
 | 
					        => 1;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    private readonly FeatureLimitKey _flKey;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private readonly DiscordSocketClient _client;
 | 
					    private readonly DiscordSocketClient _client;
 | 
				
			||||||
    private readonly PermissionService _perms;
 | 
					    private readonly PermissionService _perms;
 | 
				
			||||||
    private readonly CommandHandler _cmd;
 | 
					    private readonly CommandHandler _cmd;
 | 
				
			||||||
@@ -20,6 +25,8 @@ public class ChatterBotService : IExecOnMessage
 | 
				
			|||||||
    private readonly IBotCredentials _creds;
 | 
					    private readonly IBotCredentials _creds;
 | 
				
			||||||
    private readonly IEmbedBuilderService _eb;
 | 
					    private readonly IEmbedBuilderService _eb;
 | 
				
			||||||
    private readonly IHttpClientFactory _httpFactory;
 | 
					    private readonly IHttpClientFactory _httpFactory;
 | 
				
			||||||
 | 
					    private readonly IPatronageService _ps;
 | 
				
			||||||
 | 
					    private readonly CmdCdService _ccs;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public ChatterBotService(
 | 
					    public ChatterBotService(
 | 
				
			||||||
        DiscordSocketClient client,
 | 
					        DiscordSocketClient client,
 | 
				
			||||||
@@ -29,7 +36,9 @@ public class ChatterBotService : IExecOnMessage
 | 
				
			|||||||
        IBotStrings strings,
 | 
					        IBotStrings strings,
 | 
				
			||||||
        IHttpClientFactory factory,
 | 
					        IHttpClientFactory factory,
 | 
				
			||||||
        IBotCredentials creds,
 | 
					        IBotCredentials creds,
 | 
				
			||||||
        IEmbedBuilderService eb)
 | 
					        IEmbedBuilderService eb,
 | 
				
			||||||
 | 
					        IPatronageService ps,
 | 
				
			||||||
 | 
					        CmdCdService cmdCdService)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        _client = client;
 | 
					        _client = client;
 | 
				
			||||||
        _perms = perms;
 | 
					        _perms = perms;
 | 
				
			||||||
@@ -38,8 +47,17 @@ public class ChatterBotService : IExecOnMessage
 | 
				
			|||||||
        _creds = creds;
 | 
					        _creds = creds;
 | 
				
			||||||
        _eb = eb;
 | 
					        _eb = eb;
 | 
				
			||||||
        _httpFactory = factory;
 | 
					        _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,
 | 
					                                  .ToDictionary(gc => gc.GuildId,
 | 
				
			||||||
                                      _ => new Lazy<IChatterBotSession>(() => CreateSession(), true)));
 | 
					                                      _ => new Lazy<IChatterBotSession>(() => CreateSession(), true)));
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -48,7 +66,9 @@ public class ChatterBotService : IExecOnMessage
 | 
				
			|||||||
    {
 | 
					    {
 | 
				
			||||||
        if (!string.IsNullOrWhiteSpace(_creds.CleverbotApiKey))
 | 
					        if (!string.IsNullOrWhiteSpace(_creds.CleverbotApiKey))
 | 
				
			||||||
            return new OfficialCleverbotSession(_creds.CleverbotApiKey, _httpFactory);
 | 
					            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)
 | 
					    public string PrepareMessage(IUserMessage msg, out IChatterBotSession cleverbot)
 | 
				
			||||||
@@ -78,27 +98,11 @@ public class ChatterBotService : IExecOnMessage
 | 
				
			|||||||
        return message;
 | 
					        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)
 | 
					    public async Task<bool> ExecOnMessageAsync(IGuild guild, IUserMessage usrMsg)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        if (guild is not SocketGuild sg)
 | 
					        if (guild is not SocketGuild sg)
 | 
				
			||||||
            return false;
 | 
					            return false;
 | 
				
			||||||
 | 
					        
 | 
				
			||||||
        try
 | 
					        try
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var message = PrepareMessage(usrMsg, out var cbs);
 | 
					            var message = PrepareMessage(usrMsg, out var cbs);
 | 
				
			||||||
@@ -106,7 +110,10 @@ public class ChatterBotService : IExecOnMessage
 | 
				
			|||||||
                return false;
 | 
					                return false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var pc = _perms.GetCacheFor(guild.Id);
 | 
					            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)
 | 
					                if (pc.Verbose)
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
@@ -122,24 +129,78 @@ public class ChatterBotService : IExecOnMessage
 | 
				
			|||||||
                return true;
 | 
					                return true;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var cleverbotExecuted = await TryAsk(cbs, (ITextChannel)usrMsg.Channel, message);
 | 
					            if (await _ccs.TryBlock(sg, usrMsg.Author, CleverBotResponseStr.CLEVERBOT_RESPONSE))
 | 
				
			||||||
            if (cleverbotExecuted)
 | 
					 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                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}]
 | 
					Server: {GuildName} [{GuildId}]
 | 
				
			||||||
Channel: {ChannelName} [{ChannelId}]
 | 
					Channel: {ChannelName} [{ChannelId}]
 | 
				
			||||||
UserId: {Author} [{AuthorId}]
 | 
					UserId: {Author} [{AuthorId}]
 | 
				
			||||||
Message: {Content}",
 | 
					Message: {Content}",
 | 
				
			||||||
                    guild.Name,
 | 
					                guild.Name,
 | 
				
			||||||
                    guild.Id,
 | 
					                guild.Id,
 | 
				
			||||||
                    usrMsg.Channel?.Name,
 | 
					                usrMsg.Channel?.Name,
 | 
				
			||||||
                    usrMsg.Channel?.Id,
 | 
					                usrMsg.Channel?.Id,
 | 
				
			||||||
                    usrMsg.Author,
 | 
					                usrMsg.Author,
 | 
				
			||||||
                    usrMsg.Author.Id,
 | 
					                usrMsg.Author.Id,
 | 
				
			||||||
                    usrMsg.Content);
 | 
					                usrMsg.Content);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                return true;
 | 
					            return true;
 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        catch (Exception ex)
 | 
					        catch (Exception ex)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -14,7 +14,6 @@ public partial class Games
 | 
				
			|||||||
        public ChatterBotCommands(DbService db)
 | 
					        public ChatterBotCommands(DbService db)
 | 
				
			||||||
            => _db = db;
 | 
					            => _db = db;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [NoPublicBot]
 | 
					 | 
				
			||||||
        [Cmd]
 | 
					        [Cmd]
 | 
				
			||||||
        [RequireContext(ContextType.Guild)]
 | 
					        [RequireContext(ContextType.Guild)]
 | 
				
			||||||
        [UserPerm(GuildPerm.ManageMessages)]
 | 
					        [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);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -6,15 +6,3 @@ public class CleverbotResponse
 | 
				
			|||||||
    public string Cs { get; set; }
 | 
					    public string Cs { get; set; }
 | 
				
			||||||
    public string Output { 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; }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -36,56 +36,3 @@ public class OfficialCleverbotSession : IChatterBotSession
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					 | 
				
			||||||
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();
 | 
					                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())
 | 
					                                .OrderBy(x => x.Key == x.First().Module.Name ? int.MaxValue : x.Count())
 | 
				
			||||||
                                .ToList();
 | 
					                                .ToList();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -393,11 +393,6 @@ public partial class Help : NadekoModule<HelpService>
 | 
				
			|||||||
            };
 | 
					            };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            using var dlClient = new AmazonS3Client(accessKey, secretAcccessKey, config);
 | 
					            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))
 | 
					            using (var client = new AmazonS3Client(accessKey, secretAcccessKey, config))
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@@ -407,14 +402,29 @@ public partial class Help : NadekoModule<HelpService>
 | 
				
			|||||||
                    ContentType = "application/json",
 | 
					                    ContentType = "application/json",
 | 
				
			||||||
                    ContentBody = uploadData,
 | 
					                    ContentBody = uploadData,
 | 
				
			||||||
                    // either use a path provided in the argument or the default one for public nadeko, other/cmds.json
 | 
					                    // 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
 | 
					                    CannedACL = S3CannedACL.PublicRead
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            await using var ms = new MemoryStream();
 | 
					
 | 
				
			||||||
            await oldVersionObject.ResponseStream.CopyToAsync(ms);
 | 
					            var versionListString = "[]";
 | 
				
			||||||
            var versionListString = Encoding.UTF8.GetString(ms.ToArray());
 | 
					            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);
 | 
					            var versionList = JsonSerializer.Deserialize<List<string>>(versionListString);
 | 
				
			||||||
            if (versionList is not null && !versionList.Contains(StatsService.BOT_VERSION))
 | 
					            if (versionList is not null && !versionList.Contains(StatsService.BOT_VERSION))
 | 
				
			||||||
@@ -435,7 +445,7 @@ public partial class Help : NadekoModule<HelpService>
 | 
				
			|||||||
                    ContentType = "application/json",
 | 
					                    ContentType = "application/json",
 | 
				
			||||||
                    ContentBody = versionListString,
 | 
					                    ContentBody = versionListString,
 | 
				
			||||||
                    // either use a path provided in the argument or the default one for public nadeko, other/cmds.json
 | 
					                    // 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
 | 
					                    CannedACL = S3CannedACL.PublicRead
 | 
				
			||||||
                });
 | 
					                });
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
@@ -455,9 +465,71 @@ public partial class Help : NadekoModule<HelpService>
 | 
				
			|||||||
    [Cmd]
 | 
					    [Cmd]
 | 
				
			||||||
    public async partial Task Guide()
 | 
					    public async partial Task Guide()
 | 
				
			||||||
        => await ConfirmLocalizedAsync(strs.guide("https://nadeko.bot/commands",
 | 
					        => 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]
 | 
					    [Cmd]
 | 
				
			||||||
 | 
					    [OnlyPublicBot]
 | 
				
			||||||
    public async partial Task Donate()
 | 
					    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);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -132,6 +132,25 @@ public class HelpService : IExecNoCommand, INService
 | 
				
			|||||||
        if (cmd.Preconditions.Any(x => x is OwnerOnlyAttribute))
 | 
					        if (cmd.Preconditions.Any(x => x is OwnerOnlyAttribute))
 | 
				
			||||||
            toReturn.Add("Bot Owner Only");
 | 
					            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);
 | 
					        var userPerm = (UserPermAttribute)cmd.Preconditions.FirstOrDefault(ca => ca is UserPermAttribute);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var userPermString = string.Empty;
 | 
					        var userPermString = string.Empty;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -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 responseSpan = response.AsSpan()[140_000..];
 | 
				
			||||||
        var startIndex = responseSpan.IndexOf(_ytResultInitialData);
 | 
					        var startIndex = responseSpan.IndexOf(_ytResultInitialData);
 | 
				
			||||||
        if (startIndex == -1)
 | 
					        if (startIndex == -1)
 | 
				
			||||||
            return null; // todo future try selecting html
 | 
					            return null; // FUTURE try selecting html
 | 
				
			||||||
        startIndex += _ytResultInitialData.Length;
 | 
					        startIndex += _ytResultInitialData.Length;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        var endIndex =
 | 
					        var endIndex =
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -219,7 +219,7 @@ public sealed class MusicPlayer : IMusicPlayer
 | 
				
			|||||||
                                errorCount = 0;
 | 
					                                errorCount = 0;
 | 
				
			||||||
                            }
 | 
					                            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                            // todo future windows multimedia api
 | 
					                            // FUTURE windows multimedia api
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                            // wait for slightly less than the latency
 | 
					                            // wait for slightly less than the latency
 | 
				
			||||||
                            Thread.Sleep(delay);
 | 
					                            Thread.Sleep(delay);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -42,8 +42,6 @@ public class RadioResolver : IRadioResolver
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (query.Contains(".pls"))
 | 
					        if (query.Contains(".pls"))
 | 
				
			||||||
            //File1=http://armitunes.com:8000/
 | 
					 | 
				
			||||||
            //Regex.Match(query)
 | 
					 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            try
 | 
					            try
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@@ -59,11 +57,6 @@ public class RadioResolver : IRadioResolver
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (query.Contains(".m3u"))
 | 
					        if (query.Contains(".m3u"))
 | 
				
			||||||
            /* 
 | 
					 | 
				
			||||||
    # This is a comment
 | 
					 | 
				
			||||||
                   C:\xxx4xx\xxxxxx3x\xx2xxxx\xx.mp3
 | 
					 | 
				
			||||||
                   C:\xxx5xx\x6xxxxxx\x7xxxxx\xx.mp3
 | 
					 | 
				
			||||||
                */
 | 
					 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            try
 | 
					            try
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@@ -79,7 +72,6 @@ public class RadioResolver : IRadioResolver
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (query.Contains(".asx"))
 | 
					        if (query.Contains(".asx"))
 | 
				
			||||||
            //<ref href="http://armitunes.com:8000"/>
 | 
					 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            try
 | 
					            try
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
@@ -95,12 +87,6 @@ public class RadioResolver : IRadioResolver
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (query.Contains(".xspf"))
 | 
					        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
 | 
					            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;
 | 
					            _db = db;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        [Cmd]
 | 
					        private async Task CmdCooldownInternal(string cmdName, int secs)
 | 
				
			||||||
        [RequireContext(ContextType.Guild)]
 | 
					 | 
				
			||||||
        public async partial Task CmdCooldown(CommandOrCrInfo command, int secs)
 | 
					 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var channel = (ITextChannel)ctx.Channel;
 | 
					            var channel = (ITextChannel)ctx.Channel;
 | 
				
			||||||
            if (secs is < 0 or > 3600)
 | 
					            if (secs is < 0 or > 3600)
 | 
				
			||||||
@@ -38,7 +36,7 @@ public partial class Permissions
 | 
				
			|||||||
                return;
 | 
					                return;
 | 
				
			||||||
            }
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            var name = command.Name.ToLowerInvariant();
 | 
					            var name = cmdName.ToLowerInvariant();
 | 
				
			||||||
            await using (var uow = _db.GetDbContext())
 | 
					            await using (var uow = _db.GetDbContext())
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                var config = uow.GuildConfigsForId(channel.Guild.Id, set => set.Include(gc => gc.CommandCooldowns));
 | 
					                var config = uow.GuildConfigsForId(channel.Guild.Id, set => set.Include(gc => gc.CommandCooldowns));
 | 
				
			||||||
@@ -72,6 +70,18 @@ public partial class Permissions
 | 
				
			|||||||
                await ReplyConfirmLocalizedAsync(strs.cmdcd_add(Format.Bold(name), Format.Bold(secs.ToString())));
 | 
					                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]
 | 
					        [Cmd]
 | 
				
			||||||
        [RequireContext(ContextType.Guild)]
 | 
					        [RequireContext(ContextType.Guild)]
 | 
				
			||||||
        public async partial Task AllCmdCooldowns()
 | 
					        public async partial Task AllCmdCooldowns()
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,8 @@
 | 
				
			|||||||
#nullable disable
 | 
					#nullable disable
 | 
				
			||||||
using CodeHollow.FeedReader;
 | 
					using CodeHollow.FeedReader;
 | 
				
			||||||
using CodeHollow.FeedReader.Feeds;
 | 
					using CodeHollow.FeedReader.Feeds;
 | 
				
			||||||
 | 
					using LinqToDB;
 | 
				
			||||||
 | 
					using LinqToDB.EntityFrameworkCore;
 | 
				
			||||||
using Microsoft.EntityFrameworkCore;
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
using NadekoBot.Db;
 | 
					using NadekoBot.Db;
 | 
				
			||||||
using NadekoBot.Services.Database.Models;
 | 
					using NadekoBot.Services.Database.Models;
 | 
				
			||||||
@@ -10,11 +12,12 @@ namespace NadekoBot.Modules.Searches.Services;
 | 
				
			|||||||
public class FeedsService : INService
 | 
					public class FeedsService : INService
 | 
				
			||||||
{
 | 
					{
 | 
				
			||||||
    private readonly DbService _db;
 | 
					    private readonly DbService _db;
 | 
				
			||||||
    private readonly ConcurrentDictionary<string, HashSet<FeedSub>> _subs;
 | 
					    private readonly ConcurrentDictionary<string, List<FeedSub>> _subs;
 | 
				
			||||||
    private readonly DiscordSocketClient _client;
 | 
					    private readonly DiscordSocketClient _client;
 | 
				
			||||||
    private readonly IEmbedBuilderService _eb;
 | 
					    private readonly IEmbedBuilderService _eb;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    private readonly ConcurrentDictionary<string, DateTime> _lastPosts = new();
 | 
					    private readonly ConcurrentDictionary<string, DateTime> _lastPosts = new();
 | 
				
			||||||
 | 
					    private readonly Dictionary<string, uint> _errorCounters = new();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    public FeedsService(
 | 
					    public FeedsService(
 | 
				
			||||||
        Bot bot,
 | 
					        Bot bot,
 | 
				
			||||||
@@ -33,7 +36,7 @@ public class FeedsService : INService
 | 
				
			|||||||
                       .ToList()
 | 
					                       .ToList()
 | 
				
			||||||
                       .SelectMany(x => x.FeedSubs)
 | 
					                       .SelectMany(x => x.FeedSubs)
 | 
				
			||||||
                       .GroupBy(x => x.Url.ToLower())
 | 
					                       .GroupBy(x => x.Url.ToLower())
 | 
				
			||||||
                       .ToDictionary(x => x.Key, x => x.ToHashSet())
 | 
					                       .ToDictionary(x => x.Key, x => x.ToList())
 | 
				
			||||||
                       .ToConcurrent();
 | 
					                       .ToConcurrent();
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -43,6 +46,35 @@ public class FeedsService : INService
 | 
				
			|||||||
        _ = Task.Run(TrackFeeds);
 | 
					        _ = 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()
 | 
					    public async Task<EmbedBuilder> TrackFeeds()
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        while (true)
 | 
					        while (true)
 | 
				
			||||||
@@ -134,13 +166,17 @@ public class FeedsService : INService
 | 
				
			|||||||
                            embed.WithDescription(desc.TrimTo(2048));
 | 
					                            embed.WithDescription(desc.TrimTo(2048));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        //send the created embed to all subscribed channels
 | 
					                        //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)
 | 
					                                               .Select(x => _client.GetGuild(x.GuildConfig.GuildId)
 | 
				
			||||||
                                                                   ?.GetTextChannel(x.ChannelId))
 | 
					                                                                   ?.GetTextChannel(x.ChannelId))
 | 
				
			||||||
                                               .Where(x => x is not null)
 | 
					                                               .Where(x => x is not null)
 | 
				
			||||||
                                               .Select(x => x.EmbedAsync(embed));
 | 
					                                               .Select(x => x.EmbedAsync(embed));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                        allSendTasks.Add(feedSendTasks.WhenAll());
 | 
					                        allSendTasks.Add(feedSendTasks.WhenAll());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                        // as data retrieval was sucessful, reset error counter
 | 
				
			||||||
 | 
					                        ClearErrors(rssUrl);
 | 
				
			||||||
                    }
 | 
					                    }
 | 
				
			||||||
                }
 | 
					                }
 | 
				
			||||||
                catch (Exception ex)
 | 
					                catch (Exception ex)
 | 
				
			||||||
@@ -149,6 +185,8 @@ public class FeedsService : INService
 | 
				
			|||||||
                                + "\n {Message}",
 | 
					                                + "\n {Message}",
 | 
				
			||||||
                        rssUrl,
 | 
					                        rssUrl,
 | 
				
			||||||
                        $"[{ex.GetType().Name}]: {ex.Message}");
 | 
					                        $"[{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)
 | 
					        foreach (var feed in gc.FeedSubs)
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            _subs.AddOrUpdate(feed.Url.ToLower(),
 | 
					            _subs.AddOrUpdate(feed.Url.ToLower(),
 | 
				
			||||||
                new HashSet<FeedSub>
 | 
					                new List<FeedSub>
 | 
				
			||||||
                {
 | 
					                {
 | 
				
			||||||
                    feed
 | 
					                    feed
 | 
				
			||||||
                },
 | 
					                },
 | 
				
			||||||
@@ -216,7 +254,7 @@ public class FeedsService : INService
 | 
				
			|||||||
            return false;
 | 
					            return false;
 | 
				
			||||||
        var toRemove = items[index];
 | 
					        var toRemove = items[index];
 | 
				
			||||||
        _subs.AddOrUpdate(toRemove.Url.ToLower(),
 | 
					        _subs.AddOrUpdate(toRemove.Url.ToLower(),
 | 
				
			||||||
            new HashSet<FeedSub>(),
 | 
					            new List<FeedSub>(),
 | 
				
			||||||
            (_, old) =>
 | 
					            (_, old) =>
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                old.Remove(toRemove);
 | 
					                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
 | 
					#nullable disable
 | 
				
			||||||
using AngleSharp;
 | 
					 | 
				
			||||||
using AngleSharp.Html.Dom;
 | 
					 | 
				
			||||||
using Microsoft.Extensions.Caching.Memory;
 | 
					using Microsoft.Extensions.Caching.Memory;
 | 
				
			||||||
using NadekoBot.Modules.Administration.Services;
 | 
					using NadekoBot.Modules.Administration.Services;
 | 
				
			||||||
using NadekoBot.Modules.Searches.Common;
 | 
					using NadekoBot.Modules.Searches.Common;
 | 
				
			||||||
@@ -11,9 +9,9 @@ using SixLabors.ImageSharp;
 | 
				
			|||||||
using SixLabors.ImageSharp.Drawing.Processing;
 | 
					using SixLabors.ImageSharp.Drawing.Processing;
 | 
				
			||||||
using SixLabors.ImageSharp.PixelFormats;
 | 
					using SixLabors.ImageSharp.PixelFormats;
 | 
				
			||||||
using SixLabors.ImageSharp.Processing;
 | 
					using SixLabors.ImageSharp.Processing;
 | 
				
			||||||
 | 
					using System.Diagnostics.CodeAnalysis;
 | 
				
			||||||
using System.Net;
 | 
					using System.Net;
 | 
				
			||||||
using Color = SixLabors.ImageSharp.Color;
 | 
					using Color = SixLabors.ImageSharp.Color;
 | 
				
			||||||
using Configuration = AngleSharp.Configuration;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
namespace NadekoBot.Modules.Searches;
 | 
					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)
 | 
					                .AddField("🌇 " + Format.Bold(GetText(strs.sunset)), $"{sunset:HH:mm} {timezone}", true)
 | 
				
			||||||
                .WithOkColor()
 | 
					                .WithOkColor()
 | 
				
			||||||
                .WithFooter("Powered by openweathermap.org",
 | 
					                .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);
 | 
					        await ctx.Channel.EmbedAsync(embed);
 | 
				
			||||||
@@ -146,22 +144,6 @@ public partial class Searches : NadekoModule<SearchesService>
 | 
				
			|||||||
        await ctx.Channel.SendMessageAsync(embed: eb.Build());
 | 
					        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]
 | 
					    [Cmd]
 | 
				
			||||||
    public async partial Task Movie([Leftover] string query = null)
 | 
					    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()
 | 
					        await ctx.Channel.EmbedAsync(_eb.Create()
 | 
				
			||||||
                                        .WithOkColor()
 | 
					                                        .WithOkColor()
 | 
				
			||||||
                                        .WithTitle(movie.Title)
 | 
					                                        .WithTitle(movie.Title)
 | 
				
			||||||
                                        .WithUrl($"http://www.imdb.com/title/{movie.ImdbId}/")
 | 
					                                        .WithUrl($"https://www.imdb.com/title/{movie.ImdbId}/")
 | 
				
			||||||
                                        .WithDescription(movie.Plot.TrimTo(1000))
 | 
					                                        .WithDescription(movie.Plot.TrimTo(1000))
 | 
				
			||||||
                                        .AddField("Rating", movie.ImdbRating, true)
 | 
					                                        .AddField("Rating", movie.ImdbRating, true)
 | 
				
			||||||
                                        .AddField("Genre", movie.Genre, 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));
 | 
					        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]
 | 
					    [Cmd]
 | 
				
			||||||
    public async partial Task Lmgtfy([Leftover] string ffs = null)
 | 
					    public async partial Task Lmgtfy([Leftover] string ffs = null)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        if (!await ValidateQuery(ffs))
 | 
					        if (!await ValidateQuery(ffs))
 | 
				
			||||||
            return;
 | 
					            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}>");
 | 
					        await SendConfirmAsync($"<{shortenedUrl}>");
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -317,69 +245,6 @@ public partial class Searches : NadekoModule<SearchesService>
 | 
				
			|||||||
                                        .AddField(GetText(strs.short_url), $"<{shortLink}>"));
 | 
					                                        .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]
 | 
					    [Cmd]
 | 
				
			||||||
    public async partial Task MagicTheGathering([Leftover] string search)
 | 
					    public async partial Task MagicTheGathering([Leftover] string search)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
@@ -446,7 +311,7 @@ public partial class Searches : NadekoModule<SearchesService>
 | 
				
			|||||||
        using (var http = _httpFactory.CreateClient())
 | 
					        using (var http = _httpFactory.CreateClient())
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
            var res = await http.GetStringAsync(
 | 
					            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
 | 
					            try
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
                var items = JsonConvert.DeserializeObject<UrbanResponse>(res).List;
 | 
					                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}");
 | 
					        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))
 | 
					        if (!string.IsNullOrWhiteSpace(query))
 | 
				
			||||||
            return true;
 | 
					            return true;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -26,15 +26,6 @@ public class SearchesService : INService
 | 
				
			|||||||
        Birds
 | 
					        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<WoWJoke> WowJokes { get; } = new();
 | 
				
			||||||
    public List<MagicItem> MagicItems { get; } = new();
 | 
					    public List<MagicItem> MagicItems { get; } = new();
 | 
				
			||||||
    private readonly IHttpClientFactory _httpFactory;
 | 
					    private readonly IHttpClientFactory _httpFactory;
 | 
				
			||||||
@@ -161,7 +152,7 @@ public class SearchesService : INService
 | 
				
			|||||||
        using var http = _httpFactory.CreateClient();
 | 
					        using var http = _httpFactory.CreateClient();
 | 
				
			||||||
        try
 | 
					        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}&"
 | 
					                                                 + $"q={query}&"
 | 
				
			||||||
                                                 + "appid=42cd627dd60debf25a5739e50a217d74&"
 | 
					                                                 + "appid=42cd627dd60debf25a5739e50a217d74&"
 | 
				
			||||||
                                                 + "units=metric");
 | 
					                                                 + "units=metric");
 | 
				
			||||||
@@ -440,22 +431,6 @@ public class SearchesService : INService
 | 
				
			|||||||
    public async Task<int> GetSteamAppIdByName(string query)
 | 
					    public async Task<int> GetSteamAppIdByName(string query)
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
        const string steamGameIdsKey = "steam_names_to_appid";
 | 
					        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,
 | 
					        var gamesMap = await _cache.GetOrAddCachedDataAsync(steamGameIdsKey,
 | 
				
			||||||
            async _ =>
 | 
					            async _ =>
 | 
				
			||||||
@@ -502,150 +477,5 @@ public class SearchesService : INService
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return gamesMap[key];
 | 
					        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
 | 
					#nullable disable
 | 
				
			||||||
using LinqToDB;
 | 
					 | 
				
			||||||
using LinqToDB.EntityFrameworkCore;
 | 
					 | 
				
			||||||
using Microsoft.EntityFrameworkCore;
 | 
					using Microsoft.EntityFrameworkCore;
 | 
				
			||||||
using NadekoBot.Common.ModuleBehaviors;
 | 
					using NadekoBot.Common.ModuleBehaviors;
 | 
				
			||||||
using NadekoBot.Db;
 | 
					using NadekoBot.Db;
 | 
				
			||||||
 
 | 
				
			|||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user