Merge branch 'v4-prem' into 'v4'

NadekoBot Patronage system, Search commands improvements + fixes

See merge request Kwoth/nadekobot!252
This commit is contained in:
Kwoth
2022-06-14 07:24:34 +00:00
165 changed files with 14920 additions and 1457 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -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;
@@ -125,6 +127,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>();
@@ -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();

View File

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

View File

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

View File

@@ -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; }
@@ -27,6 +27,16 @@ Leave at 1 if you don't know what you're doing.")]
Then, go to APIs and Services -> Credentials and click Create credentials -> API key. Then, go to APIs and Services -> Credentials and click Create credentials -> API key.
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();
} }
@@ -200,4 +211,10 @@ This should be equivalent to the DiscordsKey in your NadekoBot.Votes api appsett
DiscordsKey = discordsKey; DiscordsKey = discordsKey;
} }
} }
}
public class GoogleApiConfig
{
public string SearchId { get; init; }
public string ImageSearchId { get; init; }
} }

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

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

View File

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

View File

@@ -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;
@@ -26,7 +25,7 @@ public abstract class NadekoContext : DbContext
public DbSet<ClubInfo> Clubs { get; set; } public DbSet<ClubInfo> Clubs { get; set; }
public DbSet<ClubBans> ClubBans { get; set; } public DbSet<ClubBans> ClubBans { get; set; }
public DbSet<ClubApplicants> ClubApplicants { get; set; } public DbSet<ClubApplicants> ClubApplicants { get; set; }
//logging //logging
public DbSet<LogSetting> LogSettings { get; set; } public DbSet<LogSetting> LogSettings { get; set; }
@@ -51,19 +50,23 @@ public abstract class NadekoContext : DbContext
public DbSet<AutoTranslateUser> AutoTranslateUsers { get; set; } public DbSet<AutoTranslateUser> AutoTranslateUsers { get; set; }
public DbSet<Permissionv2> Permissions { get; set; } public DbSet<Permissionv2> Permissions { get; set; }
public DbSet<BankUser> BankUsers { get; set; } public DbSet<BankUser> BankUsers { get; set; }
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; }
protected abstract string DiscordUserLastXpGainDefaultValue { get; } protected abstract string DiscordUserLastXpGainDefaultValue { get; }
protected abstract string LastLevelUpDefaultValue { get; } protected abstract string LastLevelUpDefaultValue { get; }
#endregion #endregion
protected override void OnModelCreating(ModelBuilder modelBuilder) protected override void OnModelCreating(ModelBuilder modelBuilder)
{ {
#region QUOTES #region QUOTES
@@ -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,12 +369,13 @@ 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
#region LogSettings #region LogSettings
@@ -419,7 +420,37 @@ public abstract class NadekoContext : DbContext
modelBuilder.Entity<BankUser>(bu => bu.HasIndex(x => x.UserId).IsUnique()); modelBuilder.Entity<BankUser>(bu => bu.HasIndex(x => x.UserId).IsUnique());
#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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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,20 +19,33 @@ 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();
} }
public async Task OnReadyAsync() public async Task OnReadyAsync()
{ {
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,12 +142,12 @@ 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;
} }
// remove all other roles from the same group from the user // remove all other roles from the same group from the user
// execept in group 0, which is a special, non-exclusive group // execept in group 0, which is a special, non-exclusive group
if (rero.Group != 0) if (rero.Group != 0)
@@ -141,7 +157,7 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR
.Select(x => x.RoleId) .Select(x => x.RoleId)
.Distinct(); .Distinct();
try { await user.RemoveRolesAsync(exclusive); } try { await user.RemoveRolesAsync(exclusive); }
catch { } catch { }
@@ -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();
var result = await _ps.TryGetFeatureLimitAsync(_reroFLKey, guild.OwnerId, 50);
if (result.Quota != -1 && activeReactionRoles >= result.Quota)
return result;
if (activeReactionRoles >= 50) await ctx.GetTable<ReactionRoleV2>()
return false; .InsertOrUpdateAsync(() => new()
{
GuildId = guild.Id,
ChannelId = msg.Channel.Id,
var changed = await ctx.GetTable<ReactionRoleV2>() MessageId = msg.Id,
.InsertOrUpdateAsync(() => new() Emote = emote,
{
GuildId = guildId,
ChannelId = channel.Id,
MessageId = msg.Id, RoleId = role.Id,
Emote = emote, Group = group,
LevelReq = levelReq
},
(old) => new()
{
RoleId = role.Id,
Group = group,
LevelReq = levelReq
},
() => new()
{
MessageId = msg.Id,
Emote = emote,
});
RoleId = role.Id, await tran.CommitAsync();
Group = group,
LevelReq = levelReq
},
(old) => new()
{
RoleId = role.Id,
Group = group,
LevelReq = levelReq
},
() => new()
{
MessageId = msg.Id,
Emote = emote,
});
if (changed == 0)
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>()

