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