View File

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

View File

@@ -65,7 +65,7 @@ public partial class Gambling
} }
catch catch
{ {
await ReplyErrorLocalizedAsync(strs.unable_to_dm_user); await ReplyErrorLocalizedAsync(strs.cant_dm);
} }
} }
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,16 +5,4 @@ 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; }
} }

View File

@@ -35,57 +35,4 @@ public class OfficialCleverbotSession : IChatterBotSession
return null; return null;
} }
} }
}
public class CleverbotIoSession : IChatterBotSession
{
private readonly string _key;
private readonly string _user;
private readonly IHttpClientFactory _httpFactory;
private readonly AsyncLazy<string> _nick;
private readonly string _createEndpoint = "https://cleverbot.io/1.0/create";
private readonly string _askEndpoint = "https://cleverbot.io/1.0/ask";
public CleverbotIoSession(string user, string key, IHttpClientFactory factory)
{
_key = key;
_user = user;
_httpFactory = factory;
_nick = new(GetNick);
}
private async Task<string> GetNick()
{
using var http = _httpFactory.CreateClient();
using var msg = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("user", _user), new KeyValuePair<string, string>("key", _key)
});
using var data = await http.PostAsync(_createEndpoint, msg);
var str = await data.Content.ReadAsStringAsync();
var obj = JsonConvert.DeserializeObject<CleverbotIoCreateResponse>(str);
if (obj.Status != "success")
throw new OperationCanceledException(obj.Status);
return obj.Nick;
}
public async Task<string> Think(string input)
{
using var http = _httpFactory.CreateClient();
using var msg = new FormUrlEncodedContent(new[]
{
new KeyValuePair<string, string>("user", _user), new KeyValuePair<string, string>("key", _key),
new KeyValuePair<string, string>("nick", await _nick), new KeyValuePair<string, string>("text", input)
});
using var data = await http.PostAsync(_askEndpoint, msg);
var str = await data.Content.ReadAsStringAsync();
var obj = JsonConvert.DeserializeObject<CleverbotIoAskResponse>(str);
if (obj.Status != "success")
throw new OperationCanceledException(obj.Status);
return obj.Response;
}
} }

View File

@@ -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();
@@ -294,7 +294,7 @@ public partial class Help : NadekoModule<HelpService>
if (fail.StartsWith(prefix)) if (fail.StartsWith(prefix))
fail = fail.Substring(prefix.Length); fail = fail.Substring(prefix.Length);
var group = _cmds.Modules var group = _cmds.Modules
.SelectMany(x => x.Submodules) .SelectMany(x => x.Submodules)
.Where(x => !string.IsNullOrWhiteSpace(x.Group)) .Where(x => !string.IsNullOrWhiteSpace(x.Group))
@@ -393,11 +393,6 @@ public partial class Help : NadekoModule<HelpService>
}; };
using var dlClient = new AmazonS3Client(accessKey, secretAcccessKey, config); using var 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);
}
}
} }

View File

@@ -131,6 +131,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);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -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));
@@ -71,6 +69,18 @@ public partial class Permissions
else else
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)]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -0,0 +1,8 @@
namespace NadekoBot.Modules.Searches;
public interface ISearchResult
{
string? Answer { get; }
IReadOnlyCollection<ISearchResultEntry> Entries { get; }
ISearchResultInformation Info { get; }
}

View File

@@ -0,0 +1,9 @@
namespace NadekoBot.Modules.Searches;
public interface ISearchResultEntry
{
string Title { get; }
string Url { get; }
string DisplayUrl { get; }
string? Description { get; }
}

View File

@@ -0,0 +1,7 @@
namespace NadekoBot.Modules.Searches;
public interface ISearchResultInformation
{
string TotalResults { get; }
string SearchTime { get; }
}

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

View File

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

View 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);
// }
}
}

View File

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

View File

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

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

View File

@@ -0,0 +1,6 @@
namespace NadekoBot.Modules.Searches.Youtube;
public interface IYoutubeSearchService
{
Task<VideoInfo?> SearchAsync(string query);
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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