diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 9386bd391..dd9babade 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index c2700b3db..048df1e9f 100644 --- a/CHANGELOG.md +++ b/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 diff --git a/src/Nadeko.Medusa/IEmbedBuilder.cs b/src/Nadeko.Medusa/IEmbedBuilder.cs index 81c4ce55b..001848967 100644 --- a/src/Nadeko.Medusa/IEmbedBuilder.cs +++ b/src/Nadeko.Medusa/IEmbedBuilder.cs @@ -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); diff --git a/src/Nadeko.Medusa/Nadeko.Medusa.csproj b/src/Nadeko.Medusa/Nadeko.Medusa.csproj index ea50a75c6..ea296c692 100644 --- a/src/Nadeko.Medusa/Nadeko.Medusa.csproj +++ b/src/Nadeko.Medusa/Nadeko.Medusa.csproj @@ -9,7 +9,6 @@ Nadeko.Snake The NadekoBot Team - 1.0.3 diff --git a/src/NadekoBot/Bot.cs b/src/NadekoBot/Bot.cs index a6c87f713..fe65dc16d 100644 --- a/src/NadekoBot/Bot.cs +++ b/src/NadekoBot/Bot.cs @@ -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(); @@ -164,6 +172,7 @@ public sealed class Bot //initialize Services Services = svcs.BuildServiceProvider(); Services.GetRequiredService().Initialize(); + Services.GetRequiredService(); if (Client.ShardId == 0) ApplyConfigMigrations(); diff --git a/src/NadekoBot/Common/Attributes/DontAddToIocContainerAttribute.cs b/src/NadekoBot/Common/Attributes/DontAddToIocContainerAttribute.cs new file mode 100644 index 000000000..308681372 --- /dev/null +++ b/src/NadekoBot/Common/Attributes/DontAddToIocContainerAttribute.cs @@ -0,0 +1,11 @@ +#nullable disable +namespace NadekoBot.Common; + +/// +/// Classed marked with this attribute will not be added to the service provider +/// +[AttributeUsage(AttributeTargets.Class)] +public class DontAddToIocContainerAttribute : Attribute +{ + +} \ No newline at end of file diff --git a/src/NadekoBot/Common/NoPublicBotPrecondition.cs b/src/NadekoBot/Common/Attributes/NoPublicBotPrecondition.cs similarity index 56% rename from src/NadekoBot/Common/NoPublicBotPrecondition.cs rename to src/NadekoBot/Common/Attributes/NoPublicBotPrecondition.cs index 432c1ef6c..7ed29dcb9 100644 --- a/src/NadekoBot/Common/NoPublicBotPrecondition.cs +++ b/src/NadekoBot/Common/Attributes/NoPublicBotPrecondition.cs @@ -20,11 +20,19 @@ public sealed class NoPublicBotAttribute : PreconditionAttribute } } -/// -/// Classed marked with this attribute will not be added to the service provider -/// -[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 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 + } } \ No newline at end of file diff --git a/src/NadekoBot/Common/Creds.cs b/src/NadekoBot/Common/Creds.cs index d19b7ab9a..1bd11fa9e 100644 --- a/src/NadekoBot/Common/Creds.cs +++ b/src/NadekoBot/Common/Creds.cs @@ -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; } } \ No newline at end of file diff --git a/src/NadekoBot/Common/IBotCredentials.cs b/src/NadekoBot/Common/IBotCredentials.cs index 859226e6d..8681c76a7 100644 --- a/src/NadekoBot/Common/IBotCredentials.cs +++ b/src/NadekoBot/Common/IBotCredentials.cs @@ -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 diff --git a/src/NadekoBot/Common/Interaction/NadekoActionInteraction.cs b/src/NadekoBot/Common/Interaction/NadekoActionInteraction.cs index 72673b265..62183a520 100644 --- a/src/NadekoBot/Common/Interaction/NadekoActionInteraction.cs +++ b/src/NadekoBot/Common/Interaction/NadekoActionInteraction.cs @@ -1,11 +1,11 @@ namespace NadekoBot; -public sealed class NadekoActionInteraction : NadekoOwnInteraction +public sealed class NadekoButtonActionInteraction : NadekoButtonOwnInteraction { private readonly NadekoInteractionData _data; private readonly Func _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); diff --git a/src/NadekoBot/Common/Interaction/NadekoInteraction.cs b/src/NadekoBot/Common/Interaction/NadekoInteraction.cs index cb7d0bfb6..02771c9f7 100644 --- a/src/NadekoBot/Common/Interaction/NadekoInteraction.cs +++ b/src/NadekoBot/Common/Interaction/NadekoInteraction.cs @@ -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 _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); -} \ No newline at end of file +} + +// this is all so wrong ... \ No newline at end of file diff --git a/src/NadekoBot/Common/Interaction/NadekoInteractionArray.cs b/src/NadekoBot/Common/Interaction/NadekoInteractionArray.cs new file mode 100644 index 000000000..b2471c1c9 --- /dev/null +++ b/src/NadekoBot/Common/Interaction/NadekoInteractionArray.cs @@ -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 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(); +// } +// } \ No newline at end of file diff --git a/src/NadekoBot/Common/Interaction/NadekoInteractionBuilder.cs b/src/NadekoBot/Common/Interaction/NadekoInteractionBuilder.cs index 81d96dda2..585e98481 100644 --- a/src/NadekoBot/Common/Interaction/NadekoInteractionBuilder.cs +++ b/src/NadekoBot/Common/Interaction/NadekoInteractionBuilder.cs @@ -20,6 +20,7 @@ public class NadekoInteractionBuilder // { // this.isOwn = isOwn; // return this; + // } public NadekoInteractionBuilder WithAction(in Func 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"); diff --git a/src/NadekoBot/Common/Interaction/NadekoInteractionData.cs b/src/NadekoBot/Common/Interaction/NadekoInteractionData.cs index 56e871c46..1ad8c99e4 100644 --- a/src/NadekoBot/Common/Interaction/NadekoInteractionData.cs +++ b/src/NadekoBot/Common/Interaction/NadekoInteractionData.cs @@ -5,4 +5,4 @@ /// /// Emote which will show on a button /// Custom interaction id -public record NadekoInteractionData(IEmote Emote, string CustomId); \ No newline at end of file +public record NadekoInteractionData(IEmote Emote, string CustomId, string? Text = null); \ No newline at end of file diff --git a/src/NadekoBot/Common/Interaction/NadekoOwnInteraction.cs b/src/NadekoBot/Common/Interaction/NadekoOwnInteraction.cs index f6111b607..1d59f2835 100644 --- a/src/NadekoBot/Common/Interaction/NadekoOwnInteraction.cs +++ b/src/NadekoBot/Common/Interaction/NadekoOwnInteraction.cs @@ -3,11 +3,11 @@ /// /// Interaction which only the author can use /// -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 Validate(SocketMessageComponent smc) diff --git a/src/NadekoBot/Common/NInteraction.cs b/src/NadekoBot/Common/NInteraction.cs new file mode 100644 index 000000000..723fd0b72 --- /dev/null +++ b/src/NadekoBot/Common/NInteraction.cs @@ -0,0 +1,26 @@ +namespace NadekoBot.Common; + +public abstract class NInteraction +{ + private readonly DiscordSocketClient _client; + private readonly ulong _userId; + private readonly Func _action; + + protected abstract NadekoInteractionData Data { get; } + + public NInteraction( + DiscordSocketClient client, + ulong userId, + Func action) + { + _client = client; + _userId = userId; + _action = action; + } + + public NadekoButtonInteraction GetInteraction() + => new NadekoInteractionBuilder() + .WithData(Data) + .WithAction(_action) + .Build(_client, _userId); +} \ No newline at end of file diff --git a/src/NadekoBot/Common/NadekoModule.cs b/src/NadekoBot/Common/NadekoModule.cs index add1769aa..d261fb4cf 100644 --- a/src/NadekoBot/Common/NadekoModule.cs +++ b/src/NadekoBot/Common/NadekoModule.cs @@ -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 SendConfirmAsync( @@ -47,32 +47,32 @@ public abstract class NadekoModule : ModuleBase => ctx.Channel.SendConfirmAsync(_eb, title, text, url, footer); // - public Task SendErrorAsync(string text, NadekoInteraction inter = null) + public Task SendErrorAsync(string text, NadekoButtonInteraction inter = null) => ctx.Channel.SendAsync(_eb, text, MessageType.Error, inter); - public Task SendConfirmAsync(string text, NadekoInteraction inter = null) + public Task SendConfirmAsync(string text, NadekoButtonInteraction inter = null) => ctx.Channel.SendAsync(_eb, text, MessageType.Ok, inter); - public Task SendPendingAsync(string text, NadekoInteraction inter = null) + public Task SendPendingAsync(string text, NadekoButtonInteraction inter = null) => ctx.Channel.SendAsync(_eb, text, MessageType.Pending, inter); // localized normal - public Task ErrorLocalizedAsync(LocStr str, NadekoInteraction inter = null) + public Task ErrorLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null) => SendErrorAsync(GetText(str), inter); - public Task PendingLocalizedAsync(LocStr str, NadekoInteraction inter = null) + public Task PendingLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null) => SendPendingAsync(GetText(str), inter); - public Task ConfirmLocalizedAsync(LocStr str, NadekoInteraction inter = null) + public Task ConfirmLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null) => SendConfirmAsync(GetText(str), inter); // localized replies - public Task ReplyErrorLocalizedAsync(LocStr str, NadekoInteraction inter = null) + public Task ReplyErrorLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null) => SendErrorAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}"); - public Task ReplyPendingLocalizedAsync(LocStr str, NadekoInteraction inter = null) + public Task ReplyPendingLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null) => SendPendingAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}"); - public Task ReplyConfirmLocalizedAsync(LocStr str, NadekoInteraction inter = null) + public Task ReplyConfirmLocalizedAsync(LocStr str, NadekoButtonInteraction inter = null) => SendConfirmAsync($"{Format.Bold(ctx.User.ToString())} {GetText(str)}"); public async Task PromptUserConfirmAsync(IEmbedBuilder embed) diff --git a/src/NadekoBot/Common/SmartText/SmartText.cs b/src/NadekoBot/Common/SmartText/SmartText.cs index e9e68b78c..0c4c9d6ed 100644 --- a/src/NadekoBot/Common/SmartText/SmartText.cs +++ b/src/NadekoBot/Common/SmartText/SmartText.cs @@ -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)) diff --git a/src/NadekoBot/Db/Models/DiscordUser.cs b/src/NadekoBot/Db/Models/DiscordUser.cs index 5b95f95e6..2e14ffcdf 100644 --- a/src/NadekoBot/Db/Models/DiscordUser.cs +++ b/src/NadekoBot/Db/Models/DiscordUser.cs @@ -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; } diff --git a/src/NadekoBot/Db/Models/GuildConfig.cs b/src/NadekoBot/Db/Models/GuildConfig.cs index 7c39fa270..38211e565 100644 --- a/src/NadekoBot/Db/Models/GuildConfig.cs +++ b/src/NadekoBot/Db/Models/GuildConfig.cs @@ -84,7 +84,7 @@ public class GuildConfig : DbEntity public List ShopEntries { get; set; } public ulong? GameVoiceChannel { get; set; } - public bool VerboseErrors { get; set; } + public bool VerboseErrors { get; set; } = true; public StreamRoleSettings StreamRole { get; set; } diff --git a/src/NadekoBot/Db/Models/PatronQuota.cs b/src/NadekoBot/Db/Models/PatronQuota.cs new file mode 100644 index 000000000..8b909afed --- /dev/null +++ b/src/NadekoBot/Db/Models/PatronQuota.cs @@ -0,0 +1,38 @@ +#nullable disable +namespace NadekoBot.Db.Models; + +/// +/// 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) +/// +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; } +} \ No newline at end of file diff --git a/src/NadekoBot/Db/Models/RewardedUser.cs b/src/NadekoBot/Db/Models/RewardedUser.cs index f0a8f926d..13f5b9997 100644 --- a/src/NadekoBot/Db/Models/RewardedUser.cs +++ b/src/NadekoBot/Db/Models/RewardedUser.cs @@ -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; } } \ No newline at end of file diff --git a/src/NadekoBot/Db/NadekoContext.cs b/src/NadekoBot/Db/NadekoContext.cs index 1c5887e54..5f1f85e63 100644 --- a/src/NadekoBot/Db/NadekoContext.cs +++ b/src/NadekoBot/Db/NadekoContext.cs @@ -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 Clubs { get; set; } public DbSet ClubBans { get; set; } public DbSet ClubApplicants { get; set; } - + //logging public DbSet LogSettings { get; set; } @@ -51,19 +50,23 @@ public abstract class NadekoContext : DbContext public DbSet AutoTranslateUsers { get; set; } public DbSet Permissions { get; set; } - + public DbSet BankUsers { get; set; } - + public DbSet ReactionRoles { get; set; } + public DbSet Patrons { get; set; } + + public DbSet 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(); - configEntity.HasIndex(c => c.GuildId).IsUnique(); + configEntity.HasIndex(c => c.GuildId) + .IsUnique(); + + configEntity.Property(x => x.VerboseErrors) + .HasDefaultValue(true); modelBuilder.Entity().HasOne(x => x.GuildConfig).WithOne(x => x.AntiSpamSetting); @@ -193,13 +200,6 @@ public abstract class NadekoContext : DbContext #endregion - #region PatreonRewards - - var pr = modelBuilder.Entity(); - pr.HasIndex(x => x.PatreonUserId).IsUnique(); - - #endregion - #region XpStats var xps = modelBuilder.Entity(); @@ -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(bu => bu.HasIndex(x => x.UserId).IsUnique()); #endregion - + + + #region Patron + + // currency rewards + var pr = modelBuilder.Entity(); + 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(pu => + { + pu.HasIndex(x => x.UniquePlatformUserId).IsUnique(); + pu.HasKey(x => x.UserId); + }); + + // quotes are per user id + modelBuilder.Entity(pq => + { + pq.HasIndex(x => x.UserId).IsUnique(false); + pq.HasKey(x => new + { + x.UserId, + x.FeatureType, + x.Feature + }); + }); + + #endregion } #if DEBUG diff --git a/src/NadekoBot/Migrations/MySql/20220614071410_patronage-system.Designer.cs b/src/NadekoBot/Migrations/MySql/20220614071410_patronage-system.Designer.cs new file mode 100644 index 000000000..70961f3cf --- /dev/null +++ b/src/NadekoBot/Migrations/MySql/20220614071410_patronage-system.Designer.cs @@ -0,0 +1,3481 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NadekoBot.Services.Database; + +#nullable disable + +namespace NadekoBot.Migrations.Mysql +{ + [DbContext(typeof(MysqlContext))] + [Migration("20220614071410_patronage-system")] + partial class patronagesystem + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 64); + + modelBuilder.Entity("NadekoBot.Db.Models.BankUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("Balance") + .HasColumnType("bigint") + .HasColumnName("balance"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("UserId") + .HasColumnType("bigint unsigned") + .HasColumnName("userid"); + + b.HasKey("Id") + .HasName("pk_bankusers"); + + b.HasIndex("UserId") + .IsUnique() + .HasDatabaseName("ix_bankusers_userid"); + + b.ToTable("bankusers", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.ClubApplicants", b => + { + b.Property("ClubId") + .HasColumnType("int") + .HasColumnName("clubid"); + + b.Property("UserId") + .HasColumnType("int") + .HasColumnName("userid"); + + b.HasKey("ClubId", "UserId") + .HasName("pk_clubapplicants"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_clubapplicants_userid"); + + b.ToTable("clubapplicants", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.ClubBans", b => + { + b.Property("ClubId") + .HasColumnType("int") + .HasColumnName("clubid"); + + b.Property("UserId") + .HasColumnType("int") + .HasColumnName("userid"); + + b.HasKey("ClubId", "UserId") + .HasName("pk_clubbans"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_clubbans_userid"); + + b.ToTable("clubbans", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.ClubInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("Description") + .HasColumnType("longtext") + .HasColumnName("description"); + + b.Property("ImageUrl") + .HasColumnType("longtext") + .HasColumnName("imageurl"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("varchar(20)") + .HasColumnName("name") + .UseCollation("utf8mb4_bin"); + + b.Property("OwnerId") + .HasColumnType("int") + .HasColumnName("ownerid"); + + b.Property("Xp") + .HasColumnType("int") + .HasColumnName("xp"); + + b.HasKey("Id") + .HasName("pk_clubs"); + + b.HasAlternateKey("Name") + .HasName("ak_clubs_name"); + + b.HasIndex("OwnerId") + .IsUnique() + .HasDatabaseName("ix_clubs_ownerid"); + + b.ToTable("clubs", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.DiscordUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("AvatarId") + .HasColumnType("longtext") + .HasColumnName("avatarid"); + + b.Property("ClubId") + .HasColumnType("int") + .HasColumnName("clubid"); + + b.Property("CurrencyAmount") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(0L) + .HasColumnName("currencyamount"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("Discriminator") + .HasColumnType("longtext") + .HasColumnName("discriminator"); + + b.Property("IsClubAdmin") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(false) + .HasColumnName("isclubadmin"); + + b.Property("LastLevelUp") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)") + .HasColumnName("lastlevelup") + .HasDefaultValueSql("(UTC_TIMESTAMP)"); + + b.Property("LastXpGain") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)") + .HasColumnName("lastxpgain") + .HasDefaultValueSql("(UTC_TIMESTAMP - INTERVAL 1 year)"); + + b.Property("NotifyOnLevelUp") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0) + .HasColumnName("notifyonlevelup"); + + b.Property("TotalXp") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(0L) + .HasColumnName("totalxp"); + + b.Property("UserId") + .HasColumnType("bigint unsigned") + .HasColumnName("userid"); + + b.Property("Username") + .HasColumnType("longtext") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_discorduser"); + + b.HasAlternateKey("UserId") + .HasName("ak_discorduser_userid"); + + b.HasIndex("ClubId") + .HasDatabaseName("ix_discorduser_clubid"); + + b.HasIndex("CurrencyAmount") + .HasDatabaseName("ix_discorduser_currencyamount"); + + b.HasIndex("TotalXp") + .HasDatabaseName("ix_discorduser_totalxp"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_discorduser_userid"); + + b.ToTable("discorduser", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.FollowedStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("ChannelId") + .HasColumnType("bigint unsigned") + .HasColumnName("channelid"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("int") + .HasColumnName("guildconfigid"); + + b.Property("GuildId") + .HasColumnType("bigint unsigned") + .HasColumnName("guildid"); + + b.Property("Message") + .HasColumnType("longtext") + .HasColumnName("message"); + + b.Property("Type") + .HasColumnType("int") + .HasColumnName("type"); + + b.Property("Username") + .HasColumnType("longtext") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_followedstream"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_followedstream_guildconfigid"); + + b.ToTable("followedstream", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.PatronQuota", b => + { + b.Property("UserId") + .HasColumnType("bigint unsigned") + .HasColumnName("userid"); + + b.Property("FeatureType") + .HasColumnType("int") + .HasColumnName("featuretype"); + + b.Property("Feature") + .HasColumnType("varchar(255)") + .HasColumnName("feature"); + + b.Property("DailyCount") + .HasColumnType("int unsigned") + .HasColumnName("dailycount"); + + b.Property("HourlyCount") + .HasColumnType("int unsigned") + .HasColumnName("hourlycount"); + + b.Property("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("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint unsigned") + .HasColumnName("userid"); + + b.Property("AmountCents") + .HasColumnType("int") + .HasColumnName("amountcents"); + + b.Property("LastCharge") + .HasColumnType("datetime(6)") + .HasColumnName("lastcharge"); + + b.Property("UniquePlatformUserId") + .HasColumnType("varchar(255)") + .HasColumnName("uniqueplatformuserid"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("Action") + .HasColumnType("int") + .HasColumnName("action"); + + b.Property("ActionDurationMinutes") + .HasColumnType("int") + .HasColumnName("actiondurationminutes"); + + b.Property("GuildConfigId") + .HasColumnType("int") + .HasColumnName("guildconfigid"); + + b.Property("MinAge") + .HasColumnType("time(6)") + .HasColumnName("minage"); + + b.Property("RoleId") + .HasColumnType("bigint unsigned") + .HasColumnName("roleid"); + + b.HasKey("Id") + .HasName("pk_antialtsetting"); + + b.HasIndex("GuildConfigId") + .IsUnique() + .HasDatabaseName("ix_antialtsetting_guildconfigid"); + + b.ToTable("antialtsetting", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("Action") + .HasColumnType("int") + .HasColumnName("action"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("int") + .HasColumnName("guildconfigid"); + + b.Property("PunishDuration") + .HasColumnType("int") + .HasColumnName("punishduration"); + + b.Property("Seconds") + .HasColumnType("int") + .HasColumnName("seconds"); + + b.Property("UserThreshold") + .HasColumnType("int") + .HasColumnName("userthreshold"); + + b.HasKey("Id") + .HasName("pk_antiraidsetting"); + + b.HasIndex("GuildConfigId") + .IsUnique() + .HasDatabaseName("ix_antiraidsetting_guildconfigid"); + + b.ToTable("antiraidsetting", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("AntiSpamSettingId") + .HasColumnType("int") + .HasColumnName("antispamsettingid"); + + b.Property("ChannelId") + .HasColumnType("bigint unsigned") + .HasColumnName("channelid"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.HasKey("Id") + .HasName("pk_antispamignore"); + + b.HasIndex("AntiSpamSettingId") + .HasDatabaseName("ix_antispamignore_antispamsettingid"); + + b.ToTable("antispamignore", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("Action") + .HasColumnType("int") + .HasColumnName("action"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("int") + .HasColumnName("guildconfigid"); + + b.Property("MessageThreshold") + .HasColumnType("int") + .HasColumnName("messagethreshold"); + + b.Property("MuteTime") + .HasColumnType("int") + .HasColumnName("mutetime"); + + b.Property("RoleId") + .HasColumnType("bigint unsigned") + .HasColumnName("roleid"); + + b.HasKey("Id") + .HasName("pk_antispamsetting"); + + b.HasIndex("GuildConfigId") + .IsUnique() + .HasDatabaseName("ix_antispamsetting_guildconfigid"); + + b.ToTable("antispamsetting", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AutoCommand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("ChannelId") + .HasColumnType("bigint unsigned") + .HasColumnName("channelid"); + + b.Property("ChannelName") + .HasColumnType("longtext") + .HasColumnName("channelname"); + + b.Property("CommandText") + .HasColumnType("longtext") + .HasColumnName("commandtext"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildId") + .HasColumnType("bigint unsigned") + .HasColumnName("guildid"); + + b.Property("GuildName") + .HasColumnType("longtext") + .HasColumnName("guildname"); + + b.Property("Interval") + .HasColumnType("int") + .HasColumnName("interval"); + + b.Property("VoiceChannelId") + .HasColumnType("bigint unsigned") + .HasColumnName("voicechannelid"); + + b.Property("VoiceChannelName") + .HasColumnType("longtext") + .HasColumnName("voicechannelname"); + + b.HasKey("Id") + .HasName("pk_autocommands"); + + b.ToTable("autocommands", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AutoTranslateChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("AutoDelete") + .HasColumnType("tinyint(1)") + .HasColumnName("autodelete"); + + b.Property("ChannelId") + .HasColumnType("bigint unsigned") + .HasColumnName("channelid"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildId") + .HasColumnType("bigint unsigned") + .HasColumnName("guildid"); + + b.HasKey("Id") + .HasName("pk_autotranslatechannels"); + + b.HasIndex("ChannelId") + .IsUnique() + .HasDatabaseName("ix_autotranslatechannels_channelid"); + + b.HasIndex("GuildId") + .HasDatabaseName("ix_autotranslatechannels_guildid"); + + b.ToTable("autotranslatechannels", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AutoTranslateUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("ChannelId") + .HasColumnType("int") + .HasColumnName("channelid"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("Source") + .HasColumnType("longtext") + .HasColumnName("source"); + + b.Property("Target") + .HasColumnType("longtext") + .HasColumnName("target"); + + b.Property("UserId") + .HasColumnType("bigint unsigned") + .HasColumnName("userid"); + + b.HasKey("Id") + .HasName("pk_autotranslateusers"); + + b.HasAlternateKey("ChannelId", "UserId") + .HasName("ak_autotranslateusers_channelid_userid"); + + b.ToTable("autotranslateusers", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BanTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildId") + .HasColumnType("bigint unsigned") + .HasColumnName("guildid"); + + b.Property("Text") + .HasColumnType("longtext") + .HasColumnName("text"); + + b.HasKey("Id") + .HasName("pk_bantemplates"); + + b.HasIndex("GuildId") + .IsUnique() + .HasDatabaseName("ix_bantemplates_guildid"); + + b.ToTable("bantemplates", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("ItemId") + .HasColumnType("bigint unsigned") + .HasColumnName("itemid"); + + b.Property("Type") + .HasColumnType("int") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_blacklist"); + + b.ToTable("blacklist", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("int") + .HasColumnName("guildconfigid"); + + b.Property("Mapping") + .HasColumnType("longtext") + .HasColumnName("mapping"); + + b.Property("Trigger") + .HasColumnType("longtext") + .HasColumnName("trigger"); + + b.HasKey("Id") + .HasName("pk_commandalias"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_commandalias_guildconfigid"); + + b.ToTable("commandalias", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("CommandName") + .HasColumnType("longtext") + .HasColumnName("commandname"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("int") + .HasColumnName("guildconfigid"); + + b.Property("Seconds") + .HasColumnType("int") + .HasColumnName("seconds"); + + b.HasKey("Id") + .HasName("pk_commandcooldown"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_commandcooldown_guildconfigid"); + + b.ToTable("commandcooldown", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CurrencyTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("Amount") + .HasColumnType("bigint") + .HasColumnName("amount"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("Extra") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("extra"); + + b.Property("Note") + .HasColumnType("longtext") + .HasColumnName("note"); + + b.Property("OtherId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint unsigned") + .HasColumnName("otherid") + .HasDefaultValueSql("NULL"); + + b.Property("Type") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("type"); + + b.Property("UserId") + .HasColumnType("bigint unsigned") + .HasColumnName("userid"); + + b.HasKey("Id") + .HasName("pk_currencytransactions"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_currencytransactions_userid"); + + b.ToTable("currencytransactions", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.DelMsgOnCmdChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("ChannelId") + .HasColumnType("bigint unsigned") + .HasColumnName("channelid"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("int") + .HasColumnName("guildconfigid"); + + b.Property("State") + .HasColumnType("tinyint(1)") + .HasColumnName("state"); + + b.HasKey("Id") + .HasName("pk_delmsgoncmdchannel"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_delmsgoncmdchannel_guildconfigid"); + + b.ToTable("delmsgoncmdchannel", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.DiscordPermOverride", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("Command") + .HasColumnType("varchar(255)") + .HasColumnName("command"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildId") + .HasColumnType("bigint unsigned") + .HasColumnName("guildid"); + + b.Property("Perm") + .HasColumnType("bigint unsigned") + .HasColumnName("perm"); + + b.HasKey("Id") + .HasName("pk_discordpermoverrides"); + + b.HasIndex("GuildId", "Command") + .IsUnique() + .HasDatabaseName("ix_discordpermoverrides_guildid_command"); + + b.ToTable("discordpermoverrides", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ExcludedItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("ItemId") + .HasColumnType("bigint unsigned") + .HasColumnName("itemid"); + + b.Property("ItemType") + .HasColumnType("int") + .HasColumnName("itemtype"); + + b.Property("XpSettingsId") + .HasColumnType("int") + .HasColumnName("xpsettingsid"); + + b.HasKey("Id") + .HasName("pk_excludeditem"); + + b.HasIndex("XpSettingsId") + .HasDatabaseName("ix_excludeditem_xpsettingsid"); + + b.ToTable("excludeditem", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FeedSub", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("ChannelId") + .HasColumnType("bigint unsigned") + .HasColumnName("channelid"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("int") + .HasColumnName("guildconfigid"); + + b.Property("Url") + .IsRequired() + .HasColumnType("varchar(255)") + .HasColumnName("url"); + + b.HasKey("Id") + .HasName("pk_feedsub"); + + b.HasAlternateKey("GuildConfigId", "Url") + .HasName("ak_feedsub_guildconfigid_url"); + + b.ToTable("feedsub", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("ChannelId") + .HasColumnType("bigint unsigned") + .HasColumnName("channelid"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("int") + .HasColumnName("guildconfigid"); + + b.HasKey("Id") + .HasName("pk_filterchannelid"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_filterchannelid_guildconfigid"); + + b.ToTable("filterchannelid", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("int") + .HasColumnName("guildconfigid"); + + b.Property("Word") + .HasColumnType("longtext") + .HasColumnName("word"); + + b.HasKey("Id") + .HasName("pk_filteredword"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_filteredword_guildconfigid"); + + b.ToTable("filteredword", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterLinksChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("ChannelId") + .HasColumnType("bigint unsigned") + .HasColumnName("channelid"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("int") + .HasColumnName("guildconfigid"); + + b.HasKey("Id") + .HasName("pk_filterlinkschannelid"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_filterlinkschannelid_guildconfigid"); + + b.ToTable("filterlinkschannelid", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterWordsChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("ChannelId") + .HasColumnType("bigint unsigned") + .HasColumnName("channelid"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("int") + .HasColumnName("guildconfigid"); + + b.HasKey("Id") + .HasName("pk_filterwordschannelid"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_filterwordschannelid_guildconfigid"); + + b.ToTable("filterwordschannelid", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("ChannelId") + .HasColumnType("bigint unsigned") + .HasColumnName("channelid"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("int") + .HasColumnName("guildconfigid"); + + b.HasKey("Id") + .HasName("pk_gcchannelid"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_gcchannelid_guildconfigid"); + + b.ToTable("gcchannelid", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GroupName", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("int") + .HasColumnName("guildconfigid"); + + b.Property("Name") + .HasColumnType("longtext") + .HasColumnName("name"); + + b.Property("Number") + .HasColumnType("int") + .HasColumnName("number"); + + b.HasKey("Id") + .HasName("pk_groupname"); + + b.HasIndex("GuildConfigId", "Number") + .IsUnique() + .HasDatabaseName("ix_groupname_guildconfigid_number"); + + b.ToTable("groupname", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("AutoAssignRoleIds") + .HasColumnType("longtext") + .HasColumnName("autoassignroleids"); + + b.Property("AutoDeleteByeMessagesTimer") + .HasColumnType("int") + .HasColumnName("autodeletebyemessagestimer"); + + b.Property("AutoDeleteGreetMessagesTimer") + .HasColumnType("int") + .HasColumnName("autodeletegreetmessagestimer"); + + b.Property("AutoDeleteSelfAssignedRoleMessages") + .HasColumnType("tinyint(1)") + .HasColumnName("autodeleteselfassignedrolemessages"); + + b.Property("BoostMessage") + .HasColumnType("longtext") + .HasColumnName("boostmessage"); + + b.Property("BoostMessageChannelId") + .HasColumnType("bigint unsigned") + .HasColumnName("boostmessagechannelid"); + + b.Property("BoostMessageDeleteAfter") + .HasColumnType("int") + .HasColumnName("boostmessagedeleteafter"); + + b.Property("ByeMessageChannelId") + .HasColumnType("bigint unsigned") + .HasColumnName("byemessagechannelid"); + + b.Property("ChannelByeMessageText") + .HasColumnType("longtext") + .HasColumnName("channelbyemessagetext"); + + b.Property("ChannelGreetMessageText") + .HasColumnType("longtext") + .HasColumnName("channelgreetmessagetext"); + + b.Property("CleverbotEnabled") + .HasColumnType("tinyint(1)") + .HasColumnName("cleverbotenabled"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("DeleteMessageOnCommand") + .HasColumnType("tinyint(1)") + .HasColumnName("deletemessageoncommand"); + + b.Property("DeleteStreamOnlineMessage") + .HasColumnType("tinyint(1)") + .HasColumnName("deletestreamonlinemessage"); + + b.Property("DmGreetMessageText") + .HasColumnType("longtext") + .HasColumnName("dmgreetmessagetext"); + + b.Property("ExclusiveSelfAssignedRoles") + .HasColumnType("tinyint(1)") + .HasColumnName("exclusiveselfassignedroles"); + + b.Property("FilterInvites") + .HasColumnType("tinyint(1)") + .HasColumnName("filterinvites"); + + b.Property("FilterLinks") + .HasColumnType("tinyint(1)") + .HasColumnName("filterlinks"); + + b.Property("FilterWords") + .HasColumnType("tinyint(1)") + .HasColumnName("filterwords"); + + b.Property("GameVoiceChannel") + .HasColumnType("bigint unsigned") + .HasColumnName("gamevoicechannel"); + + b.Property("GreetMessageChannelId") + .HasColumnType("bigint unsigned") + .HasColumnName("greetmessagechannelid"); + + b.Property("GuildId") + .HasColumnType("bigint unsigned") + .HasColumnName("guildid"); + + b.Property("Locale") + .HasColumnType("longtext") + .HasColumnName("locale"); + + b.Property("MuteRoleName") + .HasColumnType("longtext") + .HasColumnName("muterolename"); + + b.Property("NotifyStreamOffline") + .HasColumnType("tinyint(1)") + .HasColumnName("notifystreamoffline"); + + b.Property("PermissionRole") + .HasColumnType("longtext") + .HasColumnName("permissionrole"); + + b.Property("Prefix") + .HasColumnType("longtext") + .HasColumnName("prefix"); + + b.Property("SendBoostMessage") + .HasColumnType("tinyint(1)") + .HasColumnName("sendboostmessage"); + + b.Property("SendChannelByeMessage") + .HasColumnType("tinyint(1)") + .HasColumnName("sendchannelbyemessage"); + + b.Property("SendChannelGreetMessage") + .HasColumnType("tinyint(1)") + .HasColumnName("sendchannelgreetmessage"); + + b.Property("SendDmGreetMessage") + .HasColumnType("tinyint(1)") + .HasColumnName("senddmgreetmessage"); + + b.Property("TimeZoneId") + .HasColumnType("longtext") + .HasColumnName("timezoneid"); + + b.Property("VerboseErrors") + .ValueGeneratedOnAdd() + .HasColumnType("tinyint(1)") + .HasDefaultValue(true) + .HasColumnName("verboseerrors"); + + b.Property("VerbosePermissions") + .HasColumnType("tinyint(1)") + .HasColumnName("verbosepermissions"); + + b.Property("WarnExpireAction") + .HasColumnType("int") + .HasColumnName("warnexpireaction"); + + b.Property("WarnExpireHours") + .HasColumnType("int") + .HasColumnName("warnexpirehours"); + + b.Property("WarningsInitialized") + .HasColumnType("tinyint(1)") + .HasColumnName("warningsinitialized"); + + b.HasKey("Id") + .HasName("pk_guildconfigs"); + + b.HasIndex("GuildId") + .IsUnique() + .HasDatabaseName("ix_guildconfigs_guildid"); + + b.HasIndex("WarnExpireHours") + .HasDatabaseName("ix_guildconfigs_warnexpirehours"); + + b.ToTable("guildconfigs", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("ItemType") + .HasColumnType("int") + .HasColumnName("itemtype"); + + b.Property("LogItemId") + .HasColumnType("bigint unsigned") + .HasColumnName("logitemid"); + + b.Property("LogSettingId") + .HasColumnType("int") + .HasColumnName("logsettingid"); + + b.HasKey("Id") + .HasName("pk_ignoredlogchannels"); + + b.HasIndex("LogSettingId", "LogItemId", "ItemType") + .IsUnique() + .HasDatabaseName("ix_ignoredlogchannels_logsettingid_logitemid_itemtype"); + + b.ToTable("ignoredlogchannels", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("ChannelId") + .HasColumnType("bigint unsigned") + .HasColumnName("channelid"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("LogSettingId") + .HasColumnType("int") + .HasColumnName("logsettingid"); + + b.HasKey("Id") + .HasName("pk_ignoredvoicepresencechannels"); + + b.HasIndex("LogSettingId") + .HasDatabaseName("ix_ignoredvoicepresencechannels_logsettingid"); + + b.ToTable("ignoredvoicepresencechannels", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ImageOnlyChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("ChannelId") + .HasColumnType("bigint unsigned") + .HasColumnName("channelid"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildId") + .HasColumnType("bigint unsigned") + .HasColumnName("guildid"); + + b.HasKey("Id") + .HasName("pk_imageonlychannels"); + + b.HasIndex("ChannelId") + .IsUnique() + .HasDatabaseName("ix_imageonlychannels_channelid"); + + b.ToTable("imageonlychannels", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.LogSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("ChannelCreatedId") + .HasColumnType("bigint unsigned") + .HasColumnName("channelcreatedid"); + + b.Property("ChannelDestroyedId") + .HasColumnType("bigint unsigned") + .HasColumnName("channeldestroyedid"); + + b.Property("ChannelUpdatedId") + .HasColumnType("bigint unsigned") + .HasColumnName("channelupdatedid"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildId") + .HasColumnType("bigint unsigned") + .HasColumnName("guildid"); + + b.Property("LogOtherId") + .HasColumnType("bigint unsigned") + .HasColumnName("logotherid"); + + b.Property("LogUserPresenceId") + .HasColumnType("bigint unsigned") + .HasColumnName("loguserpresenceid"); + + b.Property("LogVoicePresenceId") + .HasColumnType("bigint unsigned") + .HasColumnName("logvoicepresenceid"); + + b.Property("LogVoicePresenceTTSId") + .HasColumnType("bigint unsigned") + .HasColumnName("logvoicepresencettsid"); + + b.Property("MessageDeletedId") + .HasColumnType("bigint unsigned") + .HasColumnName("messagedeletedid"); + + b.Property("MessageUpdatedId") + .HasColumnType("bigint unsigned") + .HasColumnName("messageupdatedid"); + + b.Property("UserBannedId") + .HasColumnType("bigint unsigned") + .HasColumnName("userbannedid"); + + b.Property("UserJoinedId") + .HasColumnType("bigint unsigned") + .HasColumnName("userjoinedid"); + + b.Property("UserLeftId") + .HasColumnType("bigint unsigned") + .HasColumnName("userleftid"); + + b.Property("UserMutedId") + .HasColumnType("bigint unsigned") + .HasColumnName("usermutedid"); + + b.Property("UserUnbannedId") + .HasColumnType("bigint unsigned") + .HasColumnName("userunbannedid"); + + b.Property("UserUpdatedId") + .HasColumnType("bigint unsigned") + .HasColumnName("userupdatedid"); + + b.HasKey("Id") + .HasName("pk_logsettings"); + + b.HasIndex("GuildId") + .IsUnique() + .HasDatabaseName("ix_logsettings_guildid"); + + b.ToTable("logsettings", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MusicPlayerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("AutoDisconnect") + .HasColumnType("tinyint(1)") + .HasColumnName("autodisconnect"); + + b.Property("AutoPlay") + .HasColumnType("tinyint(1)") + .HasColumnName("autoplay"); + + b.Property("GuildId") + .HasColumnType("bigint unsigned") + .HasColumnName("guildid"); + + b.Property("MusicChannelId") + .HasColumnType("bigint unsigned") + .HasColumnName("musicchannelid"); + + b.Property("PlayerRepeat") + .HasColumnType("int") + .HasColumnName("playerrepeat"); + + b.Property("QualityPreset") + .HasColumnType("int") + .HasColumnName("qualitypreset"); + + b.Property("Volume") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(100) + .HasColumnName("volume"); + + b.HasKey("Id") + .HasName("pk_musicplayersettings"); + + b.HasIndex("GuildId") + .IsUnique() + .HasDatabaseName("ix_musicplayersettings_guildid"); + + b.ToTable("musicplayersettings", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MusicPlaylist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("Author") + .HasColumnType("longtext") + .HasColumnName("author"); + + b.Property("AuthorId") + .HasColumnType("bigint unsigned") + .HasColumnName("authorid"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("Name") + .HasColumnType("longtext") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_musicplaylists"); + + b.ToTable("musicplaylists", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("int") + .HasColumnName("guildconfigid"); + + b.Property("UserId") + .HasColumnType("bigint unsigned") + .HasColumnName("userid"); + + b.HasKey("Id") + .HasName("pk_muteduserid"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_muteduserid_guildconfigid"); + + b.ToTable("muteduserid", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.NadekoExpression", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("AllowTarget") + .HasColumnType("tinyint(1)") + .HasColumnName("allowtarget"); + + b.Property("AutoDeleteTrigger") + .HasColumnType("tinyint(1)") + .HasColumnName("autodeletetrigger"); + + b.Property("ContainsAnywhere") + .HasColumnType("tinyint(1)") + .HasColumnName("containsanywhere"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("DmResponse") + .HasColumnType("tinyint(1)") + .HasColumnName("dmresponse"); + + b.Property("GuildId") + .HasColumnType("bigint unsigned") + .HasColumnName("guildid"); + + b.Property("Reactions") + .HasColumnType("longtext") + .HasColumnName("reactions"); + + b.Property("Response") + .HasColumnType("longtext") + .HasColumnName("response"); + + b.Property("Trigger") + .HasColumnType("longtext") + .HasColumnName("trigger"); + + b.HasKey("Id") + .HasName("pk_expressions"); + + b.ToTable("expressions", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklistedTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildId") + .HasColumnType("bigint unsigned") + .HasColumnName("guildid"); + + b.Property("Tag") + .HasColumnType("longtext") + .HasColumnName("tag"); + + b.HasKey("Id") + .HasName("pk_nsfwblacklistedtags"); + + b.HasIndex("GuildId") + .HasDatabaseName("ix_nsfwblacklistedtags_guildid"); + + b.ToTable("nsfwblacklistedtags", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("int") + .HasColumnName("guildconfigid"); + + b.Property("Index") + .HasColumnType("int") + .HasColumnName("index"); + + b.Property("IsCustomCommand") + .HasColumnType("tinyint(1)") + .HasColumnName("iscustomcommand"); + + b.Property("PrimaryTarget") + .HasColumnType("int") + .HasColumnName("primarytarget"); + + b.Property("PrimaryTargetId") + .HasColumnType("bigint unsigned") + .HasColumnName("primarytargetid"); + + b.Property("SecondaryTarget") + .HasColumnType("int") + .HasColumnName("secondarytarget"); + + b.Property("SecondaryTargetName") + .HasColumnType("longtext") + .HasColumnName("secondarytargetname"); + + b.Property("State") + .HasColumnType("tinyint(1)") + .HasColumnName("state"); + + b.HasKey("Id") + .HasName("pk_permissions"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_permissions_guildconfigid"); + + b.ToTable("permissions", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlantedCurrency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("Amount") + .HasColumnType("bigint") + .HasColumnName("amount"); + + b.Property("ChannelId") + .HasColumnType("bigint unsigned") + .HasColumnName("channelid"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildId") + .HasColumnType("bigint unsigned") + .HasColumnName("guildid"); + + b.Property("MessageId") + .HasColumnType("bigint unsigned") + .HasColumnName("messageid"); + + b.Property("Password") + .HasColumnType("longtext") + .HasColumnName("password"); + + b.Property("UserId") + .HasColumnType("bigint unsigned") + .HasColumnName("userid"); + + b.HasKey("Id") + .HasName("pk_plantedcurrency"); + + b.HasIndex("ChannelId") + .HasDatabaseName("ix_plantedcurrency_channelid"); + + b.HasIndex("MessageId") + .IsUnique() + .HasDatabaseName("ix_plantedcurrency_messageid"); + + b.ToTable("plantedcurrency", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("MusicPlaylistId") + .HasColumnType("int") + .HasColumnName("musicplaylistid"); + + b.Property("Provider") + .HasColumnType("longtext") + .HasColumnName("provider"); + + b.Property("ProviderType") + .HasColumnType("int") + .HasColumnName("providertype"); + + b.Property("Query") + .HasColumnType("longtext") + .HasColumnName("query"); + + b.Property("Title") + .HasColumnType("longtext") + .HasColumnName("title"); + + b.Property("Uri") + .HasColumnType("longtext") + .HasColumnName("uri"); + + b.HasKey("Id") + .HasName("pk_playlistsong"); + + b.HasIndex("MusicPlaylistId") + .HasDatabaseName("ix_playlistsong_musicplaylistid"); + + b.ToTable("playlistsong", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Poll", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("ChannelId") + .HasColumnType("bigint unsigned") + .HasColumnName("channelid"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildId") + .HasColumnType("bigint unsigned") + .HasColumnName("guildid"); + + b.Property("Question") + .HasColumnType("longtext") + .HasColumnName("question"); + + b.HasKey("Id") + .HasName("pk_poll"); + + b.HasIndex("GuildId") + .IsUnique() + .HasDatabaseName("ix_poll_guildid"); + + b.ToTable("poll", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PollAnswer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("Index") + .HasColumnType("int") + .HasColumnName("index"); + + b.Property("PollId") + .HasColumnType("int") + .HasColumnName("pollid"); + + b.Property("Text") + .HasColumnType("longtext") + .HasColumnName("text"); + + b.HasKey("Id") + .HasName("pk_pollanswer"); + + b.HasIndex("PollId") + .HasDatabaseName("ix_pollanswer_pollid"); + + b.ToTable("pollanswer", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PollVote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("PollId") + .HasColumnType("int") + .HasColumnName("pollid"); + + b.Property("UserId") + .HasColumnType("bigint unsigned") + .HasColumnName("userid"); + + b.Property("VoteIndex") + .HasColumnType("int") + .HasColumnName("voteindex"); + + b.HasKey("Id") + .HasName("pk_pollvote"); + + b.HasIndex("PollId") + .HasDatabaseName("ix_pollvote_pollid"); + + b.ToTable("pollvote", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Quote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("AuthorId") + .HasColumnType("bigint unsigned") + .HasColumnName("authorid"); + + b.Property("AuthorName") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("authorname"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildId") + .HasColumnType("bigint unsigned") + .HasColumnName("guildid"); + + b.Property("Keyword") + .IsRequired() + .HasColumnType("varchar(255)") + .HasColumnName("keyword"); + + b.Property("Text") + .IsRequired() + .HasColumnType("longtext") + .HasColumnName("text"); + + b.HasKey("Id") + .HasName("pk_quotes"); + + b.HasIndex("GuildId") + .HasDatabaseName("ix_quotes_guildid"); + + b.HasIndex("Keyword") + .HasDatabaseName("ix_quotes_keyword"); + + b.ToTable("quotes", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRoleV2", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("ChannelId") + .HasColumnType("bigint unsigned") + .HasColumnName("channelid"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("Emote") + .HasMaxLength(100) + .HasColumnType("varchar(100)") + .HasColumnName("emote"); + + b.Property("Group") + .HasColumnType("int") + .HasColumnName("group"); + + b.Property("GuildId") + .HasColumnType("bigint unsigned") + .HasColumnName("guildid"); + + b.Property("LevelReq") + .HasColumnType("int") + .HasColumnName("levelreq"); + + b.Property("MessageId") + .HasColumnType("bigint unsigned") + .HasColumnName("messageid"); + + b.Property("RoleId") + .HasColumnType("bigint unsigned") + .HasColumnName("roleid"); + + b.HasKey("Id") + .HasName("pk_reactionroles"); + + b.HasIndex("GuildId") + .HasDatabaseName("ix_reactionroles_guildid"); + + b.HasIndex("MessageId", "Emote") + .IsUnique() + .HasDatabaseName("ix_reactionroles_messageid_emote"); + + b.ToTable("reactionroles", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Reminder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("ChannelId") + .HasColumnType("bigint unsigned") + .HasColumnName("channelid"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("IsPrivate") + .HasColumnType("tinyint(1)") + .HasColumnName("isprivate"); + + b.Property("Message") + .HasColumnType("longtext") + .HasColumnName("message"); + + b.Property("ServerId") + .HasColumnType("bigint unsigned") + .HasColumnName("serverid"); + + b.Property("UserId") + .HasColumnType("bigint unsigned") + .HasColumnName("userid"); + + b.Property("When") + .HasColumnType("datetime(6)") + .HasColumnName("when"); + + b.HasKey("Id") + .HasName("pk_reminders"); + + b.HasIndex("When") + .HasDatabaseName("ix_reminders_when"); + + b.ToTable("reminders", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Repeater", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("ChannelId") + .HasColumnType("bigint unsigned") + .HasColumnName("channelid"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildId") + .HasColumnType("bigint unsigned") + .HasColumnName("guildid"); + + b.Property("Interval") + .HasColumnType("time(6)") + .HasColumnName("interval"); + + b.Property("LastMessageId") + .HasColumnType("bigint unsigned") + .HasColumnName("lastmessageid"); + + b.Property("Message") + .HasColumnType("longtext") + .HasColumnName("message"); + + b.Property("NoRedundant") + .HasColumnType("tinyint(1)") + .HasColumnName("noredundant"); + + b.Property("StartTimeOfDay") + .HasColumnType("time(6)") + .HasColumnName("starttimeofday"); + + b.HasKey("Id") + .HasName("pk_repeaters"); + + b.ToTable("repeaters", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RewardedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("AmountRewardedThisMonth") + .HasColumnType("bigint") + .HasColumnName("amountrewardedthismonth"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("LastReward") + .HasColumnType("datetime(6)") + .HasColumnName("lastreward"); + + b.Property("PlatformUserId") + .HasColumnType("varchar(255)") + .HasColumnName("platformuserid"); + + b.Property("UserId") + .HasColumnType("bigint unsigned") + .HasColumnName("userid"); + + b.HasKey("Id") + .HasName("pk_rewardedusers"); + + b.HasIndex("PlatformUserId") + .IsUnique() + .HasDatabaseName("ix_rewardedusers_platformuserid"); + + b.ToTable("rewardedusers", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RotatingPlayingStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("Status") + .HasColumnType("longtext") + .HasColumnName("status"); + + b.Property("Type") + .HasColumnType("int") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_rotatingstatus"); + + b.ToTable("rotatingstatus", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SelfAssignedRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("Group") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasDefaultValue(0) + .HasColumnName("group"); + + b.Property("GuildId") + .HasColumnType("bigint unsigned") + .HasColumnName("guildid"); + + b.Property("LevelRequirement") + .HasColumnType("int") + .HasColumnName("levelrequirement"); + + b.Property("RoleId") + .HasColumnType("bigint unsigned") + .HasColumnName("roleid"); + + b.HasKey("Id") + .HasName("pk_selfassignableroles"); + + b.HasIndex("GuildId", "RoleId") + .IsUnique() + .HasDatabaseName("ix_selfassignableroles_guildid_roleid"); + + b.ToTable("selfassignableroles", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("AuthorId") + .HasColumnType("bigint unsigned") + .HasColumnName("authorid"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("int") + .HasColumnName("guildconfigid"); + + b.Property("Index") + .HasColumnType("int") + .HasColumnName("index"); + + b.Property("Name") + .HasColumnType("longtext") + .HasColumnName("name"); + + b.Property("Price") + .HasColumnType("int") + .HasColumnName("price"); + + b.Property("RoleId") + .HasColumnType("bigint unsigned") + .HasColumnName("roleid"); + + b.Property("RoleName") + .HasColumnType("longtext") + .HasColumnName("rolename"); + + b.Property("Type") + .HasColumnType("int") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_shopentry"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_shopentry_guildconfigid"); + + b.ToTable("shopentry", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("ShopEntryId") + .HasColumnType("int") + .HasColumnName("shopentryid"); + + b.Property("Text") + .HasColumnType("longtext") + .HasColumnName("text"); + + b.HasKey("Id") + .HasName("pk_shopentryitem"); + + b.HasIndex("ShopEntryId") + .HasDatabaseName("ix_shopentryitem_shopentryid"); + + b.ToTable("shopentryitem", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("int") + .HasColumnName("guildconfigid"); + + b.Property("RoleId") + .HasColumnType("bigint unsigned") + .HasColumnName("roleid"); + + b.HasKey("Id") + .HasName("pk_slowmodeignoredrole"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_slowmodeignoredrole_guildconfigid"); + + b.ToTable("slowmodeignoredrole", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("int") + .HasColumnName("guildconfigid"); + + b.Property("UserId") + .HasColumnType("bigint unsigned") + .HasColumnName("userid"); + + b.HasKey("Id") + .HasName("pk_slowmodeignoreduser"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_slowmodeignoreduser_guildconfigid"); + + b.ToTable("slowmodeignoreduser", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleBlacklistedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("StreamRoleSettingsId") + .HasColumnType("int") + .HasColumnName("streamrolesettingsid"); + + b.Property("UserId") + .HasColumnType("bigint unsigned") + .HasColumnName("userid"); + + b.Property("Username") + .HasColumnType("longtext") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_streamroleblacklisteduser"); + + b.HasIndex("StreamRoleSettingsId") + .HasDatabaseName("ix_streamroleblacklisteduser_streamrolesettingsid"); + + b.ToTable("streamroleblacklisteduser", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("AddRoleId") + .HasColumnType("bigint unsigned") + .HasColumnName("addroleid"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("Enabled") + .HasColumnType("tinyint(1)") + .HasColumnName("enabled"); + + b.Property("FromRoleId") + .HasColumnType("bigint unsigned") + .HasColumnName("fromroleid"); + + b.Property("GuildConfigId") + .HasColumnType("int") + .HasColumnName("guildconfigid"); + + b.Property("Keyword") + .HasColumnType("longtext") + .HasColumnName("keyword"); + + b.HasKey("Id") + .HasName("pk_streamrolesettings"); + + b.HasIndex("GuildConfigId") + .IsUnique() + .HasDatabaseName("ix_streamrolesettings_guildconfigid"); + + b.ToTable("streamrolesettings", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleWhitelistedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("StreamRoleSettingsId") + .HasColumnType("int") + .HasColumnName("streamrolesettingsid"); + + b.Property("UserId") + .HasColumnType("bigint unsigned") + .HasColumnName("userid"); + + b.Property("Username") + .HasColumnType("longtext") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_streamrolewhitelisteduser"); + + b.HasIndex("StreamRoleSettingsId") + .HasDatabaseName("ix_streamrolewhitelisteduser_streamrolesettingsid"); + + b.ToTable("streamrolewhitelisteduser", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnbanTimer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("int") + .HasColumnName("guildconfigid"); + + b.Property("UnbanAt") + .HasColumnType("datetime(6)") + .HasColumnName("unbanat"); + + b.Property("UserId") + .HasColumnType("bigint unsigned") + .HasColumnName("userid"); + + b.HasKey("Id") + .HasName("pk_unbantimer"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_unbantimer_guildconfigid"); + + b.ToTable("unbantimer", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("int") + .HasColumnName("guildconfigid"); + + b.Property("UnmuteAt") + .HasColumnType("datetime(6)") + .HasColumnName("unmuteat"); + + b.Property("UserId") + .HasColumnType("bigint unsigned") + .HasColumnName("userid"); + + b.HasKey("Id") + .HasName("pk_unmutetimer"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_unmutetimer_guildconfigid"); + + b.ToTable("unmutetimer", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnroleTimer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("int") + .HasColumnName("guildconfigid"); + + b.Property("RoleId") + .HasColumnType("bigint unsigned") + .HasColumnName("roleid"); + + b.Property("UnbanAt") + .HasColumnType("datetime(6)") + .HasColumnName("unbanat"); + + b.Property("UserId") + .HasColumnType("bigint unsigned") + .HasColumnName("userid"); + + b.HasKey("Id") + .HasName("pk_unroletimer"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_unroletimer_guildconfigid"); + + b.ToTable("unroletimer", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UserXpStats", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("AwardedXp") + .HasColumnType("bigint") + .HasColumnName("awardedxp"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildId") + .HasColumnType("bigint unsigned") + .HasColumnName("guildid"); + + b.Property("LastLevelUp") + .ValueGeneratedOnAdd() + .HasColumnType("datetime(6)") + .HasColumnName("lastlevelup") + .HasDefaultValueSql("(UTC_TIMESTAMP)"); + + b.Property("NotifyOnLevelUp") + .HasColumnType("int") + .HasColumnName("notifyonlevelup"); + + b.Property("UserId") + .HasColumnType("bigint unsigned") + .HasColumnName("userid"); + + b.Property("Xp") + .HasColumnType("bigint") + .HasColumnName("xp"); + + b.HasKey("Id") + .HasName("pk_userxpstats"); + + b.HasIndex("AwardedXp") + .HasDatabaseName("ix_userxpstats_awardedxp"); + + b.HasIndex("GuildId") + .HasDatabaseName("ix_userxpstats_guildid"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_userxpstats_userid"); + + b.HasIndex("Xp") + .HasDatabaseName("ix_userxpstats_xp"); + + b.HasIndex("UserId", "GuildId") + .IsUnique() + .HasDatabaseName("ix_userxpstats_userid_guildid"); + + b.ToTable("userxpstats", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.VcRoleInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("int") + .HasColumnName("guildconfigid"); + + b.Property("RoleId") + .HasColumnType("bigint unsigned") + .HasColumnName("roleid"); + + b.Property("VoiceChannelId") + .HasColumnType("bigint unsigned") + .HasColumnName("voicechannelid"); + + b.HasKey("Id") + .HasName("pk_vcroleinfo"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_vcroleinfo_guildconfigid"); + + b.ToTable("vcroleinfo", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("AffinityId") + .HasColumnType("int") + .HasColumnName("affinityid"); + + b.Property("ClaimerId") + .HasColumnType("int") + .HasColumnName("claimerid"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("Price") + .HasColumnType("bigint") + .HasColumnName("price"); + + b.Property("WaifuId") + .HasColumnType("int") + .HasColumnName("waifuid"); + + b.HasKey("Id") + .HasName("pk_waifuinfo"); + + b.HasIndex("AffinityId") + .HasDatabaseName("ix_waifuinfo_affinityid"); + + b.HasIndex("ClaimerId") + .HasDatabaseName("ix_waifuinfo_claimerid"); + + b.HasIndex("Price") + .HasDatabaseName("ix_waifuinfo_price"); + + b.HasIndex("WaifuId") + .IsUnique() + .HasDatabaseName("ix_waifuinfo_waifuid"); + + b.ToTable("waifuinfo", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("ItemEmoji") + .HasColumnType("longtext") + .HasColumnName("itememoji"); + + b.Property("Name") + .HasColumnType("longtext") + .HasColumnName("name"); + + b.Property("WaifuInfoId") + .HasColumnType("int") + .HasColumnName("waifuinfoid"); + + b.HasKey("Id") + .HasName("pk_waifuitem"); + + b.HasIndex("WaifuInfoId") + .HasDatabaseName("ix_waifuitem_waifuinfoid"); + + b.ToTable("waifuitem", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuUpdate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("NewId") + .HasColumnType("int") + .HasColumnName("newid"); + + b.Property("OldId") + .HasColumnType("int") + .HasColumnName("oldid"); + + b.Property("UpdateType") + .HasColumnType("int") + .HasColumnName("updatetype"); + + b.Property("UserId") + .HasColumnType("int") + .HasColumnName("userid"); + + b.HasKey("Id") + .HasName("pk_waifuupdates"); + + b.HasIndex("NewId") + .HasDatabaseName("ix_waifuupdates_newid"); + + b.HasIndex("OldId") + .HasDatabaseName("ix_waifuupdates_oldid"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_waifuupdates_userid"); + + b.ToTable("waifuupdates", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Warning", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("Forgiven") + .HasColumnType("tinyint(1)") + .HasColumnName("forgiven"); + + b.Property("ForgivenBy") + .HasColumnType("longtext") + .HasColumnName("forgivenby"); + + b.Property("GuildId") + .HasColumnType("bigint unsigned") + .HasColumnName("guildid"); + + b.Property("Moderator") + .HasColumnType("longtext") + .HasColumnName("moderator"); + + b.Property("Reason") + .HasColumnType("longtext") + .HasColumnName("reason"); + + b.Property("UserId") + .HasColumnType("bigint unsigned") + .HasColumnName("userid"); + + b.Property("Weight") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(1L) + .HasColumnName("weight"); + + b.HasKey("Id") + .HasName("pk_warnings"); + + b.HasIndex("DateAdded") + .HasDatabaseName("ix_warnings_dateadded"); + + b.HasIndex("GuildId") + .HasDatabaseName("ix_warnings_guildid"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_warnings_userid"); + + b.ToTable("warnings", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WarningPunishment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("Count") + .HasColumnType("int") + .HasColumnName("count"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("int") + .HasColumnName("guildconfigid"); + + b.Property("Punishment") + .HasColumnType("int") + .HasColumnName("punishment"); + + b.Property("RoleId") + .HasColumnType("bigint unsigned") + .HasColumnName("roleid"); + + b.Property("Time") + .HasColumnType("int") + .HasColumnName("time"); + + b.HasKey("Id") + .HasName("pk_warningpunishment"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_warningpunishment_guildconfigid"); + + b.ToTable("warningpunishment", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpCurrencyReward", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("Amount") + .HasColumnType("int") + .HasColumnName("amount"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("Level") + .HasColumnType("int") + .HasColumnName("level"); + + b.Property("XpSettingsId") + .HasColumnType("int") + .HasColumnName("xpsettingsid"); + + b.HasKey("Id") + .HasName("pk_xpcurrencyreward"); + + b.HasIndex("XpSettingsId") + .HasDatabaseName("ix_xpcurrencyreward_xpsettingsid"); + + b.ToTable("xpcurrencyreward", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpRoleReward", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("Level") + .HasColumnType("int") + .HasColumnName("level"); + + b.Property("Remove") + .HasColumnType("tinyint(1)") + .HasColumnName("remove"); + + b.Property("RoleId") + .HasColumnType("bigint unsigned") + .HasColumnName("roleid"); + + b.Property("XpSettingsId") + .HasColumnType("int") + .HasColumnName("xpsettingsid"); + + b.HasKey("Id") + .HasName("pk_xprolereward"); + + b.HasIndex("XpSettingsId", "Level") + .IsUnique() + .HasDatabaseName("ix_xprolereward_xpsettingsid_level"); + + b.ToTable("xprolereward", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("int") + .HasColumnName("id"); + + b.Property("DateAdded") + .HasColumnType("datetime(6)") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("int") + .HasColumnName("guildconfigid"); + + b.Property("ServerExcluded") + .HasColumnType("tinyint(1)") + .HasColumnName("serverexcluded"); + + b.HasKey("Id") + .HasName("pk_xpsettings"); + + b.HasIndex("GuildConfigId") + .IsUnique() + .HasDatabaseName("ix_xpsettings_guildconfigid"); + + b.ToTable("xpsettings", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.ClubApplicants", b => + { + b.HasOne("NadekoBot.Db.Models.ClubInfo", "Club") + .WithMany("Applicants") + .HasForeignKey("ClubId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_clubapplicants_clubs_clubid"); + + b.HasOne("NadekoBot.Db.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_clubapplicants_discorduser_userid"); + + b.Navigation("Club"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.ClubBans", b => + { + b.HasOne("NadekoBot.Db.Models.ClubInfo", "Club") + .WithMany("Bans") + .HasForeignKey("ClubId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_clubbans_clubs_clubid"); + + b.HasOne("NadekoBot.Db.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_clubbans_discorduser_userid"); + + b.Navigation("Club"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.ClubInfo", b => + { + b.HasOne("NadekoBot.Db.Models.DiscordUser", "Owner") + .WithOne() + .HasForeignKey("NadekoBot.Db.Models.ClubInfo", "OwnerId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_clubs_discorduser_ownerid"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.DiscordUser", b => + { + b.HasOne("NadekoBot.Db.Models.ClubInfo", "Club") + .WithMany("Members") + .HasForeignKey("ClubId") + .OnDelete(DeleteBehavior.NoAction) + .HasConstraintName("fk_discorduser_clubs_clubid"); + + b.Navigation("Club"); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.FollowedStream", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("FollowedStreams") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_followedstream_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiAltSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithOne("AntiAltSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiAltSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_antialtsetting_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiRaidSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiRaidSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_antiraidsetting_guildconfigs_guildconfigid"); + + b.Navigation("GuildConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.HasOne("NadekoBot.Services.Database.Models.AntiSpamSetting", null) + .WithMany("IgnoredChannels") + .HasForeignKey("AntiSpamSettingId") + .HasConstraintName("fk_antispamignore_antispamsetting_antispamsettingid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiSpamSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiSpamSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_antispamsetting_guildconfigs_guildconfigid"); + + b.Navigation("GuildConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AutoTranslateUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.AutoTranslateChannel", "Channel") + .WithMany("Users") + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_autotranslateusers_autotranslatechannels_channelid"); + + b.Navigation("Channel"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("CommandAliases") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_commandalias_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("CommandCooldowns") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_commandcooldown_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.DelMsgOnCmdChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("DelMsgOnCmdChannels") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_delmsgoncmdchannel_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ExcludedItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.XpSettings", null) + .WithMany("ExclusionList") + .HasForeignKey("XpSettingsId") + .HasConstraintName("fk_excludeditem_xpsettings_xpsettingsid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FeedSub", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithMany("FeedSubs") + .HasForeignKey("GuildConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_feedsub_guildconfigs_guildconfigid"); + + b.Navigation("GuildConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("FilterInvitesChannelIds") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_filterchannelid_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("FilteredWords") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_filteredword_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterLinksChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("FilterLinksChannelIds") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_filterlinkschannelid_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterWordsChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("FilterWordsChannelIds") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_filterwordschannelid_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithMany("GenerateCurrencyChannelIds") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_gcchannelid_guildconfigs_guildconfigid"); + + b.Navigation("GuildConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GroupName", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithMany("SelfAssignableRoleGroupNames") + .HasForeignKey("GuildConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_groupname_guildconfigs_guildconfigid"); + + b.Navigation("GuildConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany("LogIgnores") + .HasForeignKey("LogSettingId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_ignoredlogchannels_logsettings_logsettingid"); + + b.Navigation("LogSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany() + .HasForeignKey("LogSettingId") + .HasConstraintName("fk_ignoredvoicepresencechannels_logsettings_logsettingid"); + + b.Navigation("LogSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("MutedUsers") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_muteduserid_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("Permissions") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_permissions_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.HasOne("NadekoBot.Services.Database.Models.MusicPlaylist", null) + .WithMany("Songs") + .HasForeignKey("MusicPlaylistId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_playlistsong_musicplaylists_musicplaylistid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PollAnswer", b => + { + b.HasOne("NadekoBot.Services.Database.Models.Poll", null) + .WithMany("Answers") + .HasForeignKey("PollId") + .HasConstraintName("fk_pollanswer_poll_pollid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PollVote", b => + { + b.HasOne("NadekoBot.Services.Database.Models.Poll", null) + .WithMany("Votes") + .HasForeignKey("PollId") + .HasConstraintName("fk_pollvote_poll_pollid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("ShopEntries") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_shopentry_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntryItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ShopEntry", null) + .WithMany("Items") + .HasForeignKey("ShopEntryId") + .HasConstraintName("fk_shopentryitem_shopentry_shopentryid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredRole", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("SlowmodeIgnoredRoles") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_slowmodeignoredrole_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("SlowmodeIgnoredUsers") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_slowmodeignoreduser_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleBlacklistedUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.StreamRoleSettings", null) + .WithMany("Blacklist") + .HasForeignKey("StreamRoleSettingsId") + .HasConstraintName("fk_streamroleblacklisteduser_streamrolesettings_streamrolesetti~"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("StreamRole") + .HasForeignKey("NadekoBot.Services.Database.Models.StreamRoleSettings", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_streamrolesettings_guildconfigs_guildconfigid"); + + b.Navigation("GuildConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleWhitelistedUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.StreamRoleSettings", null) + .WithMany("Whitelist") + .HasForeignKey("StreamRoleSettingsId") + .HasConstraintName("fk_streamrolewhitelisteduser_streamrolesettings_streamrolesetti~"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnbanTimer", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("UnbanTimer") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_unbantimer_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("UnmuteTimers") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_unmutetimer_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnroleTimer", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("UnroleTimer") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_unroletimer_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.VcRoleInfo", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("VcRoleInfos") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_vcroleinfo_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.HasOne("NadekoBot.Db.Models.DiscordUser", "Affinity") + .WithMany() + .HasForeignKey("AffinityId") + .HasConstraintName("fk_waifuinfo_discorduser_affinityid"); + + b.HasOne("NadekoBot.Db.Models.DiscordUser", "Claimer") + .WithMany() + .HasForeignKey("ClaimerId") + .HasConstraintName("fk_waifuinfo_discorduser_claimerid"); + + b.HasOne("NadekoBot.Db.Models.DiscordUser", "Waifu") + .WithOne() + .HasForeignKey("NadekoBot.Services.Database.Models.WaifuInfo", "WaifuId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_waifuinfo_discorduser_waifuid"); + + b.Navigation("Affinity"); + + b.Navigation("Claimer"); + + b.Navigation("Waifu"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.WaifuInfo", "WaifuInfo") + .WithMany("Items") + .HasForeignKey("WaifuInfoId") + .HasConstraintName("fk_waifuitem_waifuinfo_waifuinfoid"); + + b.Navigation("WaifuInfo"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuUpdate", b => + { + b.HasOne("NadekoBot.Db.Models.DiscordUser", "New") + .WithMany() + .HasForeignKey("NewId") + .HasConstraintName("fk_waifuupdates_discorduser_newid"); + + b.HasOne("NadekoBot.Db.Models.DiscordUser", "Old") + .WithMany() + .HasForeignKey("OldId") + .HasConstraintName("fk_waifuupdates_discorduser_oldid"); + + b.HasOne("NadekoBot.Db.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_waifuupdates_discorduser_userid"); + + b.Navigation("New"); + + b.Navigation("Old"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WarningPunishment", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("WarnPunishments") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_warningpunishment_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpCurrencyReward", b => + { + b.HasOne("NadekoBot.Services.Database.Models.XpSettings", "XpSettings") + .WithMany("CurrencyRewards") + .HasForeignKey("XpSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xpcurrencyreward_xpsettings_xpsettingsid"); + + b.Navigation("XpSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpRoleReward", b => + { + b.HasOne("NadekoBot.Services.Database.Models.XpSettings", "XpSettings") + .WithMany("RoleRewards") + .HasForeignKey("XpSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xprolereward_xpsettings_xpsettingsid"); + + b.Navigation("XpSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpSettings", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("XpSettings") + .HasForeignKey("NadekoBot.Services.Database.Models.XpSettings", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xpsettings_guildconfigs_guildconfigid"); + + b.Navigation("GuildConfig"); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.ClubInfo", b => + { + b.Navigation("Applicants"); + + b.Navigation("Bans"); + + b.Navigation("Members"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.Navigation("IgnoredChannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AutoTranslateChannel", b => + { + b.Navigation("Users"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.Navigation("AntiAltSetting"); + + b.Navigation("AntiRaidSetting"); + + b.Navigation("AntiSpamSetting"); + + b.Navigation("CommandAliases"); + + b.Navigation("CommandCooldowns"); + + b.Navigation("DelMsgOnCmdChannels"); + + b.Navigation("FeedSubs"); + + b.Navigation("FilterInvitesChannelIds"); + + b.Navigation("FilterLinksChannelIds"); + + b.Navigation("FilterWordsChannelIds"); + + b.Navigation("FilteredWords"); + + b.Navigation("FollowedStreams"); + + b.Navigation("GenerateCurrencyChannelIds"); + + b.Navigation("MutedUsers"); + + b.Navigation("Permissions"); + + b.Navigation("SelfAssignableRoleGroupNames"); + + b.Navigation("ShopEntries"); + + b.Navigation("SlowmodeIgnoredRoles"); + + b.Navigation("SlowmodeIgnoredUsers"); + + b.Navigation("StreamRole"); + + b.Navigation("UnbanTimer"); + + b.Navigation("UnmuteTimers"); + + b.Navigation("UnroleTimer"); + + b.Navigation("VcRoleInfos"); + + b.Navigation("WarnPunishments"); + + b.Navigation("XpSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.LogSetting", b => + { + b.Navigation("LogIgnores"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MusicPlaylist", b => + { + b.Navigation("Songs"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Poll", b => + { + b.Navigation("Answers"); + + b.Navigation("Votes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.Navigation("Blacklist"); + + b.Navigation("Whitelist"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpSettings", b => + { + b.Navigation("CurrencyRewards"); + + b.Navigation("ExclusionList"); + + b.Navigation("RoleRewards"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/NadekoBot/Migrations/MySql/20220614071410_patronage-system.cs b/src/NadekoBot/Migrations/MySql/20220614071410_patronage-system.cs new file mode 100644 index 000000000..7eb0bde01 --- /dev/null +++ b/src/NadekoBot/Migrations/MySql/20220614071410_patronage-system.cs @@ -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( + name: "xp", + table: "userxpstats", + type: "bigint", + nullable: false, + oldClrType: typeof(int), + oldType: "int"); + + migrationBuilder.AlterColumn( + name: "awardedxp", + table: "userxpstats", + type: "bigint", + nullable: false, + oldClrType: typeof(int), + oldType: "int"); + + migrationBuilder.AlterColumn( + name: "amountrewardedthismonth", + table: "rewardedusers", + type: "bigint", + nullable: false, + oldClrType: typeof(int), + oldType: "int"); + + migrationBuilder.AlterColumn( + name: "verboseerrors", + table: "guildconfigs", + type: "tinyint(1)", + nullable: false, + defaultValue: true, + oldClrType: typeof(bool), + oldType: "tinyint(1)"); + + migrationBuilder.AlterColumn( + 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(type: "bigint unsigned", nullable: false), + featuretype = table.Column(type: "int", nullable: false), + feature = table.Column(type: "varchar(255)", nullable: false) + .Annotation("MySql:CharSet", "utf8mb4"), + hourlycount = table.Column(type: "int unsigned", nullable: false), + dailycount = table.Column(type: "int unsigned", nullable: false), + monthlycount = table.Column(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(type: "bigint unsigned", nullable: false) + .Annotation("MySql:ValueGenerationStrategy", MySqlValueGenerationStrategy.IdentityColumn), + uniqueplatformuserid = table.Column(type: "varchar(255)", nullable: true) + .Annotation("MySql:CharSet", "utf8mb4"), + amountcents = table.Column(type: "int", nullable: false), + lastcharge = table.Column(type: "datetime(6)", nullable: false), + validthru = table.Column(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( + name: "xp", + table: "userxpstats", + type: "int", + nullable: false, + oldClrType: typeof(long), + oldType: "bigint"); + + migrationBuilder.AlterColumn( + name: "awardedxp", + table: "userxpstats", + type: "int", + nullable: false, + oldClrType: typeof(long), + oldType: "bigint"); + + migrationBuilder.AlterColumn( + name: "amountrewardedthismonth", + table: "rewardedusers", + type: "int", + nullable: false, + oldClrType: typeof(long), + oldType: "bigint"); + + migrationBuilder.AlterColumn( + name: "verboseerrors", + table: "guildconfigs", + type: "tinyint(1)", + nullable: false, + oldClrType: typeof(bool), + oldType: "tinyint(1)", + oldDefaultValue: true); + + migrationBuilder.AlterColumn( + name: "totalxp", + table: "discorduser", + type: "int", + nullable: false, + defaultValue: 0, + oldClrType: typeof(long), + oldType: "bigint", + oldDefaultValue: 0L); + } + } +} diff --git a/src/NadekoBot/Migrations/MySql/MysqlContextModelSnapshot.cs b/src/NadekoBot/Migrations/MySql/MysqlContextModelSnapshot.cs index f9e687e08..28f3cb7b2 100644 --- a/src/NadekoBot/Migrations/MySql/MysqlContextModelSnapshot.cs +++ b/src/NadekoBot/Migrations/MySql/MysqlContextModelSnapshot.cs @@ -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("TotalXp") + b.Property("TotalXp") .ValueGeneratedOnAdd() - .HasColumnType("int") - .HasDefaultValue(0) + .HasColumnType("bigint") + .HasDefaultValue(0L) .HasColumnName("totalxp"); b.Property("UserId") @@ -265,6 +265,74 @@ namespace NadekoBot.Migrations.Mysql b.ToTable("followedstream", (string)null); }); + modelBuilder.Entity("NadekoBot.Db.Models.PatronQuota", b => + { + b.Property("UserId") + .HasColumnType("bigint unsigned") + .HasColumnName("userid"); + + b.Property("FeatureType") + .HasColumnType("int") + .HasColumnName("featuretype"); + + b.Property("Feature") + .HasColumnType("varchar(255)") + .HasColumnName("feature"); + + b.Property("DailyCount") + .HasColumnType("int unsigned") + .HasColumnName("dailycount"); + + b.Property("HourlyCount") + .HasColumnType("int unsigned") + .HasColumnName("hourlycount"); + + b.Property("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("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("bigint unsigned") + .HasColumnName("userid"); + + b.Property("AmountCents") + .HasColumnType("int") + .HasColumnName("amountcents"); + + b.Property("LastCharge") + .HasColumnType("datetime(6)") + .HasColumnName("lastcharge"); + + b.Property("UniquePlatformUserId") + .HasColumnType("varchar(255)") + .HasColumnName("uniqueplatformuserid"); + + b.Property("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("Id") @@ -1138,7 +1206,9 @@ namespace NadekoBot.Migrations.Mysql .HasColumnName("timezoneid"); b.Property("VerboseErrors") + .ValueGeneratedOnAdd() .HasColumnType("tinyint(1)") + .HasDefaultValue(true) .HasColumnName("verboseerrors"); b.Property("VerbosePermissions") @@ -1962,8 +2032,8 @@ namespace NadekoBot.Migrations.Mysql .HasColumnType("int") .HasColumnName("id"); - b.Property("AmountRewardedThisMonth") - .HasColumnType("int") + b.Property("AmountRewardedThisMonth") + .HasColumnType("bigint") .HasColumnName("amountrewardedthismonth"); b.Property("DateAdded") @@ -1974,9 +2044,9 @@ namespace NadekoBot.Migrations.Mysql .HasColumnType("datetime(6)") .HasColumnName("lastreward"); - b.Property("PatreonUserId") + b.Property("PlatformUserId") .HasColumnType("varchar(255)") - .HasColumnName("patreonuserid"); + .HasColumnName("platformuserid"); b.Property("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("AwardedXp") - .HasColumnType("int") + b.Property("AwardedXp") + .HasColumnType("bigint") .HasColumnName("awardedxp"); b.Property("DateAdded") @@ -2430,8 +2500,8 @@ namespace NadekoBot.Migrations.Mysql .HasColumnType("bigint unsigned") .HasColumnName("userid"); - b.Property("Xp") - .HasColumnType("int") + b.Property("Xp") + .HasColumnType("bigint") .HasColumnName("xp"); b.HasKey("Id") diff --git a/src/NadekoBot/Migrations/Postgresql/20220614071421_patronage-system.Designer.cs b/src/NadekoBot/Migrations/Postgresql/20220614071421_patronage-system.Designer.cs new file mode 100644 index 000000000..86ef22544 --- /dev/null +++ b/src/NadekoBot/Migrations/Postgresql/20220614071421_patronage-system.Designer.cs @@ -0,0 +1,3621 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NadekoBot.Services.Database; +using Npgsql.EntityFrameworkCore.PostgreSQL.Metadata; + +#nullable disable + +namespace NadekoBot.Migrations.PostgreSql +{ + [DbContext(typeof(PostgreSqlContext))] + [Migration("20220614071421_patronage-system")] + partial class patronagesystem + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder + .HasAnnotation("ProductVersion", "6.0.5") + .HasAnnotation("Relational:MaxIdentifierLength", 63); + + NpgsqlModelBuilderExtensions.UseIdentityByDefaultColumns(modelBuilder); + + modelBuilder.Entity("NadekoBot.Db.Models.BankUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Balance") + .HasColumnType("bigint") + .HasColumnName("balance"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("UserId") + .HasColumnType("numeric(20,0)") + .HasColumnName("userid"); + + b.HasKey("Id") + .HasName("pk_bankusers"); + + b.HasIndex("UserId") + .IsUnique() + .HasDatabaseName("ix_bankusers_userid"); + + b.ToTable("bankusers", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.ClubApplicants", b => + { + b.Property("ClubId") + .HasColumnType("integer") + .HasColumnName("clubid"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("userid"); + + b.HasKey("ClubId", "UserId") + .HasName("pk_clubapplicants"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_clubapplicants_userid"); + + b.ToTable("clubapplicants", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.ClubBans", b => + { + b.Property("ClubId") + .HasColumnType("integer") + .HasColumnName("clubid"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("userid"); + + b.HasKey("ClubId", "UserId") + .HasName("pk_clubbans"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_clubbans_userid"); + + b.ToTable("clubbans", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.ClubInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("Description") + .HasColumnType("text") + .HasColumnName("description"); + + b.Property("ImageUrl") + .HasColumnType("text") + .HasColumnName("imageurl"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("character varying(20)") + .HasColumnName("name"); + + b.Property("OwnerId") + .HasColumnType("integer") + .HasColumnName("ownerid"); + + b.Property("Xp") + .HasColumnType("integer") + .HasColumnName("xp"); + + b.HasKey("Id") + .HasName("pk_clubs"); + + b.HasAlternateKey("Name") + .HasName("ak_clubs_name"); + + b.HasIndex("OwnerId") + .IsUnique() + .HasDatabaseName("ix_clubs_ownerid"); + + b.ToTable("clubs", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.DiscordUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AvatarId") + .HasColumnType("text") + .HasColumnName("avatarid"); + + b.Property("ClubId") + .HasColumnType("integer") + .HasColumnName("clubid"); + + b.Property("CurrencyAmount") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(0L) + .HasColumnName("currencyamount"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("Discriminator") + .HasColumnType("text") + .HasColumnName("discriminator"); + + b.Property("IsClubAdmin") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(false) + .HasColumnName("isclubadmin"); + + b.Property("LastLevelUp") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("lastlevelup") + .HasDefaultValueSql("timezone('utc', now())"); + + b.Property("LastXpGain") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("lastxpgain") + .HasDefaultValueSql("timezone('utc', now()) - interval '-1 year'"); + + b.Property("NotifyOnLevelUp") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("notifyonlevelup"); + + b.Property("TotalXp") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(0L) + .HasColumnName("totalxp"); + + b.Property("UserId") + .HasColumnType("numeric(20,0)") + .HasColumnName("userid"); + + b.Property("Username") + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_discorduser"); + + b.HasAlternateKey("UserId") + .HasName("ak_discorduser_userid"); + + b.HasIndex("ClubId") + .HasDatabaseName("ix_discorduser_clubid"); + + b.HasIndex("CurrencyAmount") + .HasDatabaseName("ix_discorduser_currencyamount"); + + b.HasIndex("TotalXp") + .HasDatabaseName("ix_discorduser_totalxp"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_discorduser_userid"); + + b.ToTable("discorduser", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.FollowedStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelId") + .HasColumnType("numeric(20,0)") + .HasColumnName("channelid"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("integer") + .HasColumnName("guildconfigid"); + + b.Property("GuildId") + .HasColumnType("numeric(20,0)") + .HasColumnName("guildid"); + + b.Property("Message") + .HasColumnType("text") + .HasColumnName("message"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.Property("Username") + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_followedstream"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_followedstream_guildconfigid"); + + b.ToTable("followedstream", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.PatronQuota", b => + { + b.Property("UserId") + .HasColumnType("numeric(20,0)") + .HasColumnName("userid"); + + b.Property("FeatureType") + .HasColumnType("integer") + .HasColumnName("featuretype"); + + b.Property("Feature") + .HasColumnType("text") + .HasColumnName("feature"); + + b.Property("DailyCount") + .HasColumnType("bigint") + .HasColumnName("dailycount"); + + b.Property("HourlyCount") + .HasColumnType("bigint") + .HasColumnName("hourlycount"); + + b.Property("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("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("userid"); + + b.Property("AmountCents") + .HasColumnType("integer") + .HasColumnName("amountcents"); + + b.Property("LastCharge") + .HasColumnType("timestamp with time zone") + .HasColumnName("lastcharge"); + + b.Property("UniquePlatformUserId") + .HasColumnType("text") + .HasColumnName("uniqueplatformuserid"); + + b.Property("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("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .HasColumnType("integer") + .HasColumnName("action"); + + b.Property("ActionDurationMinutes") + .HasColumnType("integer") + .HasColumnName("actiondurationminutes"); + + b.Property("GuildConfigId") + .HasColumnType("integer") + .HasColumnName("guildconfigid"); + + b.Property("MinAge") + .HasColumnType("interval") + .HasColumnName("minage"); + + b.Property("RoleId") + .HasColumnType("numeric(20,0)") + .HasColumnName("roleid"); + + b.HasKey("Id") + .HasName("pk_antialtsetting"); + + b.HasIndex("GuildConfigId") + .IsUnique() + .HasDatabaseName("ix_antialtsetting_guildconfigid"); + + b.ToTable("antialtsetting", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .HasColumnType("integer") + .HasColumnName("action"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("integer") + .HasColumnName("guildconfigid"); + + b.Property("PunishDuration") + .HasColumnType("integer") + .HasColumnName("punishduration"); + + b.Property("Seconds") + .HasColumnType("integer") + .HasColumnName("seconds"); + + b.Property("UserThreshold") + .HasColumnType("integer") + .HasColumnName("userthreshold"); + + b.HasKey("Id") + .HasName("pk_antiraidsetting"); + + b.HasIndex("GuildConfigId") + .IsUnique() + .HasDatabaseName("ix_antiraidsetting_guildconfigid"); + + b.ToTable("antiraidsetting", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AntiSpamSettingId") + .HasColumnType("integer") + .HasColumnName("antispamsettingid"); + + b.Property("ChannelId") + .HasColumnType("numeric(20,0)") + .HasColumnName("channelid"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.HasKey("Id") + .HasName("pk_antispamignore"); + + b.HasIndex("AntiSpamSettingId") + .HasDatabaseName("ix_antispamignore_antispamsettingid"); + + b.ToTable("antispamignore", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Action") + .HasColumnType("integer") + .HasColumnName("action"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("integer") + .HasColumnName("guildconfigid"); + + b.Property("MessageThreshold") + .HasColumnType("integer") + .HasColumnName("messagethreshold"); + + b.Property("MuteTime") + .HasColumnType("integer") + .HasColumnName("mutetime"); + + b.Property("RoleId") + .HasColumnType("numeric(20,0)") + .HasColumnName("roleid"); + + b.HasKey("Id") + .HasName("pk_antispamsetting"); + + b.HasIndex("GuildConfigId") + .IsUnique() + .HasDatabaseName("ix_antispamsetting_guildconfigid"); + + b.ToTable("antispamsetting", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AutoCommand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelId") + .HasColumnType("numeric(20,0)") + .HasColumnName("channelid"); + + b.Property("ChannelName") + .HasColumnType("text") + .HasColumnName("channelname"); + + b.Property("CommandText") + .HasColumnType("text") + .HasColumnName("commandtext"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildId") + .HasColumnType("numeric(20,0)") + .HasColumnName("guildid"); + + b.Property("GuildName") + .HasColumnType("text") + .HasColumnName("guildname"); + + b.Property("Interval") + .HasColumnType("integer") + .HasColumnName("interval"); + + b.Property("VoiceChannelId") + .HasColumnType("numeric(20,0)") + .HasColumnName("voicechannelid"); + + b.Property("VoiceChannelName") + .HasColumnType("text") + .HasColumnName("voicechannelname"); + + b.HasKey("Id") + .HasName("pk_autocommands"); + + b.ToTable("autocommands", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AutoTranslateChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AutoDelete") + .HasColumnType("boolean") + .HasColumnName("autodelete"); + + b.Property("ChannelId") + .HasColumnType("numeric(20,0)") + .HasColumnName("channelid"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildId") + .HasColumnType("numeric(20,0)") + .HasColumnName("guildid"); + + b.HasKey("Id") + .HasName("pk_autotranslatechannels"); + + b.HasIndex("ChannelId") + .IsUnique() + .HasDatabaseName("ix_autotranslatechannels_channelid"); + + b.HasIndex("GuildId") + .HasDatabaseName("ix_autotranslatechannels_guildid"); + + b.ToTable("autotranslatechannels", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AutoTranslateUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelId") + .HasColumnType("integer") + .HasColumnName("channelid"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("Source") + .HasColumnType("text") + .HasColumnName("source"); + + b.Property("Target") + .HasColumnType("text") + .HasColumnName("target"); + + b.Property("UserId") + .HasColumnType("numeric(20,0)") + .HasColumnName("userid"); + + b.HasKey("Id") + .HasName("pk_autotranslateusers"); + + b.HasAlternateKey("ChannelId", "UserId") + .HasName("ak_autotranslateusers_channelid_userid"); + + b.ToTable("autotranslateusers", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BanTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildId") + .HasColumnType("numeric(20,0)") + .HasColumnName("guildid"); + + b.Property("Text") + .HasColumnType("text") + .HasColumnName("text"); + + b.HasKey("Id") + .HasName("pk_bantemplates"); + + b.HasIndex("GuildId") + .IsUnique() + .HasDatabaseName("ix_bantemplates_guildid"); + + b.ToTable("bantemplates", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("ItemId") + .HasColumnType("numeric(20,0)") + .HasColumnName("itemid"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_blacklist"); + + b.ToTable("blacklist", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("integer") + .HasColumnName("guildconfigid"); + + b.Property("Mapping") + .HasColumnType("text") + .HasColumnName("mapping"); + + b.Property("Trigger") + .HasColumnType("text") + .HasColumnName("trigger"); + + b.HasKey("Id") + .HasName("pk_commandalias"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_commandalias_guildconfigid"); + + b.ToTable("commandalias", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("CommandName") + .HasColumnType("text") + .HasColumnName("commandname"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("integer") + .HasColumnName("guildconfigid"); + + b.Property("Seconds") + .HasColumnType("integer") + .HasColumnName("seconds"); + + b.HasKey("Id") + .HasName("pk_commandcooldown"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_commandcooldown_guildconfigid"); + + b.ToTable("commandcooldown", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CurrencyTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("bigint") + .HasColumnName("amount"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("Extra") + .IsRequired() + .HasColumnType("text") + .HasColumnName("extra"); + + b.Property("Note") + .HasColumnType("text") + .HasColumnName("note"); + + b.Property("OtherId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("otherid") + .HasDefaultValueSql("NULL"); + + b.Property("Type") + .IsRequired() + .HasColumnType("text") + .HasColumnName("type"); + + b.Property("UserId") + .HasColumnType("numeric(20,0)") + .HasColumnName("userid"); + + b.HasKey("Id") + .HasName("pk_currencytransactions"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_currencytransactions_userid"); + + b.ToTable("currencytransactions", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.DelMsgOnCmdChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelId") + .HasColumnType("numeric(20,0)") + .HasColumnName("channelid"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("integer") + .HasColumnName("guildconfigid"); + + b.Property("State") + .HasColumnType("boolean") + .HasColumnName("state"); + + b.HasKey("Id") + .HasName("pk_delmsgoncmdchannel"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_delmsgoncmdchannel_guildconfigid"); + + b.ToTable("delmsgoncmdchannel", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.DiscordPermOverride", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Command") + .HasColumnType("text") + .HasColumnName("command"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildId") + .HasColumnType("numeric(20,0)") + .HasColumnName("guildid"); + + b.Property("Perm") + .HasColumnType("numeric(20,0)") + .HasColumnName("perm"); + + b.HasKey("Id") + .HasName("pk_discordpermoverrides"); + + b.HasIndex("GuildId", "Command") + .IsUnique() + .HasDatabaseName("ix_discordpermoverrides_guildid_command"); + + b.ToTable("discordpermoverrides", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ExcludedItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("ItemId") + .HasColumnType("numeric(20,0)") + .HasColumnName("itemid"); + + b.Property("ItemType") + .HasColumnType("integer") + .HasColumnName("itemtype"); + + b.Property("XpSettingsId") + .HasColumnType("integer") + .HasColumnName("xpsettingsid"); + + b.HasKey("Id") + .HasName("pk_excludeditem"); + + b.HasIndex("XpSettingsId") + .HasDatabaseName("ix_excludeditem_xpsettingsid"); + + b.ToTable("excludeditem", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FeedSub", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelId") + .HasColumnType("numeric(20,0)") + .HasColumnName("channelid"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("integer") + .HasColumnName("guildconfigid"); + + b.Property("Url") + .IsRequired() + .HasColumnType("text") + .HasColumnName("url"); + + b.HasKey("Id") + .HasName("pk_feedsub"); + + b.HasAlternateKey("GuildConfigId", "Url") + .HasName("ak_feedsub_guildconfigid_url"); + + b.ToTable("feedsub", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelId") + .HasColumnType("numeric(20,0)") + .HasColumnName("channelid"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("integer") + .HasColumnName("guildconfigid"); + + b.HasKey("Id") + .HasName("pk_filterchannelid"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_filterchannelid_guildconfigid"); + + b.ToTable("filterchannelid", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("integer") + .HasColumnName("guildconfigid"); + + b.Property("Word") + .HasColumnType("text") + .HasColumnName("word"); + + b.HasKey("Id") + .HasName("pk_filteredword"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_filteredword_guildconfigid"); + + b.ToTable("filteredword", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterLinksChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelId") + .HasColumnType("numeric(20,0)") + .HasColumnName("channelid"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("integer") + .HasColumnName("guildconfigid"); + + b.HasKey("Id") + .HasName("pk_filterlinkschannelid"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_filterlinkschannelid_guildconfigid"); + + b.ToTable("filterlinkschannelid", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterWordsChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelId") + .HasColumnType("numeric(20,0)") + .HasColumnName("channelid"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("integer") + .HasColumnName("guildconfigid"); + + b.HasKey("Id") + .HasName("pk_filterwordschannelid"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_filterwordschannelid_guildconfigid"); + + b.ToTable("filterwordschannelid", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelId") + .HasColumnType("numeric(20,0)") + .HasColumnName("channelid"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("integer") + .HasColumnName("guildconfigid"); + + b.HasKey("Id") + .HasName("pk_gcchannelid"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_gcchannelid_guildconfigid"); + + b.ToTable("gcchannelid", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GroupName", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("integer") + .HasColumnName("guildconfigid"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Number") + .HasColumnType("integer") + .HasColumnName("number"); + + b.HasKey("Id") + .HasName("pk_groupname"); + + b.HasIndex("GuildConfigId", "Number") + .IsUnique() + .HasDatabaseName("ix_groupname_guildconfigid_number"); + + b.ToTable("groupname", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AutoAssignRoleIds") + .HasColumnType("text") + .HasColumnName("autoassignroleids"); + + b.Property("AutoDeleteByeMessagesTimer") + .HasColumnType("integer") + .HasColumnName("autodeletebyemessagestimer"); + + b.Property("AutoDeleteGreetMessagesTimer") + .HasColumnType("integer") + .HasColumnName("autodeletegreetmessagestimer"); + + b.Property("AutoDeleteSelfAssignedRoleMessages") + .HasColumnType("boolean") + .HasColumnName("autodeleteselfassignedrolemessages"); + + b.Property("BoostMessage") + .HasColumnType("text") + .HasColumnName("boostmessage"); + + b.Property("BoostMessageChannelId") + .HasColumnType("numeric(20,0)") + .HasColumnName("boostmessagechannelid"); + + b.Property("BoostMessageDeleteAfter") + .HasColumnType("integer") + .HasColumnName("boostmessagedeleteafter"); + + b.Property("ByeMessageChannelId") + .HasColumnType("numeric(20,0)") + .HasColumnName("byemessagechannelid"); + + b.Property("ChannelByeMessageText") + .HasColumnType("text") + .HasColumnName("channelbyemessagetext"); + + b.Property("ChannelGreetMessageText") + .HasColumnType("text") + .HasColumnName("channelgreetmessagetext"); + + b.Property("CleverbotEnabled") + .HasColumnType("boolean") + .HasColumnName("cleverbotenabled"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("DeleteMessageOnCommand") + .HasColumnType("boolean") + .HasColumnName("deletemessageoncommand"); + + b.Property("DeleteStreamOnlineMessage") + .HasColumnType("boolean") + .HasColumnName("deletestreamonlinemessage"); + + b.Property("DmGreetMessageText") + .HasColumnType("text") + .HasColumnName("dmgreetmessagetext"); + + b.Property("ExclusiveSelfAssignedRoles") + .HasColumnType("boolean") + .HasColumnName("exclusiveselfassignedroles"); + + b.Property("FilterInvites") + .HasColumnType("boolean") + .HasColumnName("filterinvites"); + + b.Property("FilterLinks") + .HasColumnType("boolean") + .HasColumnName("filterlinks"); + + b.Property("FilterWords") + .HasColumnType("boolean") + .HasColumnName("filterwords"); + + b.Property("GameVoiceChannel") + .HasColumnType("numeric(20,0)") + .HasColumnName("gamevoicechannel"); + + b.Property("GreetMessageChannelId") + .HasColumnType("numeric(20,0)") + .HasColumnName("greetmessagechannelid"); + + b.Property("GuildId") + .HasColumnType("numeric(20,0)") + .HasColumnName("guildid"); + + b.Property("Locale") + .HasColumnType("text") + .HasColumnName("locale"); + + b.Property("MuteRoleName") + .HasColumnType("text") + .HasColumnName("muterolename"); + + b.Property("NotifyStreamOffline") + .HasColumnType("boolean") + .HasColumnName("notifystreamoffline"); + + b.Property("PermissionRole") + .HasColumnType("text") + .HasColumnName("permissionrole"); + + b.Property("Prefix") + .HasColumnType("text") + .HasColumnName("prefix"); + + b.Property("SendBoostMessage") + .HasColumnType("boolean") + .HasColumnName("sendboostmessage"); + + b.Property("SendChannelByeMessage") + .HasColumnType("boolean") + .HasColumnName("sendchannelbyemessage"); + + b.Property("SendChannelGreetMessage") + .HasColumnType("boolean") + .HasColumnName("sendchannelgreetmessage"); + + b.Property("SendDmGreetMessage") + .HasColumnType("boolean") + .HasColumnName("senddmgreetmessage"); + + b.Property("TimeZoneId") + .HasColumnType("text") + .HasColumnName("timezoneid"); + + b.Property("VerboseErrors") + .ValueGeneratedOnAdd() + .HasColumnType("boolean") + .HasDefaultValue(true) + .HasColumnName("verboseerrors"); + + b.Property("VerbosePermissions") + .HasColumnType("boolean") + .HasColumnName("verbosepermissions"); + + b.Property("WarnExpireAction") + .HasColumnType("integer") + .HasColumnName("warnexpireaction"); + + b.Property("WarnExpireHours") + .HasColumnType("integer") + .HasColumnName("warnexpirehours"); + + b.Property("WarningsInitialized") + .HasColumnType("boolean") + .HasColumnName("warningsinitialized"); + + b.HasKey("Id") + .HasName("pk_guildconfigs"); + + b.HasIndex("GuildId") + .IsUnique() + .HasDatabaseName("ix_guildconfigs_guildid"); + + b.HasIndex("WarnExpireHours") + .HasDatabaseName("ix_guildconfigs_warnexpirehours"); + + b.ToTable("guildconfigs", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("ItemType") + .HasColumnType("integer") + .HasColumnName("itemtype"); + + b.Property("LogItemId") + .HasColumnType("numeric(20,0)") + .HasColumnName("logitemid"); + + b.Property("LogSettingId") + .HasColumnType("integer") + .HasColumnName("logsettingid"); + + b.HasKey("Id") + .HasName("pk_ignoredlogchannels"); + + b.HasIndex("LogSettingId", "LogItemId", "ItemType") + .IsUnique() + .HasDatabaseName("ix_ignoredlogchannels_logsettingid_logitemid_itemtype"); + + b.ToTable("ignoredlogchannels", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelId") + .HasColumnType("numeric(20,0)") + .HasColumnName("channelid"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("LogSettingId") + .HasColumnType("integer") + .HasColumnName("logsettingid"); + + b.HasKey("Id") + .HasName("pk_ignoredvoicepresencechannels"); + + b.HasIndex("LogSettingId") + .HasDatabaseName("ix_ignoredvoicepresencechannels_logsettingid"); + + b.ToTable("ignoredvoicepresencechannels", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ImageOnlyChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelId") + .HasColumnType("numeric(20,0)") + .HasColumnName("channelid"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildId") + .HasColumnType("numeric(20,0)") + .HasColumnName("guildid"); + + b.HasKey("Id") + .HasName("pk_imageonlychannels"); + + b.HasIndex("ChannelId") + .IsUnique() + .HasDatabaseName("ix_imageonlychannels_channelid"); + + b.ToTable("imageonlychannels", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.LogSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelCreatedId") + .HasColumnType("numeric(20,0)") + .HasColumnName("channelcreatedid"); + + b.Property("ChannelDestroyedId") + .HasColumnType("numeric(20,0)") + .HasColumnName("channeldestroyedid"); + + b.Property("ChannelUpdatedId") + .HasColumnType("numeric(20,0)") + .HasColumnName("channelupdatedid"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildId") + .HasColumnType("numeric(20,0)") + .HasColumnName("guildid"); + + b.Property("LogOtherId") + .HasColumnType("numeric(20,0)") + .HasColumnName("logotherid"); + + b.Property("LogUserPresenceId") + .HasColumnType("numeric(20,0)") + .HasColumnName("loguserpresenceid"); + + b.Property("LogVoicePresenceId") + .HasColumnType("numeric(20,0)") + .HasColumnName("logvoicepresenceid"); + + b.Property("LogVoicePresenceTTSId") + .HasColumnType("numeric(20,0)") + .HasColumnName("logvoicepresencettsid"); + + b.Property("MessageDeletedId") + .HasColumnType("numeric(20,0)") + .HasColumnName("messagedeletedid"); + + b.Property("MessageUpdatedId") + .HasColumnType("numeric(20,0)") + .HasColumnName("messageupdatedid"); + + b.Property("UserBannedId") + .HasColumnType("numeric(20,0)") + .HasColumnName("userbannedid"); + + b.Property("UserJoinedId") + .HasColumnType("numeric(20,0)") + .HasColumnName("userjoinedid"); + + b.Property("UserLeftId") + .HasColumnType("numeric(20,0)") + .HasColumnName("userleftid"); + + b.Property("UserMutedId") + .HasColumnType("numeric(20,0)") + .HasColumnName("usermutedid"); + + b.Property("UserUnbannedId") + .HasColumnType("numeric(20,0)") + .HasColumnName("userunbannedid"); + + b.Property("UserUpdatedId") + .HasColumnType("numeric(20,0)") + .HasColumnName("userupdatedid"); + + b.HasKey("Id") + .HasName("pk_logsettings"); + + b.HasIndex("GuildId") + .IsUnique() + .HasDatabaseName("ix_logsettings_guildid"); + + b.ToTable("logsettings", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MusicPlayerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AutoDisconnect") + .HasColumnType("boolean") + .HasColumnName("autodisconnect"); + + b.Property("AutoPlay") + .HasColumnType("boolean") + .HasColumnName("autoplay"); + + b.Property("GuildId") + .HasColumnType("numeric(20,0)") + .HasColumnName("guildid"); + + b.Property("MusicChannelId") + .HasColumnType("numeric(20,0)") + .HasColumnName("musicchannelid"); + + b.Property("PlayerRepeat") + .HasColumnType("integer") + .HasColumnName("playerrepeat"); + + b.Property("QualityPreset") + .HasColumnType("integer") + .HasColumnName("qualitypreset"); + + b.Property("Volume") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(100) + .HasColumnName("volume"); + + b.HasKey("Id") + .HasName("pk_musicplayersettings"); + + b.HasIndex("GuildId") + .IsUnique() + .HasDatabaseName("ix_musicplayersettings_guildid"); + + b.ToTable("musicplayersettings", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MusicPlaylist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Author") + .HasColumnType("text") + .HasColumnName("author"); + + b.Property("AuthorId") + .HasColumnType("numeric(20,0)") + .HasColumnName("authorid"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.HasKey("Id") + .HasName("pk_musicplaylists"); + + b.ToTable("musicplaylists", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("integer") + .HasColumnName("guildconfigid"); + + b.Property("UserId") + .HasColumnType("numeric(20,0)") + .HasColumnName("userid"); + + b.HasKey("Id") + .HasName("pk_muteduserid"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_muteduserid_guildconfigid"); + + b.ToTable("muteduserid", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.NadekoExpression", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AllowTarget") + .HasColumnType("boolean") + .HasColumnName("allowtarget"); + + b.Property("AutoDeleteTrigger") + .HasColumnType("boolean") + .HasColumnName("autodeletetrigger"); + + b.Property("ContainsAnywhere") + .HasColumnType("boolean") + .HasColumnName("containsanywhere"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("DmResponse") + .HasColumnType("boolean") + .HasColumnName("dmresponse"); + + b.Property("GuildId") + .HasColumnType("numeric(20,0)") + .HasColumnName("guildid"); + + b.Property("Reactions") + .HasColumnType("text") + .HasColumnName("reactions"); + + b.Property("Response") + .HasColumnType("text") + .HasColumnName("response"); + + b.Property("Trigger") + .HasColumnType("text") + .HasColumnName("trigger"); + + b.HasKey("Id") + .HasName("pk_expressions"); + + b.ToTable("expressions", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklistedTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildId") + .HasColumnType("numeric(20,0)") + .HasColumnName("guildid"); + + b.Property("Tag") + .HasColumnType("text") + .HasColumnName("tag"); + + b.HasKey("Id") + .HasName("pk_nsfwblacklistedtags"); + + b.HasIndex("GuildId") + .HasDatabaseName("ix_nsfwblacklistedtags_guildid"); + + b.ToTable("nsfwblacklistedtags", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("integer") + .HasColumnName("guildconfigid"); + + b.Property("Index") + .HasColumnType("integer") + .HasColumnName("index"); + + b.Property("IsCustomCommand") + .HasColumnType("boolean") + .HasColumnName("iscustomcommand"); + + b.Property("PrimaryTarget") + .HasColumnType("integer") + .HasColumnName("primarytarget"); + + b.Property("PrimaryTargetId") + .HasColumnType("numeric(20,0)") + .HasColumnName("primarytargetid"); + + b.Property("SecondaryTarget") + .HasColumnType("integer") + .HasColumnName("secondarytarget"); + + b.Property("SecondaryTargetName") + .HasColumnType("text") + .HasColumnName("secondarytargetname"); + + b.Property("State") + .HasColumnType("boolean") + .HasColumnName("state"); + + b.HasKey("Id") + .HasName("pk_permissions"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_permissions_guildconfigid"); + + b.ToTable("permissions", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlantedCurrency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("bigint") + .HasColumnName("amount"); + + b.Property("ChannelId") + .HasColumnType("numeric(20,0)") + .HasColumnName("channelid"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildId") + .HasColumnType("numeric(20,0)") + .HasColumnName("guildid"); + + b.Property("MessageId") + .HasColumnType("numeric(20,0)") + .HasColumnName("messageid"); + + b.Property("Password") + .HasColumnType("text") + .HasColumnName("password"); + + b.Property("UserId") + .HasColumnType("numeric(20,0)") + .HasColumnName("userid"); + + b.HasKey("Id") + .HasName("pk_plantedcurrency"); + + b.HasIndex("ChannelId") + .HasDatabaseName("ix_plantedcurrency_channelid"); + + b.HasIndex("MessageId") + .IsUnique() + .HasDatabaseName("ix_plantedcurrency_messageid"); + + b.ToTable("plantedcurrency", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("MusicPlaylistId") + .HasColumnType("integer") + .HasColumnName("musicplaylistid"); + + b.Property("Provider") + .HasColumnType("text") + .HasColumnName("provider"); + + b.Property("ProviderType") + .HasColumnType("integer") + .HasColumnName("providertype"); + + b.Property("Query") + .HasColumnType("text") + .HasColumnName("query"); + + b.Property("Title") + .HasColumnType("text") + .HasColumnName("title"); + + b.Property("Uri") + .HasColumnType("text") + .HasColumnName("uri"); + + b.HasKey("Id") + .HasName("pk_playlistsong"); + + b.HasIndex("MusicPlaylistId") + .HasDatabaseName("ix_playlistsong_musicplaylistid"); + + b.ToTable("playlistsong", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Poll", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelId") + .HasColumnType("numeric(20,0)") + .HasColumnName("channelid"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildId") + .HasColumnType("numeric(20,0)") + .HasColumnName("guildid"); + + b.Property("Question") + .HasColumnType("text") + .HasColumnName("question"); + + b.HasKey("Id") + .HasName("pk_poll"); + + b.HasIndex("GuildId") + .IsUnique() + .HasDatabaseName("ix_poll_guildid"); + + b.ToTable("poll", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PollAnswer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("Index") + .HasColumnType("integer") + .HasColumnName("index"); + + b.Property("PollId") + .HasColumnType("integer") + .HasColumnName("pollid"); + + b.Property("Text") + .HasColumnType("text") + .HasColumnName("text"); + + b.HasKey("Id") + .HasName("pk_pollanswer"); + + b.HasIndex("PollId") + .HasDatabaseName("ix_pollanswer_pollid"); + + b.ToTable("pollanswer", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PollVote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("PollId") + .HasColumnType("integer") + .HasColumnName("pollid"); + + b.Property("UserId") + .HasColumnType("numeric(20,0)") + .HasColumnName("userid"); + + b.Property("VoteIndex") + .HasColumnType("integer") + .HasColumnName("voteindex"); + + b.HasKey("Id") + .HasName("pk_pollvote"); + + b.HasIndex("PollId") + .HasDatabaseName("ix_pollvote_pollid"); + + b.ToTable("pollvote", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Quote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .HasColumnType("numeric(20,0)") + .HasColumnName("authorid"); + + b.Property("AuthorName") + .IsRequired() + .HasColumnType("text") + .HasColumnName("authorname"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildId") + .HasColumnType("numeric(20,0)") + .HasColumnName("guildid"); + + b.Property("Keyword") + .IsRequired() + .HasColumnType("text") + .HasColumnName("keyword"); + + b.Property("Text") + .IsRequired() + .HasColumnType("text") + .HasColumnName("text"); + + b.HasKey("Id") + .HasName("pk_quotes"); + + b.HasIndex("GuildId") + .HasDatabaseName("ix_quotes_guildid"); + + b.HasIndex("Keyword") + .HasDatabaseName("ix_quotes_keyword"); + + b.ToTable("quotes", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRoleV2", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelId") + .HasColumnType("numeric(20,0)") + .HasColumnName("channelid"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("Emote") + .HasMaxLength(100) + .HasColumnType("character varying(100)") + .HasColumnName("emote"); + + b.Property("Group") + .HasColumnType("integer") + .HasColumnName("group"); + + b.Property("GuildId") + .HasColumnType("numeric(20,0)") + .HasColumnName("guildid"); + + b.Property("LevelReq") + .HasColumnType("integer") + .HasColumnName("levelreq"); + + b.Property("MessageId") + .HasColumnType("numeric(20,0)") + .HasColumnName("messageid"); + + b.Property("RoleId") + .HasColumnType("numeric(20,0)") + .HasColumnName("roleid"); + + b.HasKey("Id") + .HasName("pk_reactionroles"); + + b.HasIndex("GuildId") + .HasDatabaseName("ix_reactionroles_guildid"); + + b.HasIndex("MessageId", "Emote") + .IsUnique() + .HasDatabaseName("ix_reactionroles_messageid_emote"); + + b.ToTable("reactionroles", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Reminder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelId") + .HasColumnType("numeric(20,0)") + .HasColumnName("channelid"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("IsPrivate") + .HasColumnType("boolean") + .HasColumnName("isprivate"); + + b.Property("Message") + .HasColumnType("text") + .HasColumnName("message"); + + b.Property("ServerId") + .HasColumnType("numeric(20,0)") + .HasColumnName("serverid"); + + b.Property("UserId") + .HasColumnType("numeric(20,0)") + .HasColumnName("userid"); + + b.Property("When") + .HasColumnType("timestamp with time zone") + .HasColumnName("when"); + + b.HasKey("Id") + .HasName("pk_reminders"); + + b.HasIndex("When") + .HasDatabaseName("ix_reminders_when"); + + b.ToTable("reminders", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Repeater", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("ChannelId") + .HasColumnType("numeric(20,0)") + .HasColumnName("channelid"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildId") + .HasColumnType("numeric(20,0)") + .HasColumnName("guildid"); + + b.Property("Interval") + .HasColumnType("interval") + .HasColumnName("interval"); + + b.Property("LastMessageId") + .HasColumnType("numeric(20,0)") + .HasColumnName("lastmessageid"); + + b.Property("Message") + .HasColumnType("text") + .HasColumnName("message"); + + b.Property("NoRedundant") + .HasColumnType("boolean") + .HasColumnName("noredundant"); + + b.Property("StartTimeOfDay") + .HasColumnType("interval") + .HasColumnName("starttimeofday"); + + b.HasKey("Id") + .HasName("pk_repeaters"); + + b.ToTable("repeaters", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RewardedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AmountRewardedThisMonth") + .HasColumnType("bigint") + .HasColumnName("amountrewardedthismonth"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("LastReward") + .HasColumnType("timestamp with time zone") + .HasColumnName("lastreward"); + + b.Property("PlatformUserId") + .HasColumnType("text") + .HasColumnName("platformuserid"); + + b.Property("UserId") + .HasColumnType("numeric(20,0)") + .HasColumnName("userid"); + + b.HasKey("Id") + .HasName("pk_rewardedusers"); + + b.HasIndex("PlatformUserId") + .IsUnique() + .HasDatabaseName("ix_rewardedusers_platformuserid"); + + b.ToTable("rewardedusers", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RotatingPlayingStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("Status") + .HasColumnType("text") + .HasColumnName("status"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_rotatingstatus"); + + b.ToTable("rotatingstatus", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SelfAssignedRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("Group") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasDefaultValue(0) + .HasColumnName("group"); + + b.Property("GuildId") + .HasColumnType("numeric(20,0)") + .HasColumnName("guildid"); + + b.Property("LevelRequirement") + .HasColumnType("integer") + .HasColumnName("levelrequirement"); + + b.Property("RoleId") + .HasColumnType("numeric(20,0)") + .HasColumnName("roleid"); + + b.HasKey("Id") + .HasName("pk_selfassignableroles"); + + b.HasIndex("GuildId", "RoleId") + .IsUnique() + .HasDatabaseName("ix_selfassignableroles_guildid_roleid"); + + b.ToTable("selfassignableroles", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AuthorId") + .HasColumnType("numeric(20,0)") + .HasColumnName("authorid"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("integer") + .HasColumnName("guildconfigid"); + + b.Property("Index") + .HasColumnType("integer") + .HasColumnName("index"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("Price") + .HasColumnType("integer") + .HasColumnName("price"); + + b.Property("RoleId") + .HasColumnType("numeric(20,0)") + .HasColumnName("roleid"); + + b.Property("RoleName") + .HasColumnType("text") + .HasColumnName("rolename"); + + b.Property("Type") + .HasColumnType("integer") + .HasColumnName("type"); + + b.HasKey("Id") + .HasName("pk_shopentry"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_shopentry_guildconfigid"); + + b.ToTable("shopentry", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("ShopEntryId") + .HasColumnType("integer") + .HasColumnName("shopentryid"); + + b.Property("Text") + .HasColumnType("text") + .HasColumnName("text"); + + b.HasKey("Id") + .HasName("pk_shopentryitem"); + + b.HasIndex("ShopEntryId") + .HasDatabaseName("ix_shopentryitem_shopentryid"); + + b.ToTable("shopentryitem", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("integer") + .HasColumnName("guildconfigid"); + + b.Property("RoleId") + .HasColumnType("numeric(20,0)") + .HasColumnName("roleid"); + + b.HasKey("Id") + .HasName("pk_slowmodeignoredrole"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_slowmodeignoredrole_guildconfigid"); + + b.ToTable("slowmodeignoredrole", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("integer") + .HasColumnName("guildconfigid"); + + b.Property("UserId") + .HasColumnType("numeric(20,0)") + .HasColumnName("userid"); + + b.HasKey("Id") + .HasName("pk_slowmodeignoreduser"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_slowmodeignoreduser_guildconfigid"); + + b.ToTable("slowmodeignoreduser", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleBlacklistedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("StreamRoleSettingsId") + .HasColumnType("integer") + .HasColumnName("streamrolesettingsid"); + + b.Property("UserId") + .HasColumnType("numeric(20,0)") + .HasColumnName("userid"); + + b.Property("Username") + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_streamroleblacklisteduser"); + + b.HasIndex("StreamRoleSettingsId") + .HasDatabaseName("ix_streamroleblacklisteduser_streamrolesettingsid"); + + b.ToTable("streamroleblacklisteduser", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AddRoleId") + .HasColumnType("numeric(20,0)") + .HasColumnName("addroleid"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("Enabled") + .HasColumnType("boolean") + .HasColumnName("enabled"); + + b.Property("FromRoleId") + .HasColumnType("numeric(20,0)") + .HasColumnName("fromroleid"); + + b.Property("GuildConfigId") + .HasColumnType("integer") + .HasColumnName("guildconfigid"); + + b.Property("Keyword") + .HasColumnType("text") + .HasColumnName("keyword"); + + b.HasKey("Id") + .HasName("pk_streamrolesettings"); + + b.HasIndex("GuildConfigId") + .IsUnique() + .HasDatabaseName("ix_streamrolesettings_guildconfigid"); + + b.ToTable("streamrolesettings", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleWhitelistedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("StreamRoleSettingsId") + .HasColumnType("integer") + .HasColumnName("streamrolesettingsid"); + + b.Property("UserId") + .HasColumnType("numeric(20,0)") + .HasColumnName("userid"); + + b.Property("Username") + .HasColumnType("text") + .HasColumnName("username"); + + b.HasKey("Id") + .HasName("pk_streamrolewhitelisteduser"); + + b.HasIndex("StreamRoleSettingsId") + .HasDatabaseName("ix_streamrolewhitelisteduser_streamrolesettingsid"); + + b.ToTable("streamrolewhitelisteduser", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnbanTimer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("integer") + .HasColumnName("guildconfigid"); + + b.Property("UnbanAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("unbanat"); + + b.Property("UserId") + .HasColumnType("numeric(20,0)") + .HasColumnName("userid"); + + b.HasKey("Id") + .HasName("pk_unbantimer"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_unbantimer_guildconfigid"); + + b.ToTable("unbantimer", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("integer") + .HasColumnName("guildconfigid"); + + b.Property("UnmuteAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("unmuteat"); + + b.Property("UserId") + .HasColumnType("numeric(20,0)") + .HasColumnName("userid"); + + b.HasKey("Id") + .HasName("pk_unmutetimer"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_unmutetimer_guildconfigid"); + + b.ToTable("unmutetimer", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnroleTimer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("integer") + .HasColumnName("guildconfigid"); + + b.Property("RoleId") + .HasColumnType("numeric(20,0)") + .HasColumnName("roleid"); + + b.Property("UnbanAt") + .HasColumnType("timestamp with time zone") + .HasColumnName("unbanat"); + + b.Property("UserId") + .HasColumnType("numeric(20,0)") + .HasColumnName("userid"); + + b.HasKey("Id") + .HasName("pk_unroletimer"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_unroletimer_guildconfigid"); + + b.ToTable("unroletimer", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UserXpStats", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AwardedXp") + .HasColumnType("bigint") + .HasColumnName("awardedxp"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildId") + .HasColumnType("numeric(20,0)") + .HasColumnName("guildid"); + + b.Property("LastLevelUp") + .ValueGeneratedOnAdd() + .HasColumnType("timestamp with time zone") + .HasColumnName("lastlevelup") + .HasDefaultValueSql("timezone('utc', now())"); + + b.Property("NotifyOnLevelUp") + .HasColumnType("integer") + .HasColumnName("notifyonlevelup"); + + b.Property("UserId") + .HasColumnType("numeric(20,0)") + .HasColumnName("userid"); + + b.Property("Xp") + .HasColumnType("bigint") + .HasColumnName("xp"); + + b.HasKey("Id") + .HasName("pk_userxpstats"); + + b.HasIndex("AwardedXp") + .HasDatabaseName("ix_userxpstats_awardedxp"); + + b.HasIndex("GuildId") + .HasDatabaseName("ix_userxpstats_guildid"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_userxpstats_userid"); + + b.HasIndex("Xp") + .HasDatabaseName("ix_userxpstats_xp"); + + b.HasIndex("UserId", "GuildId") + .IsUnique() + .HasDatabaseName("ix_userxpstats_userid_guildid"); + + b.ToTable("userxpstats", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.VcRoleInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("integer") + .HasColumnName("guildconfigid"); + + b.Property("RoleId") + .HasColumnType("numeric(20,0)") + .HasColumnName("roleid"); + + b.Property("VoiceChannelId") + .HasColumnType("numeric(20,0)") + .HasColumnName("voicechannelid"); + + b.HasKey("Id") + .HasName("pk_vcroleinfo"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_vcroleinfo_guildconfigid"); + + b.ToTable("vcroleinfo", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("AffinityId") + .HasColumnType("integer") + .HasColumnName("affinityid"); + + b.Property("ClaimerId") + .HasColumnType("integer") + .HasColumnName("claimerid"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("Price") + .HasColumnType("bigint") + .HasColumnName("price"); + + b.Property("WaifuId") + .HasColumnType("integer") + .HasColumnName("waifuid"); + + b.HasKey("Id") + .HasName("pk_waifuinfo"); + + b.HasIndex("AffinityId") + .HasDatabaseName("ix_waifuinfo_affinityid"); + + b.HasIndex("ClaimerId") + .HasDatabaseName("ix_waifuinfo_claimerid"); + + b.HasIndex("Price") + .HasDatabaseName("ix_waifuinfo_price"); + + b.HasIndex("WaifuId") + .IsUnique() + .HasDatabaseName("ix_waifuinfo_waifuid"); + + b.ToTable("waifuinfo", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("ItemEmoji") + .HasColumnType("text") + .HasColumnName("itememoji"); + + b.Property("Name") + .HasColumnType("text") + .HasColumnName("name"); + + b.Property("WaifuInfoId") + .HasColumnType("integer") + .HasColumnName("waifuinfoid"); + + b.HasKey("Id") + .HasName("pk_waifuitem"); + + b.HasIndex("WaifuInfoId") + .HasDatabaseName("ix_waifuitem_waifuinfoid"); + + b.ToTable("waifuitem", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuUpdate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("NewId") + .HasColumnType("integer") + .HasColumnName("newid"); + + b.Property("OldId") + .HasColumnType("integer") + .HasColumnName("oldid"); + + b.Property("UpdateType") + .HasColumnType("integer") + .HasColumnName("updatetype"); + + b.Property("UserId") + .HasColumnType("integer") + .HasColumnName("userid"); + + b.HasKey("Id") + .HasName("pk_waifuupdates"); + + b.HasIndex("NewId") + .HasDatabaseName("ix_waifuupdates_newid"); + + b.HasIndex("OldId") + .HasDatabaseName("ix_waifuupdates_oldid"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_waifuupdates_userid"); + + b.ToTable("waifuupdates", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Warning", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("Forgiven") + .HasColumnType("boolean") + .HasColumnName("forgiven"); + + b.Property("ForgivenBy") + .HasColumnType("text") + .HasColumnName("forgivenby"); + + b.Property("GuildId") + .HasColumnType("numeric(20,0)") + .HasColumnName("guildid"); + + b.Property("Moderator") + .HasColumnType("text") + .HasColumnName("moderator"); + + b.Property("Reason") + .HasColumnType("text") + .HasColumnName("reason"); + + b.Property("UserId") + .HasColumnType("numeric(20,0)") + .HasColumnName("userid"); + + b.Property("Weight") + .ValueGeneratedOnAdd() + .HasColumnType("bigint") + .HasDefaultValue(1L) + .HasColumnName("weight"); + + b.HasKey("Id") + .HasName("pk_warnings"); + + b.HasIndex("DateAdded") + .HasDatabaseName("ix_warnings_dateadded"); + + b.HasIndex("GuildId") + .HasDatabaseName("ix_warnings_guildid"); + + b.HasIndex("UserId") + .HasDatabaseName("ix_warnings_userid"); + + b.ToTable("warnings", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WarningPunishment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Count") + .HasColumnType("integer") + .HasColumnName("count"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("integer") + .HasColumnName("guildconfigid"); + + b.Property("Punishment") + .HasColumnType("integer") + .HasColumnName("punishment"); + + b.Property("RoleId") + .HasColumnType("numeric(20,0)") + .HasColumnName("roleid"); + + b.Property("Time") + .HasColumnType("integer") + .HasColumnName("time"); + + b.HasKey("Id") + .HasName("pk_warningpunishment"); + + b.HasIndex("GuildConfigId") + .HasDatabaseName("ix_warningpunishment_guildconfigid"); + + b.ToTable("warningpunishment", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpCurrencyReward", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("Amount") + .HasColumnType("integer") + .HasColumnName("amount"); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("Level") + .HasColumnType("integer") + .HasColumnName("level"); + + b.Property("XpSettingsId") + .HasColumnType("integer") + .HasColumnName("xpsettingsid"); + + b.HasKey("Id") + .HasName("pk_xpcurrencyreward"); + + b.HasIndex("XpSettingsId") + .HasDatabaseName("ix_xpcurrencyreward_xpsettingsid"); + + b.ToTable("xpcurrencyreward", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpRoleReward", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("Level") + .HasColumnType("integer") + .HasColumnName("level"); + + b.Property("Remove") + .HasColumnType("boolean") + .HasColumnName("remove"); + + b.Property("RoleId") + .HasColumnType("numeric(20,0)") + .HasColumnName("roleid"); + + b.Property("XpSettingsId") + .HasColumnType("integer") + .HasColumnName("xpsettingsid"); + + b.HasKey("Id") + .HasName("pk_xprolereward"); + + b.HasIndex("XpSettingsId", "Level") + .IsUnique() + .HasDatabaseName("ix_xprolereward_xpsettingsid_level"); + + b.ToTable("xprolereward", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("integer") + .HasColumnName("id"); + + NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); + + b.Property("DateAdded") + .HasColumnType("timestamp with time zone") + .HasColumnName("dateadded"); + + b.Property("GuildConfigId") + .HasColumnType("integer") + .HasColumnName("guildconfigid"); + + b.Property("ServerExcluded") + .HasColumnType("boolean") + .HasColumnName("serverexcluded"); + + b.HasKey("Id") + .HasName("pk_xpsettings"); + + b.HasIndex("GuildConfigId") + .IsUnique() + .HasDatabaseName("ix_xpsettings_guildconfigid"); + + b.ToTable("xpsettings", (string)null); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.ClubApplicants", b => + { + b.HasOne("NadekoBot.Db.Models.ClubInfo", "Club") + .WithMany("Applicants") + .HasForeignKey("ClubId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_clubapplicants_clubs_clubid"); + + b.HasOne("NadekoBot.Db.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_clubapplicants_discorduser_userid"); + + b.Navigation("Club"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.ClubBans", b => + { + b.HasOne("NadekoBot.Db.Models.ClubInfo", "Club") + .WithMany("Bans") + .HasForeignKey("ClubId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_clubbans_clubs_clubid"); + + b.HasOne("NadekoBot.Db.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_clubbans_discorduser_userid"); + + b.Navigation("Club"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.ClubInfo", b => + { + b.HasOne("NadekoBot.Db.Models.DiscordUser", "Owner") + .WithOne() + .HasForeignKey("NadekoBot.Db.Models.ClubInfo", "OwnerId") + .OnDelete(DeleteBehavior.SetNull) + .HasConstraintName("fk_clubs_discorduser_ownerid"); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.DiscordUser", b => + { + b.HasOne("NadekoBot.Db.Models.ClubInfo", "Club") + .WithMany("Members") + .HasForeignKey("ClubId") + .OnDelete(DeleteBehavior.NoAction) + .HasConstraintName("fk_discorduser_clubs_clubid"); + + b.Navigation("Club"); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.FollowedStream", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("FollowedStreams") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_followedstream_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiAltSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithOne("AntiAltSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiAltSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_antialtsetting_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiRaidSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiRaidSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_antiraidsetting_guildconfigs_guildconfigid"); + + b.Navigation("GuildConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.HasOne("NadekoBot.Services.Database.Models.AntiSpamSetting", null) + .WithMany("IgnoredChannels") + .HasForeignKey("AntiSpamSettingId") + .HasConstraintName("fk_antispamignore_antispamsetting_antispamsettingid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiSpamSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiSpamSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_antispamsetting_guildconfigs_guildconfigid"); + + b.Navigation("GuildConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AutoTranslateUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.AutoTranslateChannel", "Channel") + .WithMany("Users") + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_autotranslateusers_autotranslatechannels_channelid"); + + b.Navigation("Channel"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("CommandAliases") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_commandalias_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("CommandCooldowns") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_commandcooldown_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.DelMsgOnCmdChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("DelMsgOnCmdChannels") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_delmsgoncmdchannel_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ExcludedItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.XpSettings", null) + .WithMany("ExclusionList") + .HasForeignKey("XpSettingsId") + .HasConstraintName("fk_excludeditem_xpsettings_xpsettingsid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FeedSub", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithMany("FeedSubs") + .HasForeignKey("GuildConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_feedsub_guildconfigs_guildconfigid"); + + b.Navigation("GuildConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("FilterInvitesChannelIds") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_filterchannelid_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("FilteredWords") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_filteredword_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterLinksChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("FilterLinksChannelIds") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_filterlinkschannelid_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterWordsChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("FilterWordsChannelIds") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_filterwordschannelid_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithMany("GenerateCurrencyChannelIds") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_gcchannelid_guildconfigs_guildconfigid"); + + b.Navigation("GuildConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GroupName", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithMany("SelfAssignableRoleGroupNames") + .HasForeignKey("GuildConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_groupname_guildconfigs_guildconfigid"); + + b.Navigation("GuildConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany("LogIgnores") + .HasForeignKey("LogSettingId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_ignoredlogchannels_logsettings_logsettingid"); + + b.Navigation("LogSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany() + .HasForeignKey("LogSettingId") + .HasConstraintName("fk_ignoredvoicepresencechannels_logsettings_logsettingid"); + + b.Navigation("LogSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("MutedUsers") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_muteduserid_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("Permissions") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_permissions_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.HasOne("NadekoBot.Services.Database.Models.MusicPlaylist", null) + .WithMany("Songs") + .HasForeignKey("MusicPlaylistId") + .OnDelete(DeleteBehavior.Cascade) + .HasConstraintName("fk_playlistsong_musicplaylists_musicplaylistid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PollAnswer", b => + { + b.HasOne("NadekoBot.Services.Database.Models.Poll", null) + .WithMany("Answers") + .HasForeignKey("PollId") + .HasConstraintName("fk_pollanswer_poll_pollid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PollVote", b => + { + b.HasOne("NadekoBot.Services.Database.Models.Poll", null) + .WithMany("Votes") + .HasForeignKey("PollId") + .HasConstraintName("fk_pollvote_poll_pollid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("ShopEntries") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_shopentry_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntryItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ShopEntry", null) + .WithMany("Items") + .HasForeignKey("ShopEntryId") + .HasConstraintName("fk_shopentryitem_shopentry_shopentryid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredRole", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("SlowmodeIgnoredRoles") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_slowmodeignoredrole_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("SlowmodeIgnoredUsers") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_slowmodeignoreduser_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleBlacklistedUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.StreamRoleSettings", null) + .WithMany("Blacklist") + .HasForeignKey("StreamRoleSettingsId") + .HasConstraintName("fk_streamroleblacklisteduser_streamrolesettings_streamrolesett~"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("StreamRole") + .HasForeignKey("NadekoBot.Services.Database.Models.StreamRoleSettings", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_streamrolesettings_guildconfigs_guildconfigid"); + + b.Navigation("GuildConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleWhitelistedUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.StreamRoleSettings", null) + .WithMany("Whitelist") + .HasForeignKey("StreamRoleSettingsId") + .HasConstraintName("fk_streamrolewhitelisteduser_streamrolesettings_streamrolesett~"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnbanTimer", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("UnbanTimer") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_unbantimer_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("UnmuteTimers") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_unmutetimer_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnroleTimer", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("UnroleTimer") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_unroletimer_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.VcRoleInfo", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("VcRoleInfos") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_vcroleinfo_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.HasOne("NadekoBot.Db.Models.DiscordUser", "Affinity") + .WithMany() + .HasForeignKey("AffinityId") + .HasConstraintName("fk_waifuinfo_discorduser_affinityid"); + + b.HasOne("NadekoBot.Db.Models.DiscordUser", "Claimer") + .WithMany() + .HasForeignKey("ClaimerId") + .HasConstraintName("fk_waifuinfo_discorduser_claimerid"); + + b.HasOne("NadekoBot.Db.Models.DiscordUser", "Waifu") + .WithOne() + .HasForeignKey("NadekoBot.Services.Database.Models.WaifuInfo", "WaifuId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_waifuinfo_discorduser_waifuid"); + + b.Navigation("Affinity"); + + b.Navigation("Claimer"); + + b.Navigation("Waifu"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.WaifuInfo", "WaifuInfo") + .WithMany("Items") + .HasForeignKey("WaifuInfoId") + .HasConstraintName("fk_waifuitem_waifuinfo_waifuinfoid"); + + b.Navigation("WaifuInfo"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuUpdate", b => + { + b.HasOne("NadekoBot.Db.Models.DiscordUser", "New") + .WithMany() + .HasForeignKey("NewId") + .HasConstraintName("fk_waifuupdates_discorduser_newid"); + + b.HasOne("NadekoBot.Db.Models.DiscordUser", "Old") + .WithMany() + .HasForeignKey("OldId") + .HasConstraintName("fk_waifuupdates_discorduser_oldid"); + + b.HasOne("NadekoBot.Db.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_waifuupdates_discorduser_userid"); + + b.Navigation("New"); + + b.Navigation("Old"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WarningPunishment", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("WarnPunishments") + .HasForeignKey("GuildConfigId") + .HasConstraintName("fk_warningpunishment_guildconfigs_guildconfigid"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpCurrencyReward", b => + { + b.HasOne("NadekoBot.Services.Database.Models.XpSettings", "XpSettings") + .WithMany("CurrencyRewards") + .HasForeignKey("XpSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xpcurrencyreward_xpsettings_xpsettingsid"); + + b.Navigation("XpSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpRoleReward", b => + { + b.HasOne("NadekoBot.Services.Database.Models.XpSettings", "XpSettings") + .WithMany("RoleRewards") + .HasForeignKey("XpSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xprolereward_xpsettings_xpsettingsid"); + + b.Navigation("XpSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpSettings", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("XpSettings") + .HasForeignKey("NadekoBot.Services.Database.Models.XpSettings", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired() + .HasConstraintName("fk_xpsettings_guildconfigs_guildconfigid"); + + b.Navigation("GuildConfig"); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.ClubInfo", b => + { + b.Navigation("Applicants"); + + b.Navigation("Bans"); + + b.Navigation("Members"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.Navigation("IgnoredChannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AutoTranslateChannel", b => + { + b.Navigation("Users"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.Navigation("AntiAltSetting"); + + b.Navigation("AntiRaidSetting"); + + b.Navigation("AntiSpamSetting"); + + b.Navigation("CommandAliases"); + + b.Navigation("CommandCooldowns"); + + b.Navigation("DelMsgOnCmdChannels"); + + b.Navigation("FeedSubs"); + + b.Navigation("FilterInvitesChannelIds"); + + b.Navigation("FilterLinksChannelIds"); + + b.Navigation("FilterWordsChannelIds"); + + b.Navigation("FilteredWords"); + + b.Navigation("FollowedStreams"); + + b.Navigation("GenerateCurrencyChannelIds"); + + b.Navigation("MutedUsers"); + + b.Navigation("Permissions"); + + b.Navigation("SelfAssignableRoleGroupNames"); + + b.Navigation("ShopEntries"); + + b.Navigation("SlowmodeIgnoredRoles"); + + b.Navigation("SlowmodeIgnoredUsers"); + + b.Navigation("StreamRole"); + + b.Navigation("UnbanTimer"); + + b.Navigation("UnmuteTimers"); + + b.Navigation("UnroleTimer"); + + b.Navigation("VcRoleInfos"); + + b.Navigation("WarnPunishments"); + + b.Navigation("XpSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.LogSetting", b => + { + b.Navigation("LogIgnores"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MusicPlaylist", b => + { + b.Navigation("Songs"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Poll", b => + { + b.Navigation("Answers"); + + b.Navigation("Votes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.Navigation("Blacklist"); + + b.Navigation("Whitelist"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpSettings", b => + { + b.Navigation("CurrencyRewards"); + + b.Navigation("ExclusionList"); + + b.Navigation("RoleRewards"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/NadekoBot/Migrations/Postgresql/20220614071421_patronage-system.cs b/src/NadekoBot/Migrations/Postgresql/20220614071421_patronage-system.cs new file mode 100644 index 000000000..c16ea5b4c --- /dev/null +++ b/src/NadekoBot/Migrations/Postgresql/20220614071421_patronage-system.cs @@ -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( + name: "xp", + table: "userxpstats", + type: "bigint", + nullable: false, + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "awardedxp", + table: "userxpstats", + type: "bigint", + nullable: false, + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "amountrewardedthismonth", + table: "rewardedusers", + type: "bigint", + nullable: false, + oldClrType: typeof(int), + oldType: "integer"); + + migrationBuilder.AlterColumn( + name: "verboseerrors", + table: "guildconfigs", + type: "boolean", + nullable: false, + defaultValue: true, + oldClrType: typeof(bool), + oldType: "boolean"); + + migrationBuilder.AlterColumn( + 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(type: "numeric(20,0)", nullable: false), + featuretype = table.Column(type: "integer", nullable: false), + feature = table.Column(type: "text", nullable: false), + hourlycount = table.Column(type: "bigint", nullable: false), + dailycount = table.Column(type: "bigint", nullable: false), + monthlycount = table.Column(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(type: "numeric(20,0)", nullable: false), + uniqueplatformuserid = table.Column(type: "text", nullable: true), + amountcents = table.Column(type: "integer", nullable: false), + lastcharge = table.Column(type: "timestamp with time zone", nullable: false), + validthru = table.Column(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( + name: "xp", + table: "userxpstats", + type: "integer", + nullable: false, + oldClrType: typeof(long), + oldType: "bigint"); + + migrationBuilder.AlterColumn( + name: "awardedxp", + table: "userxpstats", + type: "integer", + nullable: false, + oldClrType: typeof(long), + oldType: "bigint"); + + migrationBuilder.AlterColumn( + name: "amountrewardedthismonth", + table: "rewardedusers", + type: "integer", + nullable: false, + oldClrType: typeof(long), + oldType: "bigint"); + + migrationBuilder.AlterColumn( + name: "verboseerrors", + table: "guildconfigs", + type: "boolean", + nullable: false, + oldClrType: typeof(bool), + oldType: "boolean", + oldDefaultValue: true); + + migrationBuilder.AlterColumn( + name: "totalxp", + table: "discorduser", + type: "integer", + nullable: false, + defaultValue: 0, + oldClrType: typeof(long), + oldType: "bigint", + oldDefaultValue: 0L); + } + } +} diff --git a/src/NadekoBot/Migrations/Postgresql/PostgreSqlContextModelSnapshot.cs b/src/NadekoBot/Migrations/Postgresql/PostgreSqlContextModelSnapshot.cs index 3fe08a74d..fc3eb8bac 100644 --- a/src/NadekoBot/Migrations/Postgresql/PostgreSqlContextModelSnapshot.cs +++ b/src/NadekoBot/Migrations/Postgresql/PostgreSqlContextModelSnapshot.cs @@ -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("TotalXp") + b.Property("TotalXp") .ValueGeneratedOnAdd() - .HasColumnType("integer") - .HasDefaultValue(0) + .HasColumnType("bigint") + .HasDefaultValue(0L) .HasColumnName("totalxp"); b.Property("UserId") @@ -275,6 +275,74 @@ namespace NadekoBot.Migrations.PostgreSql b.ToTable("followedstream", (string)null); }); + modelBuilder.Entity("NadekoBot.Db.Models.PatronQuota", b => + { + b.Property("UserId") + .HasColumnType("numeric(20,0)") + .HasColumnName("userid"); + + b.Property("FeatureType") + .HasColumnType("integer") + .HasColumnName("featuretype"); + + b.Property("Feature") + .HasColumnType("text") + .HasColumnName("feature"); + + b.Property("DailyCount") + .HasColumnType("bigint") + .HasColumnName("dailycount"); + + b.Property("HourlyCount") + .HasColumnType("bigint") + .HasColumnName("hourlycount"); + + b.Property("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("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("numeric(20,0)") + .HasColumnName("userid"); + + b.Property("AmountCents") + .HasColumnType("integer") + .HasColumnName("amountcents"); + + b.Property("LastCharge") + .HasColumnType("timestamp with time zone") + .HasColumnName("lastcharge"); + + b.Property("UniquePlatformUserId") + .HasColumnType("text") + .HasColumnName("uniqueplatformuserid"); + + b.Property("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("Id") @@ -1194,7 +1262,9 @@ namespace NadekoBot.Migrations.PostgreSql .HasColumnName("timezoneid"); b.Property("VerboseErrors") + .ValueGeneratedOnAdd() .HasColumnType("boolean") + .HasDefaultValue(true) .HasColumnName("verboseerrors"); b.Property("VerbosePermissions") @@ -2058,8 +2128,8 @@ namespace NadekoBot.Migrations.PostgreSql NpgsqlPropertyBuilderExtensions.UseIdentityByDefaultColumn(b.Property("Id")); - b.Property("AmountRewardedThisMonth") - .HasColumnType("integer") + b.Property("AmountRewardedThisMonth") + .HasColumnType("bigint") .HasColumnName("amountrewardedthismonth"); b.Property("DateAdded") @@ -2070,9 +2140,9 @@ namespace NadekoBot.Migrations.PostgreSql .HasColumnType("timestamp with time zone") .HasColumnName("lastreward"); - b.Property("PatreonUserId") + b.Property("PlatformUserId") .HasColumnType("text") - .HasColumnName("patreonuserid"); + .HasColumnName("platformuserid"); b.Property("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("Id")); - b.Property("AwardedXp") - .HasColumnType("integer") + b.Property("AwardedXp") + .HasColumnType("bigint") .HasColumnName("awardedxp"); b.Property("DateAdded") @@ -2552,8 +2622,8 @@ namespace NadekoBot.Migrations.PostgreSql .HasColumnType("numeric(20,0)") .HasColumnName("userid"); - b.Property("Xp") - .HasColumnType("integer") + b.Property("Xp") + .HasColumnType("bigint") .HasColumnName("xp"); b.HasKey("Id") diff --git a/src/NadekoBot/Migrations/Sqlite/20210707002343_cleanup.cs b/src/NadekoBot/Migrations/Sqlite/20210707002343_cleanup.cs index 70c3aee72..9196a4bd1 100644 --- a/src/NadekoBot/Migrations/Sqlite/20210707002343_cleanup.cs +++ b/src/NadekoBot/Migrations/Sqlite/20210707002343_cleanup.cs @@ -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( diff --git a/src/NadekoBot/Migrations/Sqlite/20220614071359_patronage-system.Designer.cs b/src/NadekoBot/Migrations/Sqlite/20220614071359_patronage-system.Designer.cs new file mode 100644 index 000000000..6e25b6178 --- /dev/null +++ b/src/NadekoBot/Migrations/Sqlite/20220614071359_patronage-system.Designer.cs @@ -0,0 +1,2796 @@ +// +using System; +using Microsoft.EntityFrameworkCore; +using Microsoft.EntityFrameworkCore.Infrastructure; +using Microsoft.EntityFrameworkCore.Migrations; +using Microsoft.EntityFrameworkCore.Storage.ValueConversion; +using NadekoBot.Services.Database; + +#nullable disable + +namespace NadekoBot.Migrations +{ + [DbContext(typeof(SqliteContext))] + [Migration("20220614071359_patronage-system")] + partial class patronagesystem + { + protected override void BuildTargetModel(ModelBuilder modelBuilder) + { +#pragma warning disable 612, 618 + modelBuilder.HasAnnotation("ProductVersion", "6.0.5"); + + modelBuilder.Entity("NadekoBot.Db.Models.BankUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Balance") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId") + .IsUnique(); + + b.ToTable("BankUsers"); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.ClubApplicants", b => + { + b.Property("ClubId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("ClubId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ClubApplicants"); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.ClubBans", b => + { + b.Property("ClubId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("ClubId", "UserId"); + + b.HasIndex("UserId"); + + b.ToTable("ClubBans"); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.ClubInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("Description") + .HasColumnType("TEXT"); + + b.Property("ImageUrl") + .HasColumnType("TEXT"); + + b.Property("Name") + .IsRequired() + .HasMaxLength(20) + .HasColumnType("TEXT"); + + b.Property("OwnerId") + .HasColumnType("INTEGER"); + + b.Property("Xp") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasAlternateKey("Name"); + + b.HasIndex("OwnerId") + .IsUnique(); + + b.ToTable("Clubs"); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.DiscordUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AvatarId") + .HasColumnType("TEXT"); + + b.Property("ClubId") + .HasColumnType("INTEGER"); + + b.Property("CurrencyAmount") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0L); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("Discriminator") + .HasColumnType("TEXT"); + + b.Property("IsClubAdmin") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(false); + + b.Property("LastLevelUp") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("datetime('now')"); + + b.Property("LastXpGain") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("datetime('now', '-1 years')"); + + b.Property("NotifyOnLevelUp") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("TotalXp") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0L); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("Username") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasAlternateKey("UserId"); + + b.HasIndex("ClubId"); + + b.HasIndex("CurrencyAmount"); + + b.HasIndex("TotalXp"); + + b.HasIndex("UserId"); + + b.ToTable("DiscordUser"); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.FollowedStream", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.Property("Username") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FollowedStream"); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.PatronQuota", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("FeatureType") + .HasColumnType("INTEGER"); + + b.Property("Feature") + .HasColumnType("TEXT"); + + b.Property("DailyCount") + .HasColumnType("INTEGER"); + + b.Property("HourlyCount") + .HasColumnType("INTEGER"); + + b.Property("MonthlyCount") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "FeatureType", "Feature"); + + b.HasIndex("UserId"); + + b.ToTable("PatronQuotas"); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.PatronUser", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AmountCents") + .HasColumnType("INTEGER"); + + b.Property("LastCharge") + .HasColumnType("TEXT"); + + b.Property("UniquePlatformUserId") + .HasColumnType("TEXT"); + + b.Property("ValidThru") + .HasColumnType("TEXT"); + + b.HasKey("UserId"); + + b.HasIndex("UniquePlatformUserId") + .IsUnique(); + + b.ToTable("Patrons"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiAltSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Action") + .HasColumnType("INTEGER"); + + b.Property("ActionDurationMinutes") + .HasColumnType("INTEGER"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("MinAge") + .HasColumnType("TEXT"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("AntiAltSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Action") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("PunishDuration") + .HasColumnType("INTEGER"); + + b.Property("Seconds") + .HasColumnType("INTEGER"); + + b.Property("UserThreshold") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("AntiRaidSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AntiSpamSettingId") + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("AntiSpamSettingId"); + + b.ToTable("AntiSpamIgnore"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Action") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("MessageThreshold") + .HasColumnType("INTEGER"); + + b.Property("MuteTime") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("AntiSpamSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AutoCommand", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("ChannelName") + .HasColumnType("TEXT"); + + b.Property("CommandText") + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("GuildName") + .HasColumnType("TEXT"); + + b.Property("Interval") + .HasColumnType("INTEGER"); + + b.Property("VoiceChannelId") + .HasColumnType("INTEGER"); + + b.Property("VoiceChannelName") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("AutoCommands"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AutoTranslateChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AutoDelete") + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChannelId") + .IsUnique(); + + b.HasIndex("GuildId"); + + b.ToTable("AutoTranslateChannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AutoTranslateUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("Source") + .HasColumnType("TEXT"); + + b.Property("Target") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasAlternateKey("ChannelId", "UserId"); + + b.ToTable("AutoTranslateUsers"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BanTemplate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("Text") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GuildId") + .IsUnique(); + + b.ToTable("BanTemplates"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.BlacklistEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("INTEGER"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("Blacklist"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("Mapping") + .HasColumnType("TEXT"); + + b.Property("Trigger") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("CommandAlias"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("CommandName") + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("Seconds") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("CommandCooldown"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CurrencyTransaction", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("Extra") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Note") + .HasColumnType("TEXT"); + + b.Property("OtherId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValueSql("NULL"); + + b.Property("Type") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("UserId"); + + b.ToTable("CurrencyTransactions"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.DelMsgOnCmdChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("State") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("DelMsgOnCmdChannel"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.DiscordPermOverride", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Command") + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("Perm") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildId", "Command") + .IsUnique(); + + b.ToTable("DiscordPermOverrides"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ExcludedItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("ItemId") + .HasColumnType("INTEGER"); + + b.Property("ItemType") + .HasColumnType("INTEGER"); + + b.Property("XpSettingsId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("XpSettingsId"); + + b.ToTable("ExcludedItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FeedSub", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("Url") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasAlternateKey("GuildConfigId", "Url"); + + b.ToTable("FeedSub"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FilterChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("Word") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FilteredWord"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterLinksChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FilterLinksChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterWordsChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("FilterWordsChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("GCChannelId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GroupName", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Number") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId", "Number") + .IsUnique(); + + b.ToTable("GroupName"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AutoAssignRoleIds") + .HasColumnType("TEXT"); + + b.Property("AutoDeleteByeMessagesTimer") + .HasColumnType("INTEGER"); + + b.Property("AutoDeleteGreetMessagesTimer") + .HasColumnType("INTEGER"); + + b.Property("AutoDeleteSelfAssignedRoleMessages") + .HasColumnType("INTEGER"); + + b.Property("BoostMessage") + .HasColumnType("TEXT"); + + b.Property("BoostMessageChannelId") + .HasColumnType("INTEGER"); + + b.Property("BoostMessageDeleteAfter") + .HasColumnType("INTEGER"); + + b.Property("ByeMessageChannelId") + .HasColumnType("INTEGER"); + + b.Property("ChannelByeMessageText") + .HasColumnType("TEXT"); + + b.Property("ChannelGreetMessageText") + .HasColumnType("TEXT"); + + b.Property("CleverbotEnabled") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DeleteMessageOnCommand") + .HasColumnType("INTEGER"); + + b.Property("DeleteStreamOnlineMessage") + .HasColumnType("INTEGER"); + + b.Property("DmGreetMessageText") + .HasColumnType("TEXT"); + + b.Property("ExclusiveSelfAssignedRoles") + .HasColumnType("INTEGER"); + + b.Property("FilterInvites") + .HasColumnType("INTEGER"); + + b.Property("FilterLinks") + .HasColumnType("INTEGER"); + + b.Property("FilterWords") + .HasColumnType("INTEGER"); + + b.Property("GameVoiceChannel") + .HasColumnType("INTEGER"); + + b.Property("GreetMessageChannelId") + .HasColumnType("INTEGER"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("Locale") + .HasColumnType("TEXT"); + + b.Property("MuteRoleName") + .HasColumnType("TEXT"); + + b.Property("NotifyStreamOffline") + .HasColumnType("INTEGER"); + + b.Property("PermissionRole") + .HasColumnType("TEXT"); + + b.Property("Prefix") + .HasColumnType("TEXT"); + + b.Property("SendBoostMessage") + .HasColumnType("INTEGER"); + + b.Property("SendChannelByeMessage") + .HasColumnType("INTEGER"); + + b.Property("SendChannelGreetMessage") + .HasColumnType("INTEGER"); + + b.Property("SendDmGreetMessage") + .HasColumnType("INTEGER"); + + b.Property("TimeZoneId") + .HasColumnType("TEXT"); + + b.Property("VerboseErrors") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); + + b.Property("VerbosePermissions") + .HasColumnType("INTEGER"); + + b.Property("WarnExpireAction") + .HasColumnType("INTEGER"); + + b.Property("WarnExpireHours") + .HasColumnType("INTEGER"); + + b.Property("WarningsInitialized") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildId") + .IsUnique(); + + b.HasIndex("WarnExpireHours"); + + b.ToTable("GuildConfigs"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("ItemType") + .HasColumnType("INTEGER"); + + b.Property("LogItemId") + .HasColumnType("INTEGER"); + + b.Property("LogSettingId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LogSettingId", "LogItemId", "ItemType") + .IsUnique(); + + b.ToTable("IgnoredLogChannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("LogSettingId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("LogSettingId"); + + b.ToTable("IgnoredVoicePresenceCHannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ImageOnlyChannel", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChannelId") + .IsUnique(); + + b.ToTable("ImageOnlyChannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.LogSetting", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelCreatedId") + .HasColumnType("INTEGER"); + + b.Property("ChannelDestroyedId") + .HasColumnType("INTEGER"); + + b.Property("ChannelUpdatedId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("LogOtherId") + .HasColumnType("INTEGER"); + + b.Property("LogUserPresenceId") + .HasColumnType("INTEGER"); + + b.Property("LogVoicePresenceId") + .HasColumnType("INTEGER"); + + b.Property("LogVoicePresenceTTSId") + .HasColumnType("INTEGER"); + + b.Property("MessageDeletedId") + .HasColumnType("INTEGER"); + + b.Property("MessageUpdatedId") + .HasColumnType("INTEGER"); + + b.Property("UserBannedId") + .HasColumnType("INTEGER"); + + b.Property("UserJoinedId") + .HasColumnType("INTEGER"); + + b.Property("UserLeftId") + .HasColumnType("INTEGER"); + + b.Property("UserMutedId") + .HasColumnType("INTEGER"); + + b.Property("UserUnbannedId") + .HasColumnType("INTEGER"); + + b.Property("UserUpdatedId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildId") + .IsUnique(); + + b.ToTable("LogSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MusicPlayerSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AutoDisconnect") + .HasColumnType("INTEGER"); + + b.Property("AutoPlay") + .HasColumnType("INTEGER"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("MusicChannelId") + .HasColumnType("INTEGER"); + + b.Property("PlayerRepeat") + .HasColumnType("INTEGER"); + + b.Property("QualityPreset") + .HasColumnType("INTEGER"); + + b.Property("Volume") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(100); + + b.HasKey("Id"); + + b.HasIndex("GuildId") + .IsUnique(); + + b.ToTable("MusicPlayerSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MusicPlaylist", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Author") + .HasColumnType("TEXT"); + + b.Property("AuthorId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("MusicPlaylists"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("MutedUserId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.NadekoExpression", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AllowTarget") + .HasColumnType("INTEGER"); + + b.Property("AutoDeleteTrigger") + .HasColumnType("INTEGER"); + + b.Property("ContainsAnywhere") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("DmResponse") + .HasColumnType("INTEGER"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("Reactions") + .HasColumnType("TEXT"); + + b.Property("Response") + .HasColumnType("TEXT"); + + b.Property("Trigger") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Expressions"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.NsfwBlacklistedTag", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("Tag") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GuildId"); + + b.ToTable("NsfwBlacklistedTags"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("IsCustomCommand") + .HasColumnType("INTEGER"); + + b.Property("PrimaryTarget") + .HasColumnType("INTEGER"); + + b.Property("PrimaryTargetId") + .HasColumnType("INTEGER"); + + b.Property("SecondaryTarget") + .HasColumnType("INTEGER"); + + b.Property("SecondaryTargetName") + .HasColumnType("TEXT"); + + b.Property("State") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("Permissions"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlantedCurrency", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("MessageId") + .HasColumnType("INTEGER"); + + b.Property("Password") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("ChannelId"); + + b.HasIndex("MessageId") + .IsUnique(); + + b.ToTable("PlantedCurrency"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("MusicPlaylistId") + .HasColumnType("INTEGER"); + + b.Property("Provider") + .HasColumnType("TEXT"); + + b.Property("ProviderType") + .HasColumnType("INTEGER"); + + b.Property("Query") + .HasColumnType("TEXT"); + + b.Property("Title") + .HasColumnType("TEXT"); + + b.Property("Uri") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("MusicPlaylistId"); + + b.ToTable("PlaylistSong"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Poll", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("Question") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GuildId") + .IsUnique(); + + b.ToTable("Poll"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PollAnswer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("PollId") + .HasColumnType("INTEGER"); + + b.Property("Text") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("PollId"); + + b.ToTable("PollAnswer"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PollVote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("PollId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("VoteIndex") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PollId"); + + b.ToTable("PollVote"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Quote", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnType("INTEGER"); + + b.Property("AuthorName") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("Keyword") + .IsRequired() + .HasColumnType("TEXT"); + + b.Property("Text") + .IsRequired() + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GuildId"); + + b.HasIndex("Keyword"); + + b.ToTable("Quotes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ReactionRoleV2", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("Emote") + .HasMaxLength(100) + .HasColumnType("TEXT"); + + b.Property("Group") + .HasColumnType("INTEGER"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("LevelReq") + .HasColumnType("INTEGER"); + + b.Property("MessageId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildId"); + + b.HasIndex("MessageId", "Emote") + .IsUnique(); + + b.ToTable("ReactionRoles"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Reminder", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("IsPrivate") + .HasColumnType("INTEGER"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("ServerId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("When") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("When"); + + b.ToTable("Reminders"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Repeater", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("ChannelId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("Interval") + .HasColumnType("TEXT"); + + b.Property("LastMessageId") + .HasColumnType("INTEGER"); + + b.Property("Message") + .HasColumnType("TEXT"); + + b.Property("NoRedundant") + .HasColumnType("INTEGER"); + + b.Property("StartTimeOfDay") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.ToTable("Repeaters"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RewardedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AmountRewardedThisMonth") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("LastReward") + .HasColumnType("TEXT"); + + b.Property("PlatformUserId") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("PlatformUserId") + .IsUnique(); + + b.ToTable("RewardedUsers"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.RotatingPlayingStatus", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("Status") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.ToTable("RotatingStatus"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SelfAssignedRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("Group") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(0); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("LevelRequirement") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildId", "RoleId") + .IsUnique(); + + b.ToTable("SelfAssignableRoles"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AuthorId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("Index") + .HasColumnType("INTEGER"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.Property("RoleName") + .HasColumnType("TEXT"); + + b.Property("Type") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("ShopEntry"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntryItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("ShopEntryId") + .HasColumnType("INTEGER"); + + b.Property("Text") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("ShopEntryId"); + + b.ToTable("ShopEntryItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredRole", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("SlowmodeIgnoredRole"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("SlowmodeIgnoredUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleBlacklistedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("StreamRoleSettingsId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("Username") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("StreamRoleSettingsId"); + + b.ToTable("StreamRoleBlacklistedUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AddRoleId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("Enabled") + .HasColumnType("INTEGER"); + + b.Property("FromRoleId") + .HasColumnType("INTEGER"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("Keyword") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("StreamRoleSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleWhitelistedUser", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("StreamRoleSettingsId") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("Username") + .HasColumnType("TEXT"); + + b.HasKey("Id"); + + b.HasIndex("StreamRoleSettingsId"); + + b.ToTable("StreamRoleWhitelistedUser"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnbanTimer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("UnbanAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("UnbanTimer"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("UnmuteAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("UnmuteTimer"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnroleTimer", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.Property("UnbanAt") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("UnroleTimer"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UserXpStats", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AwardedXp") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("LastLevelUp") + .ValueGeneratedOnAdd() + .HasColumnType("TEXT") + .HasDefaultValueSql("datetime('now')"); + + b.Property("NotifyOnLevelUp") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("Xp") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AwardedXp"); + + b.HasIndex("GuildId"); + + b.HasIndex("UserId"); + + b.HasIndex("Xp"); + + b.HasIndex("UserId", "GuildId") + .IsUnique(); + + b.ToTable("UserXpStats"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.VcRoleInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.Property("VoiceChannelId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("VcRoleInfo"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AffinityId") + .HasColumnType("INTEGER"); + + b.Property("ClaimerId") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("Price") + .HasColumnType("INTEGER"); + + b.Property("WaifuId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("AffinityId"); + + b.HasIndex("ClaimerId"); + + b.HasIndex("Price"); + + b.HasIndex("WaifuId") + .IsUnique(); + + b.ToTable("WaifuInfo"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuItem", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("ItemEmoji") + .HasColumnType("TEXT"); + + b.Property("Name") + .HasColumnType("TEXT"); + + b.Property("WaifuInfoId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("WaifuInfoId"); + + b.ToTable("WaifuItem"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuUpdate", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("NewId") + .HasColumnType("INTEGER"); + + b.Property("OldId") + .HasColumnType("INTEGER"); + + b.Property("UpdateType") + .HasColumnType("INTEGER"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("NewId"); + + b.HasIndex("OldId"); + + b.HasIndex("UserId"); + + b.ToTable("WaifuUpdates"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Warning", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("Forgiven") + .HasColumnType("INTEGER"); + + b.Property("ForgivenBy") + .HasColumnType("TEXT"); + + b.Property("GuildId") + .HasColumnType("INTEGER"); + + b.Property("Moderator") + .HasColumnType("TEXT"); + + b.Property("Reason") + .HasColumnType("TEXT"); + + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("Weight") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(1L); + + b.HasKey("Id"); + + b.HasIndex("DateAdded"); + + b.HasIndex("GuildId"); + + b.HasIndex("UserId"); + + b.ToTable("Warnings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WarningPunishment", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Count") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("Punishment") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.Property("Time") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId"); + + b.ToTable("WarningPunishment"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpCurrencyReward", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("Amount") + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("XpSettingsId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("XpSettingsId"); + + b.ToTable("XpCurrencyReward"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpRoleReward", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("Level") + .HasColumnType("INTEGER"); + + b.Property("Remove") + .HasColumnType("INTEGER"); + + b.Property("RoleId") + .HasColumnType("INTEGER"); + + b.Property("XpSettingsId") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("XpSettingsId", "Level") + .IsUnique(); + + b.ToTable("XpRoleReward"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpSettings", b => + { + b.Property("Id") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("DateAdded") + .HasColumnType("TEXT"); + + b.Property("GuildConfigId") + .HasColumnType("INTEGER"); + + b.Property("ServerExcluded") + .HasColumnType("INTEGER"); + + b.HasKey("Id"); + + b.HasIndex("GuildConfigId") + .IsUnique(); + + b.ToTable("XpSettings"); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.ClubApplicants", b => + { + b.HasOne("NadekoBot.Db.Models.ClubInfo", "Club") + .WithMany("Applicants") + .HasForeignKey("ClubId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NadekoBot.Db.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Club"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.ClubBans", b => + { + b.HasOne("NadekoBot.Db.Models.ClubInfo", "Club") + .WithMany("Bans") + .HasForeignKey("ClubId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.HasOne("NadekoBot.Db.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Club"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.ClubInfo", b => + { + b.HasOne("NadekoBot.Db.Models.DiscordUser", "Owner") + .WithOne() + .HasForeignKey("NadekoBot.Db.Models.ClubInfo", "OwnerId") + .OnDelete(DeleteBehavior.SetNull); + + b.Navigation("Owner"); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.DiscordUser", b => + { + b.HasOne("NadekoBot.Db.Models.ClubInfo", "Club") + .WithMany("Members") + .HasForeignKey("ClubId") + .OnDelete(DeleteBehavior.NoAction); + + b.Navigation("Club"); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.FollowedStream", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("FollowedStreams") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiAltSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithOne("AntiAltSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiAltSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiRaidSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiRaidSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiRaidSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GuildConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamIgnore", b => + { + b.HasOne("NadekoBot.Services.Database.Models.AntiSpamSetting", null) + .WithMany("IgnoredChannels") + .HasForeignKey("AntiSpamSettingId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("AntiSpamSetting") + .HasForeignKey("NadekoBot.Services.Database.Models.AntiSpamSetting", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GuildConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AutoTranslateUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.AutoTranslateChannel", "Channel") + .WithMany("Users") + .HasForeignKey("ChannelId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Channel"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandAlias", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("CommandAliases") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.CommandCooldown", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("CommandCooldowns") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.DelMsgOnCmdChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("DelMsgOnCmdChannels") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ExcludedItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.XpSettings", null) + .WithMany("ExclusionList") + .HasForeignKey("XpSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FeedSub", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithMany("FeedSubs") + .HasForeignKey("GuildConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GuildConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("FilterInvitesChannelIds") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilteredWord", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("FilteredWords") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterLinksChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("FilterLinksChannelIds") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.FilterWordsChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("FilterWordsChannelIds") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GCChannelId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithMany("GenerateCurrencyChannelIds") + .HasForeignKey("GuildConfigId"); + + b.Navigation("GuildConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GroupName", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithMany("SelfAssignableRoleGroupNames") + .HasForeignKey("GuildConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GuildConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredLogItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany("LogIgnores") + .HasForeignKey("LogSettingId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("LogSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.IgnoredVoicePresenceChannel", b => + { + b.HasOne("NadekoBot.Services.Database.Models.LogSetting", "LogSetting") + .WithMany() + .HasForeignKey("LogSettingId"); + + b.Navigation("LogSetting"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MutedUserId", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("MutedUsers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Permissionv2", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("Permissions") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PlaylistSong", b => + { + b.HasOne("NadekoBot.Services.Database.Models.MusicPlaylist", null) + .WithMany("Songs") + .HasForeignKey("MusicPlaylistId") + .OnDelete(DeleteBehavior.Cascade); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PollAnswer", b => + { + b.HasOne("NadekoBot.Services.Database.Models.Poll", null) + .WithMany("Answers") + .HasForeignKey("PollId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.PollVote", b => + { + b.HasOne("NadekoBot.Services.Database.Models.Poll", null) + .WithMany("Votes") + .HasForeignKey("PollId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("ShopEntries") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntryItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.ShopEntry", null) + .WithMany("Items") + .HasForeignKey("ShopEntryId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredRole", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("SlowmodeIgnoredRoles") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.SlowmodeIgnoredUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("SlowmodeIgnoredUsers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleBlacklistedUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.StreamRoleSettings", null) + .WithMany("Blacklist") + .HasForeignKey("StreamRoleSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("StreamRole") + .HasForeignKey("NadekoBot.Services.Database.Models.StreamRoleSettings", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GuildConfig"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleWhitelistedUser", b => + { + b.HasOne("NadekoBot.Services.Database.Models.StreamRoleSettings", null) + .WithMany("Whitelist") + .HasForeignKey("StreamRoleSettingsId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnbanTimer", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("UnbanTimer") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnmuteTimer", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("UnmuteTimers") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.UnroleTimer", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("UnroleTimer") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.VcRoleInfo", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("VcRoleInfos") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.HasOne("NadekoBot.Db.Models.DiscordUser", "Affinity") + .WithMany() + .HasForeignKey("AffinityId"); + + b.HasOne("NadekoBot.Db.Models.DiscordUser", "Claimer") + .WithMany() + .HasForeignKey("ClaimerId"); + + b.HasOne("NadekoBot.Db.Models.DiscordUser", "Waifu") + .WithOne() + .HasForeignKey("NadekoBot.Services.Database.Models.WaifuInfo", "WaifuId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("Affinity"); + + b.Navigation("Claimer"); + + b.Navigation("Waifu"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuItem", b => + { + b.HasOne("NadekoBot.Services.Database.Models.WaifuInfo", "WaifuInfo") + .WithMany("Items") + .HasForeignKey("WaifuInfoId"); + + b.Navigation("WaifuInfo"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuUpdate", b => + { + b.HasOne("NadekoBot.Db.Models.DiscordUser", "New") + .WithMany() + .HasForeignKey("NewId"); + + b.HasOne("NadekoBot.Db.Models.DiscordUser", "Old") + .WithMany() + .HasForeignKey("OldId"); + + b.HasOne("NadekoBot.Db.Models.DiscordUser", "User") + .WithMany() + .HasForeignKey("UserId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("New"); + + b.Navigation("Old"); + + b.Navigation("User"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WarningPunishment", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", null) + .WithMany("WarnPunishments") + .HasForeignKey("GuildConfigId"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpCurrencyReward", b => + { + b.HasOne("NadekoBot.Services.Database.Models.XpSettings", "XpSettings") + .WithMany("CurrencyRewards") + .HasForeignKey("XpSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("XpSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpRoleReward", b => + { + b.HasOne("NadekoBot.Services.Database.Models.XpSettings", "XpSettings") + .WithMany("RoleRewards") + .HasForeignKey("XpSettingsId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("XpSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpSettings", b => + { + b.HasOne("NadekoBot.Services.Database.Models.GuildConfig", "GuildConfig") + .WithOne("XpSettings") + .HasForeignKey("NadekoBot.Services.Database.Models.XpSettings", "GuildConfigId") + .OnDelete(DeleteBehavior.Cascade) + .IsRequired(); + + b.Navigation("GuildConfig"); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.ClubInfo", b => + { + b.Navigation("Applicants"); + + b.Navigation("Bans"); + + b.Navigation("Members"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiSpamSetting", b => + { + b.Navigation("IgnoredChannels"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.AutoTranslateChannel", b => + { + b.Navigation("Users"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.GuildConfig", b => + { + b.Navigation("AntiAltSetting"); + + b.Navigation("AntiRaidSetting"); + + b.Navigation("AntiSpamSetting"); + + b.Navigation("CommandAliases"); + + b.Navigation("CommandCooldowns"); + + b.Navigation("DelMsgOnCmdChannels"); + + b.Navigation("FeedSubs"); + + b.Navigation("FilterInvitesChannelIds"); + + b.Navigation("FilterLinksChannelIds"); + + b.Navigation("FilterWordsChannelIds"); + + b.Navigation("FilteredWords"); + + b.Navigation("FollowedStreams"); + + b.Navigation("GenerateCurrencyChannelIds"); + + b.Navigation("MutedUsers"); + + b.Navigation("Permissions"); + + b.Navigation("SelfAssignableRoleGroupNames"); + + b.Navigation("ShopEntries"); + + b.Navigation("SlowmodeIgnoredRoles"); + + b.Navigation("SlowmodeIgnoredUsers"); + + b.Navigation("StreamRole"); + + b.Navigation("UnbanTimer"); + + b.Navigation("UnmuteTimers"); + + b.Navigation("UnroleTimer"); + + b.Navigation("VcRoleInfos"); + + b.Navigation("WarnPunishments"); + + b.Navigation("XpSettings"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.LogSetting", b => + { + b.Navigation("LogIgnores"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.MusicPlaylist", b => + { + b.Navigation("Songs"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.Poll", b => + { + b.Navigation("Answers"); + + b.Navigation("Votes"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.ShopEntry", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.StreamRoleSettings", b => + { + b.Navigation("Blacklist"); + + b.Navigation("Whitelist"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.WaifuInfo", b => + { + b.Navigation("Items"); + }); + + modelBuilder.Entity("NadekoBot.Services.Database.Models.XpSettings", b => + { + b.Navigation("CurrencyRewards"); + + b.Navigation("ExclusionList"); + + b.Navigation("RoleRewards"); + }); +#pragma warning restore 612, 618 + } + } +} diff --git a/src/NadekoBot/Migrations/Sqlite/20220614071359_patronage-system.cs b/src/NadekoBot/Migrations/Sqlite/20220614071359_patronage-system.cs new file mode 100644 index 000000000..0618bc44f --- /dev/null +++ b/src/NadekoBot/Migrations/Sqlite/20220614071359_patronage-system.cs @@ -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( + name: "VerboseErrors", + table: "GuildConfigs", + type: "INTEGER", + nullable: false, + defaultValue: true, + oldClrType: typeof(bool), + oldType: "INTEGER"); + + migrationBuilder.AlterColumn( + 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(type: "INTEGER", nullable: false), + FeatureType = table.Column(type: "INTEGER", nullable: false), + Feature = table.Column(type: "TEXT", nullable: false), + HourlyCount = table.Column(type: "INTEGER", nullable: false), + DailyCount = table.Column(type: "INTEGER", nullable: false), + MonthlyCount = table.Column(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(type: "INTEGER", nullable: false) + .Annotation("Sqlite:Autoincrement", true), + UniquePlatformUserId = table.Column(type: "TEXT", nullable: true), + AmountCents = table.Column(type: "INTEGER", nullable: false), + LastCharge = table.Column(type: "TEXT", nullable: false), + ValidThru = table.Column(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( + name: "VerboseErrors", + table: "GuildConfigs", + type: "INTEGER", + nullable: false, + oldClrType: typeof(bool), + oldType: "INTEGER", + oldDefaultValue: true); + + migrationBuilder.AlterColumn( + name: "TotalXp", + table: "DiscordUser", + type: "INTEGER", + nullable: false, + defaultValue: 0, + oldClrType: typeof(long), + oldType: "INTEGER", + oldDefaultValue: 0L); + } + } +} diff --git a/src/NadekoBot/Migrations/Sqlite/NadekoSqliteContextModelSnapshot.cs b/src/NadekoBot/Migrations/Sqlite/NadekoSqliteContextModelSnapshot.cs index b389659c9..b0b1ebeed 100644 --- a/src/NadekoBot/Migrations/Sqlite/NadekoSqliteContextModelSnapshot.cs +++ b/src/NadekoBot/Migrations/Sqlite/NadekoSqliteContextModelSnapshot.cs @@ -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("TotalXp") + b.Property("TotalXp") .ValueGeneratedOnAdd() .HasColumnType("INTEGER") - .HasDefaultValue(0); + .HasDefaultValue(0L); b.Property("UserId") .HasColumnType("INTEGER"); @@ -209,6 +209,59 @@ namespace NadekoBot.Migrations b.ToTable("FollowedStream"); }); + modelBuilder.Entity("NadekoBot.Db.Models.PatronQuota", b => + { + b.Property("UserId") + .HasColumnType("INTEGER"); + + b.Property("FeatureType") + .HasColumnType("INTEGER"); + + b.Property("Feature") + .HasColumnType("TEXT"); + + b.Property("DailyCount") + .HasColumnType("INTEGER"); + + b.Property("HourlyCount") + .HasColumnType("INTEGER"); + + b.Property("MonthlyCount") + .HasColumnType("INTEGER"); + + b.HasKey("UserId", "FeatureType", "Feature"); + + b.HasIndex("UserId"); + + b.ToTable("PatronQuotas"); + }); + + modelBuilder.Entity("NadekoBot.Db.Models.PatronUser", b => + { + b.Property("UserId") + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER"); + + b.Property("AmountCents") + .HasColumnType("INTEGER"); + + b.Property("LastCharge") + .HasColumnType("TEXT"); + + b.Property("UniquePlatformUserId") + .HasColumnType("TEXT"); + + b.Property("ValidThru") + .HasColumnType("TEXT"); + + b.HasKey("UserId"); + + b.HasIndex("UniquePlatformUserId") + .IsUnique(); + + b.ToTable("Patrons"); + }); + modelBuilder.Entity("NadekoBot.Services.Database.Models.AntiAltSetting", b => { b.Property("Id") @@ -890,7 +943,9 @@ namespace NadekoBot.Migrations .HasColumnType("TEXT"); b.Property("VerboseErrors") - .HasColumnType("INTEGER"); + .ValueGeneratedOnAdd() + .HasColumnType("INTEGER") + .HasDefaultValue(true); b.Property("VerbosePermissions") .HasColumnType("INTEGER"); @@ -1531,7 +1586,7 @@ namespace NadekoBot.Migrations .ValueGeneratedOnAdd() .HasColumnType("INTEGER"); - b.Property("AmountRewardedThisMonth") + b.Property("AmountRewardedThisMonth") .HasColumnType("INTEGER"); b.Property("DateAdded") @@ -1540,7 +1595,7 @@ namespace NadekoBot.Migrations b.Property("LastReward") .HasColumnType("TEXT"); - b.Property("PatreonUserId") + b.Property("PlatformUserId") .HasColumnType("TEXT"); b.Property("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("AwardedXp") + b.Property("AwardedXp") .HasColumnType("INTEGER"); b.Property("DateAdded") @@ -1897,7 +1952,7 @@ namespace NadekoBot.Migrations b.Property("UserId") .HasColumnType("INTEGER"); - b.Property("Xp") + b.Property("Xp") .HasColumnType("INTEGER"); b.HasKey("Id"); diff --git a/src/NadekoBot/Modules/Administration/DangerousCommands/DangerousCommandsService.cs b/src/NadekoBot/Modules/Administration/DangerousCommands/DangerousCommandsService.cs index f21696068..0f5b3704a 100644 --- a/src/NadekoBot/Modules/Administration/DangerousCommands/DangerousCommandsService.cs +++ b/src/NadekoBot/Modules/Administration/DangerousCommands/DangerousCommandsService.cs @@ -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(); diff --git a/src/NadekoBot/Modules/Administration/Prune/PruneService.cs b/src/NadekoBot/Modules/Administration/Prune/PruneService.cs index 570791564..1a0851006 100644 --- a/src/NadekoBot/Modules/Administration/Prune/PruneService.cs +++ b/src/NadekoBot/Modules/Administration/Prune/PruneService.cs @@ -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(); var singleDeletable = new List(); @@ -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 diff --git a/src/NadekoBot/Modules/Administration/Role/IReactionRoleService.cs b/src/NadekoBot/Modules/Administration/Role/IReactionRoleService.cs index 61b283996..cdd5a51a8 100644 --- a/src/NadekoBot/Modules/Administration/Role/IReactionRoleService.cs +++ b/src/NadekoBot/Modules/Administration/Role/IReactionRoleService.cs @@ -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 /// /// Adds a single reaction role /// - /// - /// - /// + /// Guild where to add a reaction role + /// Message to which to add a reaction role /// /// /// /// - /// - Task AddReactionRole( - ulong guildId, + /// The result of the operation + Task> AddReactionRole( + IGuild guild, IMessage msg, - ITextChannel channel, string emote, IRole role, int group = 0, diff --git a/src/NadekoBot/Modules/Administration/Role/ReactionRoleCommands.cs b/src/NadekoBot/Modules/Administration/Role/ReactionRoleCommands.cs index 7c0b436e8..e6c58a6b4 100644 --- a/src/NadekoBot/Modules/Administration/Role/ReactionRoleCommands.cs +++ b/src/NadekoBot/Modules/Administration/Role/ReactionRoleCommands.cs @@ -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)] diff --git a/src/NadekoBot/Modules/Administration/Role/ReactionRolesService.cs b/src/NadekoBot/Modules/Administration/Role/ReactionRolesService.cs index ac01974a3..12ae0460a 100644 --- a/src/NadekoBot/Modules/Administration/Role/ReactionRolesService.cs +++ b/src/NadekoBot/Modules/Administration/Role/ReactionRolesService.cs @@ -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> _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() - .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() - .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 /// /// Adds a single reaction role /// - /// - /// - /// + /// Guild where to add a reaction role + /// Message to which to add a reaction role /// /// /// /// - /// - public async Task AddReactionRole( - ulong guildId, + /// The result of the operation + public async Task> 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() - .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() + .InsertOrUpdateAsync(() => new() + { + GuildId = guild.Id, + ChannelId = msg.Channel.Id, - var changed = await ctx.GetTable() - .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(); } /// @@ -326,7 +342,10 @@ public sealed class ReactionRolesService : IReadyExecutor, INService, IReactionR return output.Length; } - public async Task> TransferReactionRolesAsync(ulong guildId, ulong fromMessageId, ulong toMessageId) + public async Task> TransferReactionRolesAsync( + ulong guildId, + ulong fromMessageId, + ulong toMessageId) { await using var ctx = _db.GetDbContext(); var updated = ctx.GetTable() diff --git a/src/NadekoBot/Modules/Administration/Role/RoleCommands.cs b/src/NadekoBot/Modules/Administration/Role/RoleCommands.cs index 89d06d4d4..6da732203 100644 --- a/src/NadekoBot/Modules/Administration/Role/RoleCommands.cs +++ b/src/NadekoBot/Modules/Administration/Role/RoleCommands.cs @@ -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); diff --git a/src/NadekoBot/Modules/Gambling/Bank/BankCommands.cs b/src/NadekoBot/Modules/Gambling/Bank/BankCommands.cs index a0cd16ab1..7061da101 100644 --- a/src/NadekoBot/Modules/Gambling/Bank/BankCommands.cs +++ b/src/NadekoBot/Modules/Gambling/Bank/BankCommands.cs @@ -65,7 +65,7 @@ public partial class Gambling } catch { - await ReplyErrorLocalizedAsync(strs.unable_to_dm_user); + await ReplyErrorLocalizedAsync(strs.cant_dm); } } } diff --git a/src/NadekoBot/Modules/Gambling/Bank/BankService.cs b/src/NadekoBot/Modules/Gambling/Bank/BankService.cs index 3ca5c38d5..e754df30e 100644 --- a/src/NadekoBot/Modules/Gambling/Bank/BankService.cs +++ b/src/NadekoBot/Modules/Gambling/Bank/BankService.cs @@ -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 BurnAllAsync(ulong userId) + { + await using var ctx = _db.GetDbContext(); + var output = await ctx.GetTable() + .Where(x => x.UserId == userId) + .UpdateWithOutputAsync(old => new() + { + Balance = 0 + }); + if (output.Length == 0) + return 0; + + return output[0].Deleted.Balance; + } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Bank/IBankService.cs b/src/NadekoBot/Modules/Gambling/Bank/IBankService.cs index d35166f68..1140fbcde 100644 --- a/src/NadekoBot/Modules/Gambling/Bank/IBankService.cs +++ b/src/NadekoBot/Modules/Gambling/Bank/IBankService.cs @@ -5,4 +5,5 @@ public interface IBankService Task DepositAsync(ulong userId, long amount); Task WithdrawAsync(ulong userId, long amount); Task GetBalanceAsync(ulong userId); + Task BurnAllAsync(ulong userId); } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/CashInteraction.cs b/src/NadekoBot/Modules/Gambling/CashInteraction.cs index 2628e1cc4..8aef1c1ca 100644 --- a/src/NadekoBot/Modules/Gambling/CashInteraction.cs +++ b/src/NadekoBot/Modules/Gambling/CashInteraction.cs @@ -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 action) - => new NadekoInteractionBuilder() - .WithData(Data) - .WithAction(action) - .Build(client, userId); + public CashInteraction(DiscordSocketClient client, ulong userId, Func action) + : base(client, userId, action) + { + } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Gambling.cs b/src/NadekoBot/Modules/Gambling/Gambling.cs index 8e6734a81..dd7386ece 100644 --- a/src/NadekoBot/Modules/Gambling/Gambling.cs +++ b/src/NadekoBot/Modules/Gambling/Gambling.cs @@ -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 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 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 _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 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 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 .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 await ctx.Channel.EmbedAsync(embed); } + + private static readonly ImmutableArray _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]}』")); + } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Wheel/WheelOfFortuneCommands.cs b/src/NadekoBot/Modules/Gambling/Wheel/WheelOfFortuneCommands.cs deleted file mode 100644 index 4f8434b26..000000000 --- a/src/NadekoBot/Modules/Gambling/Wheel/WheelOfFortuneCommands.cs +++ /dev/null @@ -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 - { - private static readonly ImmutableArray _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]}』")); - } - } -} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Gambling/Wheel/WheelOfFortune.cs b/src/NadekoBot/Modules/Gambling/WheelOfFortune.cs similarity index 100% rename from src/NadekoBot/Modules/Gambling/Wheel/WheelOfFortune.cs rename to src/NadekoBot/Modules/Gambling/WheelOfFortune.cs diff --git a/src/NadekoBot/Modules/Games/ChatterBot/ChatterbotService.cs b/src/NadekoBot/Modules/Games/ChatterBot/ChatterbotService.cs index 6d925f2b2..05beced7a 100644 --- a/src/NadekoBot/Modules/Games/ChatterBot/ChatterbotService.cs +++ b/src/NadekoBot/Modules/Games/ChatterBot/ChatterbotService.cs @@ -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(() => 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 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 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) { diff --git a/src/NadekoBot/Modules/Games/ChatterBot/CleverBotCommands.cs b/src/NadekoBot/Modules/Games/ChatterBot/CleverBotCommands.cs index 00a0a59a0..d96efaacd 100644 --- a/src/NadekoBot/Modules/Games/ChatterBot/CleverBotCommands.cs +++ b/src/NadekoBot/Modules/Games/ChatterBot/CleverBotCommands.cs @@ -14,7 +14,6 @@ public partial class Games public ChatterBotCommands(DbService db) => _db = db; - [NoPublicBot] [Cmd] [RequireContext(ContextType.Guild)] [UserPerm(GuildPerm.ManageMessages)] diff --git a/src/NadekoBot/Modules/Games/ChatterBot/_Common/ChatterBotSession.cs b/src/NadekoBot/Modules/Games/ChatterBot/_Common/ChatterBotSession.cs deleted file mode 100644 index eca281a2c..000000000 --- a/src/NadekoBot/Modules/Games/ChatterBot/_Common/ChatterBotSession.cs +++ /dev/null @@ -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 Think(string message) - { - using var http = _httpFactory.CreateClient(); - var res = await http.GetStringAsync(string.Format(ApiEndpoint, message)); - var cbr = JsonConvert.DeserializeObject(res); - return cbr.BotSay.Replace("
", "\n", StringComparison.InvariantCulture); - } -} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/ChatterBot/_Common/CleverbotResponse.cs b/src/NadekoBot/Modules/Games/ChatterBot/_Common/CleverbotResponse.cs index 60b9eddda..27d64a792 100644 --- a/src/NadekoBot/Modules/Games/ChatterBot/_Common/CleverbotResponse.cs +++ b/src/NadekoBot/Modules/Games/ChatterBot/_Common/CleverbotResponse.cs @@ -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; } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Games/ChatterBot/_Common/OfficialCleverbotSession.cs b/src/NadekoBot/Modules/Games/ChatterBot/_Common/OfficialCleverbotSession.cs index 1960f2343..73ed93bd2 100644 --- a/src/NadekoBot/Modules/Games/ChatterBot/_Common/OfficialCleverbotSession.cs +++ b/src/NadekoBot/Modules/Games/ChatterBot/_Common/OfficialCleverbotSession.cs @@ -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 _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 GetNick() - { - using var http = _httpFactory.CreateClient(); - using var msg = new FormUrlEncodedContent(new[] - { - new KeyValuePair("user", _user), new KeyValuePair("key", _key) - }); - using var data = await http.PostAsync(_createEndpoint, msg); - var str = await data.Content.ReadAsStringAsync(); - var obj = JsonConvert.DeserializeObject(str); - if (obj.Status != "success") - throw new OperationCanceledException(obj.Status); - - return obj.Nick; - } - - public async Task Think(string input) - { - using var http = _httpFactory.CreateClient(); - using var msg = new FormUrlEncodedContent(new[] - { - new KeyValuePair("user", _user), new KeyValuePair("key", _key), - new KeyValuePair("nick", await _nick), new KeyValuePair("text", input) - }); - using var data = await http.PostAsync(_askEndpoint, msg); - var str = await data.Content.ReadAsStringAsync(); - var obj = JsonConvert.DeserializeObject(str); - if (obj.Status != "success") - throw new OperationCanceledException(obj.Status); - - return obj.Response; - } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Help/Help.cs b/src/NadekoBot/Modules/Help/Help.cs index 46cb4969a..3a49bee52 100644 --- a/src/NadekoBot/Modules/Help/Help.cs +++ b/src/NadekoBot/Modules/Help/Help.cs @@ -212,7 +212,7 @@ public partial class Help : NadekoModule 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 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 }; 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 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>(versionListString); if (versionList is not null && !versionList.Contains(StatsService.BOT_VERSION)) @@ -435,7 +445,7 @@ public partial class Help : NadekoModule 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 [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 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: + + +**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 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); + } + } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Help/HelpService.cs b/src/NadekoBot/Modules/Help/HelpService.cs index dc23ab9a0..1739c5eac 100644 --- a/src/NadekoBot/Modules/Help/HelpService.cs +++ b/src/NadekoBot/Modules/Help/HelpService.cs @@ -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); diff --git a/src/NadekoBot/Modules/Help/Interactions/DonateSelfhostingInteraction.cs b/src/NadekoBot/Modules/Help/Interactions/DonateSelfhostingInteraction.cs new file mode 100644 index 000000000..f71a793e5 --- /dev/null +++ b/src/NadekoBot/Modules/Help/Interactions/DonateSelfhostingInteraction.cs @@ -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 action) + : base(client, userId, action) + { + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Help/Interactions/DonateTroubleshootInteraction.cs b/src/NadekoBot/Modules/Help/Interactions/DonateTroubleshootInteraction.cs new file mode 100644 index 000000000..8a3eacf8a --- /dev/null +++ b/src/NadekoBot/Modules/Help/Interactions/DonateTroubleshootInteraction.cs @@ -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 action) + : base(client, userId, action) + { + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Music/Services/extractor/YtLoader.cs b/src/NadekoBot/Modules/Music/Services/extractor/YtLoader.cs index eab4ebfca..2e2b32334 100644 --- a/src/NadekoBot/Modules/Music/Services/extractor/YtLoader.cs +++ b/src/NadekoBot/Modules/Music/Services/extractor/YtLoader.cs @@ -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 = diff --git a/src/NadekoBot/Modules/Music/_Common/Impl/MusicPlayer.cs b/src/NadekoBot/Modules/Music/_Common/Impl/MusicPlayer.cs index 5cabf8509..963c90270 100644 --- a/src/NadekoBot/Modules/Music/_Common/Impl/MusicPlayer.cs +++ b/src/NadekoBot/Modules/Music/_Common/Impl/MusicPlayer.cs @@ -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); diff --git a/src/NadekoBot/Modules/Music/_Common/Resolvers/RadioResolveStrategy.cs b/src/NadekoBot/Modules/Music/_Common/Resolvers/RadioResolveStrategy.cs index 49c665276..3a9e10063 100644 --- a/src/NadekoBot/Modules/Music/_Common/Resolvers/RadioResolveStrategy.cs +++ b/src/NadekoBot/Modules/Music/_Common/Resolvers/RadioResolveStrategy.cs @@ -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")) - // { try { @@ -95,12 +87,6 @@ public class RadioResolver : IRadioResolver } if (query.Contains(".xspf")) - /* - - - - file:///mp3s/song_1.mp3 - */ { try { diff --git a/src/NadekoBot/Modules/Permissions/CommandCooldown/CleverBotResponseStr.cs b/src/NadekoBot/Modules/Permissions/CommandCooldown/CleverBotResponseStr.cs new file mode 100644 index 000000000..ad02f2bf6 --- /dev/null +++ b/src/NadekoBot/Modules/Permissions/CommandCooldown/CleverBotResponseStr.cs @@ -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"; +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Permissions/CommandCooldown/CleverbotResponseCmdCdTypeReader.cs b/src/NadekoBot/Modules/Permissions/CommandCooldown/CleverbotResponseCmdCdTypeReader.cs new file mode 100644 index 000000000..51ef19c8c --- /dev/null +++ b/src/NadekoBot/Modules/Permissions/CommandCooldown/CleverbotResponseCmdCdTypeReader.cs @@ -0,0 +1,15 @@ +#nullable disable +using NadekoBot.Common.TypeReaders; +using static NadekoBot.Common.TypeReaders.TypeReaderResult; + +namespace NadekoBot.Modules.Permissions; + +public class CleverbotResponseCmdCdTypeReader : NadekoTypeReader +{ + public override ValueTask> ReadAsync( + ICommandContext ctx, + string input) + => input.ToLowerInvariant() == CleverBotResponseStr.CLEVERBOT_RESPONSE + ? new(FromSuccess(new CleverBotResponseStr())) + : new(FromError(CommandError.ParseFailed, "Not a valid cleverbot")); +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Permissions/CommandCooldown/CmdCdsCommands.cs b/src/NadekoBot/Modules/Permissions/CommandCooldown/CmdCdsCommands.cs index 970d84e1d..e8dedc3ff 100644 --- a/src/NadekoBot/Modules/Permissions/CommandCooldown/CmdCdsCommands.cs +++ b/src/NadekoBot/Modules/Permissions/CommandCooldown/CmdCdsCommands.cs @@ -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)] diff --git a/src/NadekoBot/Modules/Searches/Feeds/FeedsService.cs b/src/NadekoBot/Modules/Searches/Feeds/FeedsService.cs index 925e53113..991c6af29 100644 --- a/src/NadekoBot/Modules/Searches/Feeds/FeedsService.cs +++ b/src/NadekoBot/Modules/Searches/Feeds/FeedsService.cs @@ -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> _subs; + private readonly ConcurrentDictionary> _subs; private readonly DiscordSocketClient _client; private readonly IEmbedBuilderService _eb; private readonly ConcurrentDictionary _lastPosts = new(); + private readonly Dictionary _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 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() + .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 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 + new List { feed }, @@ -216,7 +254,7 @@ public class FeedsService : INService return false; var toRemove = items[index]; _subs.AddOrUpdate(toRemove.Url.ToLower(), - new HashSet(), + new List(), (_, old) => { old.Remove(toRemove); diff --git a/src/NadekoBot/Modules/Searches/Search/DefaultSearchServiceFactory.cs b/src/NadekoBot/Modules/Searches/Search/DefaultSearchServiceFactory.cs new file mode 100644 index 000000000..9060a62e0 --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Search/DefaultSearchServiceFactory.cs @@ -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 + }; +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Search/DuckDuckGoScrape/DuckDuckGoSeachService.cs b/src/NadekoBot/Modules/Searches/Search/DuckDuckGoScrape/DuckDuckGoSeachService.cs new file mode 100644 index 000000000..7aacde717 --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Search/DuckDuckGoScrape/DuckDuckGoSeachService.cs @@ -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 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"); +// } +// } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Search/Google/GoogleCustomSearchResult.cs b/src/NadekoBot/Modules/Searches/Search/Google/GoogleCustomSearchResult.cs new file mode 100644 index 000000000..1234abe1a --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Search/Google/GoogleCustomSearchResult.cs @@ -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 ISearchResult.Entries + => Entries ?? Array.Empty(); + + [JsonPropertyName("searchInformation")] + public GoogleSearchResultInformation Info { get; init; } = null!; + + [JsonPropertyName("items")] + public IReadOnlyCollection? Entries { get; init; } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Search/Google/GoogleImageData.cs b/src/NadekoBot/Modules/Searches/Search/Google/GoogleImageData.cs new file mode 100644 index 000000000..079218f3f --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Search/Google/GoogleImageData.cs @@ -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!; +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Search/Google/GoogleImageResult.cs b/src/NadekoBot/Modules/Searches/Search/Google/GoogleImageResult.cs new file mode 100644 index 000000000..e29afa3bc --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Search/Google/GoogleImageResult.cs @@ -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 IImageSearchResult.Entries + => Entries ?? Array.Empty(); + + [JsonPropertyName("searchInformation")] + public GoogleSearchResultInformation Info { get; init; } = null!; + + [JsonPropertyName("items")] + public IReadOnlyCollection? Entries { get; init; } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Search/Google/GoogleImageResultEntry.cs b/src/NadekoBot/Modules/Searches/Search/Google/GoogleImageResultEntry.cs new file mode 100644 index 000000000..304c61ff8 --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Search/Google/GoogleImageResultEntry.cs @@ -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!; +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Search/Google/GoogleSearchResultInformation.cs b/src/NadekoBot/Modules/Searches/Search/Google/GoogleSearchResultInformation.cs new file mode 100644 index 000000000..265c1d7ae --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Search/Google/GoogleSearchResultInformation.cs @@ -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!; +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Search/Google/GoogleSearchService.cs b/src/NadekoBot/Modules/Searches/Search/Google/GoogleSearchService.cs new file mode 100644 index 000000000..5639ffa1c --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Search/Google/GoogleSearchService.cs @@ -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 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(stream); + + return result; + } + + public override async ITask 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(stream); + + return result; + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Search/Google/OfficialGoogleSearchResultEntry.cs b/src/NadekoBot/Modules/Searches/Search/Google/OfficialGoogleSearchResultEntry.cs new file mode 100644 index 000000000..362b2c8d6 --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Search/Google/OfficialGoogleSearchResultEntry.cs @@ -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!; +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Search/GoogleScrape/GoogleScrapeService.cs b/src/NadekoBot/Modules/Searches/Search/GoogleScrape/GoogleScrapeService.cs new file mode 100644 index 000000000..d9aa576a7 --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Search/GoogleScrape/GoogleScrapeService.cs @@ -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 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, 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); +// } +// } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Search/IImageSearchResult.cs b/src/NadekoBot/Modules/Searches/Search/IImageSearchResult.cs new file mode 100644 index 000000000..4e6e4d32a --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Search/IImageSearchResult.cs @@ -0,0 +1,13 @@ +namespace NadekoBot.Modules.Searches; + +public interface IImageSearchResult +{ + ISearchResultInformation Info { get; } + + IReadOnlyCollection Entries { get; } +} + +public interface IImageSearchResultEntry +{ + string Link { get; } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Search/ISearchResult.cs b/src/NadekoBot/Modules/Searches/Search/ISearchResult.cs new file mode 100644 index 000000000..3f03875c0 --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Search/ISearchResult.cs @@ -0,0 +1,8 @@ +namespace NadekoBot.Modules.Searches; + +public interface ISearchResult +{ + string? Answer { get; } + IReadOnlyCollection Entries { get; } + ISearchResultInformation Info { get; } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Search/ISearchResultEntry.cs b/src/NadekoBot/Modules/Searches/Search/ISearchResultEntry.cs new file mode 100644 index 000000000..96a2bbe50 --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Search/ISearchResultEntry.cs @@ -0,0 +1,9 @@ +namespace NadekoBot.Modules.Searches; + +public interface ISearchResultEntry +{ + string Title { get; } + string Url { get; } + string DisplayUrl { get; } + string? Description { get; } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Search/ISearchResultInformation.cs b/src/NadekoBot/Modules/Searches/Search/ISearchResultInformation.cs new file mode 100644 index 000000000..98e3f936d --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Search/ISearchResultInformation.cs @@ -0,0 +1,7 @@ +namespace NadekoBot.Modules.Searches; + +public interface ISearchResultInformation +{ + string TotalResults { get; } + string SearchTime { get; } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Search/ISearchService.cs b/src/NadekoBot/Modules/Searches/Search/ISearchService.cs new file mode 100644 index 000000000..ac903af90 --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Search/ISearchService.cs @@ -0,0 +1,9 @@ +using MorseCode.ITask; + +namespace NadekoBot.Modules.Searches; + +public interface ISearchService +{ + ITask SearchAsync(string query); + ITask SearchImagesAsync(string query); +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Search/ISearchServiceFactory.cs b/src/NadekoBot/Modules/Searches/Search/ISearchServiceFactory.cs new file mode 100644 index 000000000..951c2eab4 --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Search/ISearchServiceFactory.cs @@ -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); +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Search/SearchCommands.cs b/src/NadekoBot/Modules/Searches/Search/SearchCommands.cs new file mode 100644 index 000000000..c7c46a369 --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Search/SearchCommands.cs @@ -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(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 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); +// } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Search/SearchServiceBase.cs b/src/NadekoBot/Modules/Searches/Search/SearchServiceBase.cs new file mode 100644 index 000000000..385dfc5c4 --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Search/SearchServiceBase.cs @@ -0,0 +1,9 @@ +using MorseCode.ITask; + +namespace NadekoBot.Modules.Searches; + +public abstract class SearchServiceBase : ISearchService +{ + public abstract ITask SearchAsync(string query); + public abstract ITask SearchImagesAsync(string query); +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Search/Searx/SearxImageSearchResult.cs b/src/NadekoBot/Modules/Searches/Search/Searx/SearxImageSearchResult.cs new file mode 100644 index 000000000..6f9dca024 --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Search/Searx/SearxImageSearchResult.cs @@ -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 Entries + => Results; + + [JsonPropertyName("results")] + public List Results { get; set; } = new List(); + + [JsonPropertyName("query")] + public string Query { get; set; } = null!; + + [JsonPropertyName("number_of_results")] + public double NumberOfResults { get; set; } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Search/Searx/SearxImageSearchResultEntry.cs b/src/NadekoBot/Modules/Searches/Search/Searx/SearxImageSearchResultEntry.cs new file mode 100644 index 000000000..b04c34255 --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Search/Searx/SearxImageSearchResultEntry.cs @@ -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; +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Search/Searx/SearxInfobox.cs b/src/NadekoBot/Modules/Searches/Search/Searx/SearxInfobox.cs new file mode 100644 index 000000000..2d6c51f92 --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Search/Searx/SearxInfobox.cs @@ -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 Urls { get; } = new List(); +// +// [JsonPropertyName("engine")] +// public string Engine { get; set; } +// +// [JsonPropertyName("engines")] +// public List Engines { get; } = new List(); +// +// [JsonPropertyName("attributes")] +// public List Attributes { get; } = new List(); +// } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Search/Searx/SearxSearchAttribute.cs b/src/NadekoBot/Modules/Searches/Search/Searx/SearxSearchAttribute.cs new file mode 100644 index 000000000..876479e52 --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Search/Searx/SearxSearchAttribute.cs @@ -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; } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Search/Searx/SearxSearchResult.cs b/src/NadekoBot/Modules/Searches/Search/Searx/SearxSearchResult.cs new file mode 100644 index 000000000..13f4f80a0 --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Search/Searx/SearxSearchResult.cs @@ -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 Results { get; set; } = new List(); + + [JsonPropertyName("answers")] + public List Answers { get; set; } = new List(); + // + // [JsonPropertyName("corrections")] + // public List Corrections { get; } = new List(); + + // [JsonPropertyName("infoboxes")] + // public List Infoboxes { get; } = new List(); + // + // [JsonPropertyName("suggestions")] + // public List Suggestions { get; } = new List(); + + // [JsonPropertyName("unresponsive_engines")] + // public List UnresponsiveEngines { get; } = new List(); + + + public string SearchTime { get; set; } = null!; + + public IReadOnlyCollection Entries + => Results; + + public ISearchResultInformation Info + => new SearxSearchResultInformation() + { + SearchTime = SearchTime, + TotalResults = NumberOfResults.ToString("N", CultureInfo.InvariantCulture) + }; + + public string? Answer + => Answers.FirstOrDefault(); +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Search/Searx/SearxSearchResultEntry.cs b/src/NadekoBot/Modules/Searches/Search/Searx/SearxSearchResultEntry.cs new file mode 100644 index 000000000..af4b354aa --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Search/Searx/SearxSearchResultEntry.cs @@ -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 ParsedUrl { get; } = new List(); + // + // [JsonPropertyName("template")] + // public string Template { get; set; } + // + // [JsonPropertyName("engines")] + // public List Engines { get; } = new List(); + // + // [JsonPropertyName("positions")] + // public List Positions { get; } = new List(); + // + // [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; } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Search/Searx/SearxSearchResultInformation.cs b/src/NadekoBot/Modules/Searches/Search/Searx/SearxSearchResultInformation.cs new file mode 100644 index 000000000..c3a307328 --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Search/Searx/SearxSearchResultInformation.cs @@ -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; +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Search/Searx/SearxSearchService.cs b/src/NadekoBot/Modules/Searches/Search/Searx/SearxSearchService.cs new file mode 100644 index 000000000..374c601eb --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Search/Searx/SearxSearchService.cs @@ -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 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(res); + + if (dat is null) + return new SearxSearchResult(); + + dat.SearchTime = sw.Elapsed.TotalSeconds.ToString("N2", CultureInfo.InvariantCulture); + return dat; + } + + public override async ITask 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(res); + + if (dat is null) + return new SearxImageSearchResult(); + + dat.SearchTime = sw.Elapsed.TotalSeconds.ToString("N2", CultureInfo.InvariantCulture); + return dat; + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Search/Searx/SearxUrlData.cs b/src/NadekoBot/Modules/Searches/Search/Searx/SearxUrlData.cs new file mode 100644 index 000000000..8e16e0177 --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Search/Searx/SearxUrlData.cs @@ -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; } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Search/Youtube/IYoutubeSearchService.cs b/src/NadekoBot/Modules/Searches/Search/Youtube/IYoutubeSearchService.cs new file mode 100644 index 000000000..b69b5fd91 --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Search/Youtube/IYoutubeSearchService.cs @@ -0,0 +1,6 @@ +namespace NadekoBot.Modules.Searches.Youtube; + +public interface IYoutubeSearchService +{ + Task SearchAsync(string query); +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Search/Youtube/InvidiousSearchResponse.cs b/src/NadekoBot/Modules/Searches/Search/Youtube/InvidiousSearchResponse.cs new file mode 100644 index 000000000..101f725de --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Search/Youtube/InvidiousSearchResponse.cs @@ -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!; +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Search/Youtube/InvidiousYtSearchService.cs b/src/NadekoBot/Modules/Searches/Search/Youtube/InvidiousYtSearchService.cs new file mode 100644 index 000000000..7ec1efa7d --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Search/Youtube/InvidiousYtSearchService.cs @@ -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 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>( + $"{instance}/api/v1/search" + + $"?q={query}" + + $"&type=video"); + + if (res is null or {Count: 0}) + return null; + + return new VideoInfo(res[0].VideoId); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Search/Youtube/VideoInfo.cs b/src/NadekoBot/Modules/Searches/Search/Youtube/VideoInfo.cs new file mode 100644 index 000000000..edf429694 --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Search/Youtube/VideoInfo.cs @@ -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; } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Search/Youtube/YoutubeDataApiSearchService.cs b/src/NadekoBot/Modules/Searches/Search/Youtube/YoutubeDataApiSearchService.cs new file mode 100644 index 000000000..386a83d15 --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Search/Youtube/YoutubeDataApiSearchService.cs @@ -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 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 + }; + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Search/Youtube/YtdlYoutubeSearchService.cs b/src/NadekoBot/Modules/Searches/Search/Youtube/YtdlYoutubeSearchService.cs new file mode 100644 index 000000000..6c1223c2c --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Search/Youtube/YtdlYoutubeSearchService.cs @@ -0,0 +1,7 @@ +namespace NadekoBot.Modules.Searches.Youtube; + +public sealed class YtdlYoutubeSearchService : YoutubedlxServiceBase, INService +{ + public override async Task SearchAsync(string query) + => await InternalGetInfoAsync(query, false); +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Search/Youtube/YtdlpYoutubeSearchService.cs b/src/NadekoBot/Modules/Searches/Search/Youtube/YtdlpYoutubeSearchService.cs new file mode 100644 index 000000000..b39f4d6bd --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Search/Youtube/YtdlpYoutubeSearchService.cs @@ -0,0 +1,7 @@ +namespace NadekoBot.Modules.Searches.Youtube; + +public sealed class YtdlpYoutubeSearchService : YoutubedlxServiceBase, INService +{ + public override async Task SearchAsync(string query) + => await InternalGetInfoAsync(query, true); +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Search/Youtube/YtdlxServiceBase.cs b/src/NadekoBot/Modules/Searches/Search/Youtube/YtdlxServiceBase.cs new file mode 100644 index 000000000..a9ac1b7f9 --- /dev/null +++ b/src/NadekoBot/Modules/Searches/Search/Youtube/YtdlxServiceBase.cs @@ -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 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 SearchAsync(string query); +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/Searches.cs b/src/NadekoBot/Modules/Searches/Searches.cs index b32bae5f8..0bc986486 100644 --- a/src/NadekoBot/Modules/Searches/Searches.cs +++ b/src/NadekoBot/Modules/Searches/Searches.cs @@ -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 .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 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 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 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 .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 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(res).List; @@ -732,7 +597,7 @@ public partial class Searches : NadekoModule await ctx.Channel.SendMessageAsync($"https://store.steampowered.com/app/{appId}"); } - private async Task ValidateQuery(string query) + private async Task ValidateQuery([MaybeNullWhen(false)] string query) { if (!string.IsNullOrWhiteSpace(query)) return true; diff --git a/src/NadekoBot/Modules/Searches/SearchesService.cs b/src/NadekoBot/Modules/Searches/SearchesService.cs index 9d5612da7..d1cebc3fd 100644 --- a/src/NadekoBot/Modules/Searches/SearchesService.cs +++ b/src/NadekoBot/Modules/Searches/SearchesService.cs @@ -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 WowJokes { get; } = new(); public List 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 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() } }).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 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, 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 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 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>(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 Results { get; } - public string FullQueryLink { get; } - public string TotalResults { get; } - - public GoogleSearchResultData( - IReadOnlyList results, - string fullQueryLink, - string totalResults) - { - Results = results; - FullQueryLink = fullQueryLink; - TotalResults = totalResults; - } } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/StreamNotification/StreamNotificationService.cs b/src/NadekoBot/Modules/Searches/StreamNotification/StreamNotificationService.cs index 66f2b5484..1992b8223 100644 --- a/src/NadekoBot/Modules/Searches/StreamNotification/StreamNotificationService.cs +++ b/src/NadekoBot/Modules/Searches/StreamNotification/StreamNotificationService.cs @@ -1,6 +1,4 @@ #nullable disable -using LinqToDB; -using LinqToDB.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; using NadekoBot.Common.ModuleBehaviors; using NadekoBot.Db; diff --git a/src/NadekoBot/Modules/Searches/Services/AtlExtensions.cs b/src/NadekoBot/Modules/Searches/_Common/AtlExtensions.cs similarity index 100% rename from src/NadekoBot/Modules/Searches/Services/AtlExtensions.cs rename to src/NadekoBot/Modules/Searches/_Common/AtlExtensions.cs diff --git a/src/NadekoBot/Modules/Searches/_Common/Config/ImgSearchEngine.cs b/src/NadekoBot/Modules/Searches/_Common/Config/ImgSearchEngine.cs new file mode 100644 index 000000000..639b54a71 --- /dev/null +++ b/src/NadekoBot/Modules/Searches/_Common/Config/ImgSearchEngine.cs @@ -0,0 +1,7 @@ +namespace NadekoBot.Modules.Searches; + +public enum ImgSearchEngine +{ + Google, + Searx, +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/_Common/Config/SearchesConfig.cs b/src/NadekoBot/Modules/Searches/_Common/Config/SearchesConfig.cs new file mode 100644 index 000000000..3da7b0743 --- /dev/null +++ b/src/NadekoBot/Modules/Searches/_Common/Config/SearchesConfig.cs @@ -0,0 +1,63 @@ +using Cloneable; +using NadekoBot.Common.Yml; + +namespace NadekoBot.Modules.Searches; + +[Cloneable] +public partial class SearchesConfig : ICloneable +{ + [Comment(@"Which engine should .search command +'google' requires googleApiKey and google.searchId set in creds.yml +'searx' requires at least one searx instance specified in the 'searxInstances' property below")] + public WebSearchEngine WebSearchEngine { get; set; } = WebSearchEngine.Google; + + [Comment(@"Which engine should .image command use +'google' requires googleApiKey and google.imageSearchId set in creds.yml +'searx' requires at least one searx instance specified in the 'searxInstances' property below")] + public ImgSearchEngine ImgSearchEngine { get; set; } = ImgSearchEngine.Google; + + + [Comment(@"Which search provider will be used for the `.youtube` command. + +- `ytDataApiv3` - uses google's official youtube data api. Requires `GoogleApiKey` set in creds and youtube data api enabled in developers console + +- `ytdl` - default, uses youtube-dl. Requires `youtube-dl` to be installed and it's path added to env variables. Slow. + +- `ytdlp` - recommended easy, uses `yt-dlp`. Requires `yt-dlp` to be installed and it's path added to env variables + +- `invidious` - recommended advanced, uses invidious api. Requires at least one invidious instance specified in the `invidiousInstances` property")] + public YoutubeSearcher YtProvider { get; set; } = YoutubeSearcher.Ytdl; + + [Comment(@"Set the searx instance urls in case you want to use 'searx' for either img or web search. +Nadeko will use a random one for each request. +Use a fully qualified url. Example: `https://my-searx-instance.mydomain.com` +Instances specified must support 'format=json' query parameter. +- In case you're running your own searx instance, set + +search: + formats: + - json + +in 'searxng/settings.yml' on your server + +- If you're using a public instance, make sure that the instance you're using supports it (they usually don't)")] + public List SearxInstances { get; set; } = new List(); + + [Comment(@"Set the invidious instance urls in case you want to use 'invidious' for `.youtube` search +Nadeko will use a random one for each request. +These instances may be used for music queue functionality in the future. +Use a fully qualified url. Example: https://my-invidious-instance.mydomain.com + +Instances specified must have api available. +You check that by opening an api endpoint in your browser. For example: https://my-invidious-instance.mydomain.com/api/v1/trending")] + public List InvidiousInstances { get; set; } = new List(); +} + +public enum YoutubeSearcher +{ + YtDataApiv3, + Ytdl, + Ytdlp, + Invid, + Invidious = 3 +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/_Common/Config/SearchesConfigService.cs b/src/NadekoBot/Modules/Searches/_Common/Config/SearchesConfigService.cs new file mode 100644 index 000000000..3222da3f7 --- /dev/null +++ b/src/NadekoBot/Modules/Searches/_Common/Config/SearchesConfigService.cs @@ -0,0 +1,31 @@ +using NadekoBot.Common.Configs; + +namespace NadekoBot.Modules.Searches; + +public class SearchesConfigService : ConfigServiceBase +{ + private static string FILE_PATH = "data/searches.yml"; + private static readonly TypedKey _changeKey = new("config.searches.updated"); + + public override string Name + => "searches"; + + public SearchesConfigService(IConfigSeria serializer, IPubSub pubSub) + : base(FILE_PATH, serializer, pubSub, _changeKey) + { + AddParsedProp("webEngine", + sc => sc.WebSearchEngine, + ConfigParsers.InsensitiveEnum, + ConfigPrinters.ToString); + + AddParsedProp("imgEngine", + sc => sc.ImgSearchEngine, + ConfigParsers.InsensitiveEnum, + ConfigPrinters.ToString); + + AddParsedProp("ytProvider", + sc => sc.YtProvider, + ConfigParsers.InsensitiveEnum, + ConfigPrinters.ToString); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Searches/_Common/Config/WebSearchEngine.cs b/src/NadekoBot/Modules/Searches/_Common/Config/WebSearchEngine.cs new file mode 100644 index 000000000..097210e0f --- /dev/null +++ b/src/NadekoBot/Modules/Searches/_Common/Config/WebSearchEngine.cs @@ -0,0 +1,7 @@ +namespace NadekoBot.Modules.Searches; + +public enum WebSearchEngine +{ + Google, + Searx, +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Utility/Info/InfoCommands.cs b/src/NadekoBot/Modules/Utility/Info/InfoCommands.cs index 25a9d3c2c..e1af89181 100644 --- a/src/NadekoBot/Modules/Utility/Info/InfoCommands.cs +++ b/src/NadekoBot/Modules/Utility/Info/InfoCommands.cs @@ -1,4 +1,5 @@ #nullable disable +using NadekoBot.Modules.Utility.Patronage; using System.Text; namespace NadekoBot.Modules.Utility; @@ -10,11 +11,13 @@ public partial class Utility { private readonly DiscordSocketClient _client; private readonly IStatsService _stats; + private readonly IPatronageService _ps; - public InfoCommands(DiscordSocketClient client, IStatsService stats) + public InfoCommands(DiscordSocketClient client, IStatsService stats, IPatronageService ps) { _client = client; _stats = stats; + _ps = ps; } [Cmd] @@ -106,6 +109,7 @@ public partial class Utility var embed = _eb.Create().AddField(GetText(strs.name), $"**{user.Username}**#{user.Discriminator}", true); if (!string.IsNullOrWhiteSpace(user.Nickname)) embed.AddField(GetText(strs.nickname), user.Nickname, true); + embed.AddField(GetText(strs.id), user.Id.ToString(), true) .AddField(GetText(strs.joined_server), $"{user.JoinedAt?.ToString("dd.MM.yyyy HH:mm") ?? "?"}", true) .AddField(GetText(strs.joined_discord), $"{user.CreatedAt:dd.MM.yyyy HH:mm}", true) @@ -114,9 +118,24 @@ public partial class Utility true) .WithOkColor(); + var patron = await _ps.GetPatronAsync(user.Id); + + if (patron.Tier != PatronTier.None) + { + embed.WithFooter(patron.Tier switch + { + PatronTier.V => "❤️❤️", + PatronTier.X => "❤️❤️❤️", + PatronTier.XX => "❤️❤️❤️❤️", + PatronTier.L => "❤️❤️❤️❤️❤️", + _ => "❤️", + }); + } + var av = user.RealAvatarUrl(); if (av.IsAbsoluteUri) embed.WithThumbnailUrl(av.ToString()); + await ctx.Channel.EmbedAsync(embed); } diff --git a/src/NadekoBot/Modules/Utility/Patreon/PatreonRewardsService.cs b/src/NadekoBot/Modules/Utility/Patreon/PatreonRewardsService.cs deleted file mode 100644 index c8372cf54..000000000 --- a/src/NadekoBot/Modules/Utility/Patreon/PatreonRewardsService.cs +++ /dev/null @@ -1,305 +0,0 @@ -#nullable disable -using LinqToDB.EntityFrameworkCore; -using NadekoBot.Common.ModuleBehaviors; -using NadekoBot.Modules.Gambling.Services; -using NadekoBot.Modules.Utility.Common.Patreon; -using NadekoBot.Services.Database.Models; -using StackExchange.Redis; -using System.Net; -using System.Net.Http.Json; -using System.Text.Json; - -namespace NadekoBot.Modules.Utility; - -public class PatreonRewardsService : INService, IReadyExecutor -{ - public TimeSpan Interval { get; } = TimeSpan.FromMinutes(3); - - public DateTime LastUpdate { get; private set; } = DateTime.UtcNow; - - private readonly SemaphoreSlim _claimLockJustInCase = new(1, 1); - private readonly DbService _db; - private readonly ICurrencyService _currency; - private readonly GamblingConfigService _gamblingConfigService; - private readonly ConnectionMultiplexer _redis; - private readonly IBotCredsProvider _credsProvider; - private readonly IHttpClientFactory _httpFactory; - private readonly IEmbedBuilderService _eb; - private readonly DiscordSocketClient _client; - - public PatreonRewardsService( - DbService db, - ICurrencyService currency, - IHttpClientFactory factory, - IEmbedBuilderService eb, - DiscordSocketClient client, - GamblingConfigService gamblingConfigService, - ConnectionMultiplexer redis, - IBotCredsProvider credsProvider) - { - _db = db; - _currency = currency; - _gamblingConfigService = gamblingConfigService; - _redis = redis; - _credsProvider = credsProvider; - _httpFactory = factory; - _eb = eb; - _client = client; - } - - public async Task OnReadyAsync() - { - if (_client.ShardId != 0) - return; - - using var t = new PeriodicTimer(Interval); - do - { - try - { - await RefreshPledges(_credsProvider.GetCreds()); - } - catch (Exception ex) - { - Log.Error(ex, "Unexpected error refreshing patreon pledges: {ErrorMessage}", ex.Message); - } - } while (await t.WaitForNextTickAsync()); - } - - private DateTime LastAccessTokenUpdate(IBotCredentials creds) - { - var db = _redis.GetDatabase(); - var val = db.StringGet($"{creds.RedisKey()}_patreon_update"); - - if (val == default) - return DateTime.MinValue; - - var lastTime = DateTime.FromBinary((long)val); - return lastTime; - } - - private async Task UpdateAccessToken(IBotCredentials creds) - { - Log.Information("Updating patreon access token..."); - try - { - using var http = _httpFactory.CreateClient(); - using var res = await http.PostAsync("https://www.patreon.com/api/oauth2/token" - + "?grant_type=refresh_token" - + $"&refresh_token={creds.Patreon.RefreshToken}" - + $"&client_id={creds.Patreon.ClientId}" - + $"&client_secret={creds.Patreon.ClientSecret}", - null); - - res.EnsureSuccessStatusCode(); - - var data = await res.Content.ReadFromJsonAsync(); - - if (data is null) - throw new("Invalid patreon response."); - - _credsProvider.ModifyCredsFile(oldData => - { - oldData.Patreon.AccessToken = data.AccessToken; - oldData.Patreon.RefreshToken = data.RefreshToken; - }); - - var db = _redis.GetDatabase(); - await db.StringSetAsync($"{creds.RedisKey()}_patreon_update", DateTime.UtcNow.ToBinary()); - return true; - } - catch (Exception ex) - { - Log.Error("Failed updating patreon access token: {ErrorMessage}", ex.ToString()); - return false; - } - } - - private bool HasPatreonCreds(IBotCredentials creds) - { - var cid = creds.Patreon.ClientId; - var cs = creds.Patreon.ClientSecret; - var rt = creds.Patreon.RefreshToken; - return !(string.IsNullOrWhiteSpace(cid) || string.IsNullOrWhiteSpace(cs) || string.IsNullOrWhiteSpace(rt)); - } - - public async Task RefreshPledges(IBotCredentials creds) - { - if (DateTime.UtcNow.Day < 5) - return; - - if (string.IsNullOrWhiteSpace(creds.Patreon.CampaignId)) - return; - - var lastUpdate = LastAccessTokenUpdate(creds); - var now = DateTime.UtcNow; - - if (lastUpdate.Year != now.Year - || lastUpdate.Month != now.Month - || string.IsNullOrWhiteSpace(creds.Patreon.AccessToken)) - { - // if the user has the necessary patreon creds - // and the access token expired or doesn't exist - // -> update access token - if (!HasPatreonCreds(creds)) - return; - - var success = await UpdateAccessToken(creds); - if (!success) - return; - } - - LastUpdate = DateTime.UtcNow; - try - { - var members = new List(); - var users = new List(); - using (var http = _httpFactory.CreateClient()) - { - http.DefaultRequestHeaders.Clear(); - http.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", - $"Bearer {creds.Patreon.AccessToken}"); - - var page = $"https://www.patreon.com/api/oauth2/v2/campaigns/{creds.Patreon.CampaignId}/members" - + "?fields%5Bmember%5D=full_name,currently_entitled_amount_cents" - + "&fields%5Buser%5D=social_connections" - + "&include=user"; - PatreonResponse data; - do - { - var res = await http.GetStringAsync(page); - data = JsonSerializer.Deserialize(res); - - if (data is null) - break; - - members.AddRange(data.Data); - users.AddRange(data.Included); - } while (!string.IsNullOrWhiteSpace(page = data.Links?.Next)); - } - - var userData = members.Join(users, - m => m.Relationships.User.Data.Id, - u => u.Id, - (m, u) => new - { - PatreonUserId = m.Relationships.User.Data.Id, - UserId = ulong.TryParse( - u.Attributes?.SocialConnections?.Discord?.UserId ?? string.Empty, - out var userId) - ? userId - : 0, - EntitledTo = m.Attributes.CurrentlyEntitledAmountCents - }) - .Where(x => x is - { - UserId: not 0, - EntitledTo: > 0 - }) - .ToList(); - - foreach (var pledge in userData) - await ClaimReward(pledge.UserId, pledge.PatreonUserId, pledge.EntitledTo); - } - catch (HttpRequestException ex) when (ex.StatusCode == HttpStatusCode.Unauthorized) - { - Log.Warning("Patreon credentials invalid or expired. I will try to refresh them during the next run"); - var db = _redis.GetDatabase(); - await db.KeyDeleteAsync($"{creds.RedisKey()}_patreon_update"); - } - catch (Exception ex) - { - Log.Warning(ex, "Error refreshing patreon pledges"); - } - } - - public async Task ClaimReward(ulong userId, string patreonUserId, int cents) - { - await _claimLockJustInCase.WaitAsync(); - var settings = _gamblingConfigService.Data; - var now = DateTime.UtcNow; - try - { - var eligibleFor = (int)(cents * settings.PatreonCurrencyPerCent); - - await using var uow = _db.GetDbContext(); - var users = uow.Set(); - var usr = await users.FirstOrDefaultAsyncEF(x => x.PatreonUserId == patreonUserId); - - if (usr is null) - { - users.Add(new() - { - PatreonUserId = patreonUserId, - LastReward = now, - AmountRewardedThisMonth = eligibleFor - }); - - await uow.SaveChangesAsync(); - - await _currency.AddAsync(userId, eligibleFor, new("patreon", "new")); - - Log.Information("Sending new currency reward to {UserId}", userId); - await SendMessageToUser(userId, - "Thank you for your pledge! " + $"You've been awarded **{eligibleFor}**{settings.Currency.Sign} !"); - return eligibleFor; - } - - if (usr.LastReward.Month != now.Month) - { - usr.LastReward = now; - usr.AmountRewardedThisMonth = eligibleFor; - - await uow.SaveChangesAsync(); - - await _currency.AddAsync(userId, eligibleFor, new("patreon", "recurring")); - - Log.Information("Sending recurring currency reward to {UserId}", userId); - await SendMessageToUser(userId, - "Thank you for your continued support! " - + $"You've been awarded **{eligibleFor}**{settings.Currency.Sign} for this month's support!"); - - return eligibleFor; - } - - if (usr.AmountRewardedThisMonth < eligibleFor) - { - var toAward = eligibleFor - usr.AmountRewardedThisMonth; - - usr.LastReward = now; - usr.AmountRewardedThisMonth = eligibleFor; - await uow.SaveChangesAsync(); - - await _currency.AddAsync(userId, toAward, new("patreon", "update")); - - Log.Information("Sending updated currency reward to {UserId}", userId); - await SendMessageToUser(userId, - "Thank you for increasing your pledge! " - + $"You've been awarded an additional **{toAward}**{settings.Currency.Sign} !"); - return toAward; - } - - return 0; - } - finally - { - _claimLockJustInCase.Release(); - } - } - - private async Task SendMessageToUser(ulong userId, string message) - { - try - { - var user = (IUser)_client.GetUser(userId) ?? await _client.Rest.GetUserAsync(userId); - if (user is null) - return; - - await user.SendConfirmAsync(_eb, message); - } - catch - { - // ignored - } - } -} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Utility/Patronage/Config/PatronConfigData.cs b/src/NadekoBot/Modules/Utility/Patronage/Config/PatronConfigData.cs new file mode 100644 index 000000000..bff64dda9 --- /dev/null +++ b/src/NadekoBot/Modules/Utility/Patronage/Config/PatronConfigData.cs @@ -0,0 +1,37 @@ +using NadekoBot.Common.Yml; +using Cloneable; + +namespace NadekoBot.Modules.Utility.Patronage; + +[Cloneable] +public partial class PatronConfigData : ICloneable +{ + [Comment("DO NOT CHANGE")] + public int Version { get; set; } = 1; + + [Comment("Whether the patronage feature is enabled")] + public bool IsEnabled { get; set; } + + [Comment("List of patron only features and relevant quota data")] + public FeatureQuotas Quotas { get; set; } + + public PatronConfigData() + { + Quotas = new(); + } + + public class FeatureQuotas + { + [Comment("Dictionary of feature names with their respective limits. Set to null for unlimited")] + public Dictionary> Features { get; set; } = new(); + + [Comment("Dictionary of commands with their respective quota data")] + public Dictionary?>> Commands { get; set; } = new(); + + [Comment("Dictionary of groups with their respective quota data")] + public Dictionary?>> Groups { get; set; } = new(); + + [Comment("Dictionary of modules with their respective quota data")] + public Dictionary?>> Modules { get; set; } = new(); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Utility/Patronage/Config/PatronTier.cs b/src/NadekoBot/Modules/Utility/Patronage/Config/PatronTier.cs new file mode 100644 index 000000000..d9eb32a7c --- /dev/null +++ b/src/NadekoBot/Modules/Utility/Patronage/Config/PatronTier.cs @@ -0,0 +1,14 @@ +// ReSharper disable InconsistentNaming +namespace NadekoBot.Modules.Utility.Patronage; + +public enum PatronTier +{ + None, + I, + V, + X, + XX, + L, + C, + ComingSoon +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Utility/Patronage/Config/PatronageConfig.cs b/src/NadekoBot/Modules/Utility/Patronage/Config/PatronageConfig.cs new file mode 100644 index 000000000..ffd9560f2 --- /dev/null +++ b/src/NadekoBot/Modules/Utility/Patronage/Config/PatronageConfig.cs @@ -0,0 +1,18 @@ +using NadekoBot.Common.Configs; + +namespace NadekoBot.Modules.Utility.Patronage; + +public class PatronageConfig : ConfigServiceBase +{ + public override string Name + => "patron"; + + private static readonly TypedKey _changeKey + = new TypedKey("config.patron.updated"); + + private const string FILE_PATH = "data/patron.yml"; + + public PatronageConfig(IConfigSeria serializer, IPubSub pubSub) : base(FILE_PATH, serializer, pubSub, _changeKey) + { + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Utility/Patronage/Config/QuotaPer.cs b/src/NadekoBot/Modules/Utility/Patronage/Config/QuotaPer.cs new file mode 100644 index 000000000..9c7db6c34 --- /dev/null +++ b/src/NadekoBot/Modules/Utility/Patronage/Config/QuotaPer.cs @@ -0,0 +1,8 @@ +namespace NadekoBot.Modules.Utility.Patronage; + +public enum QuotaPer +{ + PerHour, + PerDay, + PerMonth, +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Utility/Patronage/CurrencyRewardService.cs b/src/NadekoBot/Modules/Utility/Patronage/CurrencyRewardService.cs new file mode 100644 index 000000000..5ba470879 --- /dev/null +++ b/src/NadekoBot/Modules/Utility/Patronage/CurrencyRewardService.cs @@ -0,0 +1,190 @@ +#nullable disable +using LinqToDB; +using LinqToDB.EntityFrameworkCore; +using NadekoBot.Modules.Utility.Patronage; +using NadekoBot.Modules.Gambling.Bank; +using NadekoBot.Modules.Gambling.Services; +using NadekoBot.Services.Currency; +using NadekoBot.Services.Database.Models; + +namespace NadekoBot.Modules.Utility; + +public class CurrencyRewardService : INService, IDisposable +{ + private readonly ICurrencyService _cs; + private readonly IPatronageService _ps; + private readonly DbService _db; + private readonly IBankService _bs; + private readonly IEmbedBuilderService _eb; + private readonly GamblingConfigService _config; + private readonly DiscordSocketClient _client; + + public CurrencyRewardService( + ICurrencyService cs, + IPatronageService ps, + DbService db, + IBankService bs, + IEmbedBuilderService eb, + GamblingConfigService config, + DiscordSocketClient client) + { + _cs = cs; + _ps = ps; + _db = db; + _bs = bs; + _eb = eb; + _config = config; + _client = client; + + _ps.OnNewPatronPayment += OnNewPayment; + _ps.OnPatronRefunded += OnPatronRefund; + _ps.OnPatronUpdated += OnPatronUpdate; + } + + public void Dispose() + { + _ps.OnNewPatronPayment -= OnNewPayment; + _ps.OnPatronRefunded -= OnPatronRefund; + _ps.OnPatronUpdated -= OnPatronUpdate; + } + + private async Task OnPatronUpdate(Patron oldPatron, Patron newPatron) + { + if (oldPatron.Amount != newPatron.Amount) + { + var conf = _config.Data; + + var newAmount = (long)(Math.Max(newPatron.Amount, oldPatron.Amount) * conf.PatreonCurrencyPerCent); + UpdateOutput[] output; + await using (var ctx = _db.GetDbContext()) + { + output = await ctx.GetTable() + .Where(x => x.PlatformUserId == newPatron.UnqiuePlatformUserId) + .UpdateWithOutputAsync(old => new() + { + PlatformUserId = newPatron.UnqiuePlatformUserId, + UserId = newPatron.UserId, + // amount before bonuses + AmountRewardedThisMonth = newAmount, + LastReward = newPatron.PaidAt + }); + } + + // if the user wasn't previously in the db for some reason, + // we will treat him as a new patron + if (output.Length == 0) + { + await OnNewPayment(newPatron); + return; + } + + var oldAmount = output[0].Deleted.AmountRewardedThisMonth; + + var diff = newAmount - oldAmount; + if (diff <= 0) + return; // no action if new is lower + + // if the user pledges 5$ or more, they will get X % more flowers where X is amount in dollars, + // up to 100% + + var realAmount = GetRealCurrencyReward(newPatron.Amount, diff, out var percentBonus); + await _cs.AddAsync(newPatron.UserId, realAmount, new TxData("patron","update")); + + _ = SendMessageToUser(newPatron.UserId, + $"You've received an additional **{realAmount}**{_config.Data.Currency.Sign} as a currency reward (+{percentBonus}%)!"); + } + } + + private long GetRealCurrencyReward(int fullPledge, long currentAmount, out int percentBonus) + { + // needs at least 5$ to be eligible for a bonus + if (fullPledge < 500) + { + percentBonus = 0; + return currentAmount; + } + + var dollarValue = fullPledge / 100; + percentBonus = dollarValue switch + { + > 100 => 100, + _ => dollarValue + }; + return (long)(currentAmount * (1 + (percentBonus / 100.0f))); + } + + // on a new payment, always give the full amount. + private async Task OnNewPayment(Patron patron) + { + var amount = (long)(patron.Amount * _config.Data.PatreonCurrencyPerCent); + await using var ctx = _db.GetDbContext(); + await ctx.GetTable() + .InsertOrUpdateAsync(() => new() + { + PlatformUserId = patron.UnqiuePlatformUserId, + UserId = patron.UserId, + AmountRewardedThisMonth = amount, + LastReward = patron.PaidAt, + }, + old => new() + { + AmountRewardedThisMonth = amount, + UserId = patron.UserId, + LastReward = patron.PaidAt + }, + () => new() + { + PlatformUserId = patron.UnqiuePlatformUserId + }); + + var realAmount = GetRealCurrencyReward(patron.Amount, amount, out var percentBonus); + await _cs.AddAsync(patron.UserId, realAmount, new("patron", "new")); + _ = SendMessageToUser(patron.UserId, + $"You've received **{realAmount}**{_config.Data.Currency.Sign} as a currency reward (**+{percentBonus}%**)!"); + } + + private async Task SendMessageToUser(ulong userId, string message) + { + try + { + var user = (IUser)_client.GetUser(userId) ?? await _client.Rest.GetUserAsync(userId); + if (user is null) + return; + + var eb = _eb.Create() + .WithOkColor() + .WithDescription(message); + + await user.EmbedAsync(eb); + } + catch + { + Log.Warning("Unable to send a \"Currency Reward\" message to the patron {UserId}", userId); + } + } + + private async Task OnPatronRefund(Patron patron) + { + await using var ctx = _db.GetDbContext(); + _ = await ctx.GetTable() + .UpdateWithOutputAsync(old => new() + { + AmountRewardedThisMonth = old.AmountRewardedThisMonth * 2 + }); + + // var toTake = old.Length == 0 + // ? patron.Amount + // : old[0].Inserted.AmountRewardedThisMonth; + + // if (toTake > 0) + // { + // Log.Warning("Wiping the wallet and bank of the user {UserId} due to a refund/fraud...", + // patron.UserId); + // await _cs.RemoveAsync(patron.UserId, patron.Amount, new("patreon", "refund")); + // await _bs.BurnAllAsync(patron.UserId); + // Log.Warning("Burned {Amount} currency from the bank of the user {UserId} due to a refund/fraud.", + // patron.Amount, + // patron.UserId); + // } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Utility/Patronage/FeatureQuotaStats.cs b/src/NadekoBot/Modules/Utility/Patronage/FeatureQuotaStats.cs new file mode 100644 index 000000000..f3bbe4bfc --- /dev/null +++ b/src/NadekoBot/Modules/Utility/Patronage/FeatureQuotaStats.cs @@ -0,0 +1,8 @@ +namespace NadekoBot.Modules.Utility.Patronage; + +public readonly struct FeatureQuotaStats +{ + public (uint Cur, uint Max) Hourly { get; init; } + public (uint Cur, uint Max) Daily { get; init; } + public (uint Cur, uint Max) Monthly { get; init; } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Utility/Patronage/InsufficientTier.cs b/src/NadekoBot/Modules/Utility/Patronage/InsufficientTier.cs new file mode 100644 index 000000000..7d478b98d --- /dev/null +++ b/src/NadekoBot/Modules/Utility/Patronage/InsufficientTier.cs @@ -0,0 +1,11 @@ +using NadekoBot.Db.Models; + +namespace NadekoBot.Modules.Utility.Patronage; + +public readonly struct InsufficientTier +{ + public FeatureType FeatureType { get; init; } + public string Feature { get; init; } + public PatronTier RequiredTier { get; init; } + public PatronTier UserTier { get; init; } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Utility/Patronage/Patreon/PatreonClient.cs b/src/NadekoBot/Modules/Utility/Patronage/Patreon/PatreonClient.cs new file mode 100644 index 000000000..63d1184e3 --- /dev/null +++ b/src/NadekoBot/Modules/Utility/Patronage/Patreon/PatreonClient.cs @@ -0,0 +1,150 @@ +#nullable disable +using NadekoBot.Modules.Utility.Common.Patreon; +using OneOf; +using OneOf.Types; +using System.Net.Http.Json; +using System.Text.Json; + +namespace NadekoBot.Modules.Utility; + +public class PatreonClient : IDisposable +{ + private readonly string _clientId; + private readonly string _clientSecret; + private string refreshToken; + + + private string accessToken = string.Empty; + private readonly HttpClient _http; + + private DateTime refreshAt = DateTime.UtcNow; + + public PatreonClient(string clientId, string clientSecret, string refreshToken) + { + _clientId = clientId; + _clientSecret = clientSecret; + this.refreshToken = refreshToken; + + _http = new(); + } + + public void Dispose() + => _http.Dispose(); + + public PatreonCredentials GetCredentials() + => new PatreonCredentials() + { + AccessToken = accessToken, + ClientId = _clientId, + ClientSecret = _clientSecret, + RefreshToken = refreshToken, + }; + + public async Task>> RefreshTokenAsync(bool force) + { + if (!force && IsTokenValid()) + return new Success(); + + var res = await _http.PostAsync("https://www.patreon.com/api/oauth2/token" + + "?grant_type=refresh_token" + + $"&refresh_token={refreshToken}" + + $"&client_id={_clientId}" + + $"&client_secret={_clientSecret}", + null); + + if (!res.IsSuccessStatusCode) + return new Error($"Request did not return a sucess status code. Status code: {res.StatusCode}"); + + try + { + var data = await res.Content.ReadFromJsonAsync(); + + if (data is null) + return new Error($"Invalid data retrieved from Patreon."); + + refreshToken = data.RefreshToken; + accessToken = data.AccessToken; + + refreshAt = DateTime.UtcNow.AddSeconds(data.ExpiresIn - 5.Minutes().TotalSeconds); + return new Success(); + } + catch (Exception ex) + { + return new Error($"Error during deserialization: {ex.Message}"); + } + } + + private async ValueTask EnsureTokenValidAsync() + { + if (!IsTokenValid()) + { + var res = await RefreshTokenAsync(true); + return res.Match( + static _ => true, + static err => + { + Log.Warning("Error getting token: {ErrorMessage}", err.Value); + return false; + }); + } + + return true; + } + + private bool IsTokenValid() + => refreshAt > DateTime.UtcNow && !string.IsNullOrWhiteSpace(accessToken); + + public async Task>, Error>> GetMembersAsync(string campaignId) + { + if (!await EnsureTokenValidAsync()) + return new Error("Unable to get patreon token"); + + return OneOf>, Error>.FromT0( + GetMembersInternalAsync(campaignId)); + } + + private async IAsyncEnumerable> GetMembersInternalAsync(string campaignId) + { + _http.DefaultRequestHeaders.Clear(); + _http.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", + $"Bearer {accessToken}"); + + var page = + $"https://www.patreon.com/api/oauth2/v2/campaigns/{campaignId}/members" + + $"?fields%5Bmember%5D=full_name,currently_entitled_amount_cents,last_charge_date,last_charge_status" + + $"&fields%5Buser%5D=social_connections" + + $"&include=user" + + $"&sort=-last_charge_date"; + PatreonMembersResponse data; + + do + { + var res = await _http.GetStreamAsync(page); + data = await JsonSerializer.DeserializeAsync(res); + + if (data is null) + break; + + var userData = data.Data + .Join(data.Included, + static m => m.Relationships.User.Data.Id, + static u => u.Id, + static (m, u) => new PatreonMemberData() + { + PatreonUserId = m.Relationships.User.Data.Id, + UserId = ulong.TryParse( + u.Attributes?.SocialConnections?.Discord?.UserId ?? string.Empty, + out var userId) + ? userId + : 0, + EntitledToCents = m.Attributes.CurrentlyEntitledAmountCents, + LastChargeDate = m.Attributes.LastChargeDate, + LastChargeStatus = m.Attributes.LastChargeStatus + }) + .ToArray(); + + yield return userData; + + } while (!string.IsNullOrWhiteSpace(page = data.Links?.Next)); + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Utility/Patronage/Patreon/PatreonCredentials.cs b/src/NadekoBot/Modules/Utility/Patronage/Patreon/PatreonCredentials.cs new file mode 100644 index 000000000..67f619a30 --- /dev/null +++ b/src/NadekoBot/Modules/Utility/Patronage/Patreon/PatreonCredentials.cs @@ -0,0 +1,10 @@ +#nullable disable +namespace NadekoBot.Modules.Utility; + +public readonly struct PatreonCredentials +{ + public string ClientId { get; init; } + public string ClientSecret { get; init; } + public string AccessToken { get; init; } + public string RefreshToken { get; init; } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Utility/Patronage/Patreon/PatreonMemberData.cs b/src/NadekoBot/Modules/Utility/Patronage/Patreon/PatreonMemberData.cs new file mode 100644 index 000000000..f4c309348 --- /dev/null +++ b/src/NadekoBot/Modules/Utility/Patronage/Patreon/PatreonMemberData.cs @@ -0,0 +1,33 @@ +#nullable disable +namespace NadekoBot.Modules.Utility; + +public sealed class PatreonMemberData : ISubscriberData +{ + public string PatreonUserId { get; init; } + public ulong UserId { get; init; } + public DateTime? LastChargeDate { get; init; } + public string LastChargeStatus { get; init; } + public int EntitledToCents { get; init; } + + public string UniquePlatformUserId + => PatreonUserId; + ulong ISubscriberData.UserId + => UserId; + public int Cents + => EntitledToCents; + public DateTime? LastCharge + => LastChargeDate; + public SubscriptionChargeStatus ChargeStatus + => LastChargeStatus switch + { + "Paid" => SubscriptionChargeStatus.Paid, + "Fraud" or "Refunded" => SubscriptionChargeStatus.Refunded, + "Declined" or "Pending" => SubscriptionChargeStatus.Unpaid, + _ => SubscriptionChargeStatus.Other, + }; +} + +public sealed class PatreonPledgeData +{ + +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Utility/Patreon/PatreonRefreshData.cs b/src/NadekoBot/Modules/Utility/Patronage/Patreon/PatreonRefreshData.cs similarity index 100% rename from src/NadekoBot/Modules/Utility/Patreon/PatreonRefreshData.cs rename to src/NadekoBot/Modules/Utility/Patronage/Patreon/PatreonRefreshData.cs diff --git a/src/NadekoBot/Modules/Utility/Patronage/Patreon/PatreonSubscriptionHandler.cs b/src/NadekoBot/Modules/Utility/Patronage/Patreon/PatreonSubscriptionHandler.cs new file mode 100644 index 000000000..4c17e2ae9 --- /dev/null +++ b/src/NadekoBot/Modules/Utility/Patronage/Patreon/PatreonSubscriptionHandler.cs @@ -0,0 +1,81 @@ +#nullable disable +using NadekoBot.Modules.Utility.Patronage; + +namespace NadekoBot.Modules.Utility; + +/// +/// Service tasked with handling pledges on patreon +/// +public sealed class PatreonSubscriptionHandler : ISubscriptionHandler, INService +{ + private readonly IBotCredsProvider _credsProvider; + private readonly PatreonClient _patreonClient; + + public PatreonSubscriptionHandler(IBotCredsProvider credsProvider) + { + _credsProvider = credsProvider; + var botCreds = credsProvider.GetCreds(); + _patreonClient = new PatreonClient(botCreds.Patreon.ClientId, botCreds.Patreon.ClientSecret, botCreds.Patreon.RefreshToken); + } + + public async IAsyncEnumerable> GetPatronsAsync() + { + var botCreds = _credsProvider.GetCreds(); + + if (string.IsNullOrWhiteSpace(botCreds.Patreon.CampaignId) + || string.IsNullOrWhiteSpace(botCreds.Patreon.ClientId) + || string.IsNullOrWhiteSpace(botCreds.Patreon.ClientSecret) + || string.IsNullOrWhiteSpace(botCreds.Patreon.RefreshToken)) + yield break; + + var result = await _patreonClient.RefreshTokenAsync(false); + if (!result.TryPickT0(out _, out var error)) + { + Log.Warning("Unable to refresh patreon token: {ErrorMessage}", error.Value); + yield break; + } + + var patreonCreds = _patreonClient.GetCredentials(); + + _credsProvider.ModifyCredsFile(c => + { + c.Patreon.AccessToken = patreonCreds.AccessToken; + c.Patreon.RefreshToken = patreonCreds.RefreshToken; + }); + + IAsyncEnumerable> data; + try + { + var maybeUserData = await _patreonClient.GetMembersAsync(botCreds.Patreon.CampaignId); + data = maybeUserData.Match( + static userData => userData, + static err => + { + Log.Warning("Error while getting patreon members: {ErrorMessage}", err.Value); + return AsyncEnumerable.Empty>(); + }); + } + catch (Exception ex) + { + Log.Warning(ex, + "Unexpected error while refreshing patreon members: {ErroMessage}", + ex.Message); + + yield break; + } + + var now = DateTime.UtcNow; + var firstOfThisMonth = new DateOnly(now.Year, now.Month, 1); + await foreach (var batch in data) + { + // send only active patrons + var toReturn = batch.Where(x => x.Cents > 0 + && x.LastCharge is { } lc + && lc.ToUniversalTime().ToDateOnly() >= firstOfThisMonth) + .ToArray(); + + if (toReturn.Length > 0) + yield return toReturn; + } + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Utility/Patronage/Patron.cs b/src/NadekoBot/Modules/Utility/Patronage/Patron.cs new file mode 100644 index 000000000..ae5a1d888 --- /dev/null +++ b/src/NadekoBot/Modules/Utility/Patronage/Patron.cs @@ -0,0 +1,38 @@ +namespace NadekoBot.Modules.Utility.Patronage; + +public readonly struct Patron +{ + /// + /// Unique id assigned to this patron by the payment platform + /// + public string UnqiuePlatformUserId { get; init; } + + /// + /// Discord UserId to which this is connected to + /// + public ulong UserId { get; init; } + + /// + /// Amount the Patron is currently pledging or paid + /// + public int Amount { get; init; } + + /// + /// Current Tier of the patron + /// (do not question it in consumer classes, as the calculation should be always internal and may change) + /// + public PatronTier Tier { get; init; } + + /// + /// When was the last time this was paid + /// + public DateTime PaidAt { get; init; } + + /// + /// After which date does the user's Patronage benefit end + /// + public DateTime ValidThru { get; init; } + + public bool IsActive + => !ValidThru.IsBeforeToday(); +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Utility/Patronage/PatronExtensions.cs b/src/NadekoBot/Modules/Utility/Patronage/PatronExtensions.cs new file mode 100644 index 000000000..afd8a0d0f --- /dev/null +++ b/src/NadekoBot/Modules/Utility/Patronage/PatronExtensions.cs @@ -0,0 +1,39 @@ +namespace NadekoBot.Modules.Utility.Patronage; + +public static class PatronExtensions +{ + public static DateOnly ToDateOnly(this DateTime dateTime) + => DateOnly.FromDateTime(dateTime); + + public static bool IsBeforeToday(this DateTime date) + => date < DateTime.UtcNow.Date; + + public static string ToFullName(this PatronTier tier) + => tier switch + { + _ => $"Patron Tier {tier}", + }; + + public static string ToFullName(this QuotaPer per) + => per.Humanize(LetterCasing.LowerCase); + + public static DateTime DayOfNextMonth(this DateTime date, int day) + { + var nextMonth = date.AddMonths(1); + var dt = DateTime.SpecifyKind(new(nextMonth.Year, nextMonth.Month, day), DateTimeKind.Utc); + return dt; + } + + public static DateTime FirstOfNextMonth(this DateTime date) + => date.DayOfNextMonth(1); + + public static DateTime SecondOfNextMonth(this DateTime date) + => date.DayOfNextMonth(2); + + public static string ToShortAndRelativeTimestampTag(this DateTime date) + { + var fullResetStr = TimestampTag.FromDateTime(date, TimestampTagStyles.ShortDateTime); + var relativeResetStr = TimestampTag.FromDateTime(date, TimestampTagStyles.Relative); + return $"{fullResetStr}\n{relativeResetStr}"; + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Utility/Patronage/PatronageCommands.cs b/src/NadekoBot/Modules/Utility/Patronage/PatronageCommands.cs new file mode 100644 index 000000000..c7f5585dd --- /dev/null +++ b/src/NadekoBot/Modules/Utility/Patronage/PatronageCommands.cs @@ -0,0 +1,153 @@ +using NadekoBot.Modules.Utility.Patronage; + +namespace NadekoBot.Modules.Utility; + +public partial class Utility +{ + [OnlyPublicBot] + public partial class PatronageCommands : NadekoModule + { + private readonly PatronageService _service; + private readonly PatronageConfig _pConf; + + public PatronageCommands(PatronageService service, PatronageConfig pConf) + { + _service = service; + _pConf = pConf; + } + + [Cmd] + [Priority(2)] + public partial Task Patron() + => InternalPatron(ctx.User); + + [Cmd] + [Priority(0)] + [OwnerOnly] + public partial Task Patron(IUser user) + => InternalPatron(user); + + [Cmd] + [Priority(0)] + [OwnerOnly] + public async partial Task PatronMessage(PatronTier tierAndHigher, string message) + { + _ = ctx.Channel.TriggerTypingAsync(); + var result = await _service.SendMessageToPatronsAsync(tierAndHigher, message); + + await ReplyConfirmLocalizedAsync(strs.patron_msg_sent( + Format.Code(tierAndHigher.ToString()), + Format.Bold(result.Success.ToString()), + Format.Bold(result.Failed.ToString()))); + } + + // [Cmd] + // [OwnerOnly] + // public async partial Task PatronGift(IUser user, int amount) + // { + // // i can't figure out a good way to gift more than one month at the moment. + // + // if (amount < 1) + // return; + // + // var patron = _service.GiftPatronAsync(user, amount); + // + // var eb = _eb.Create(ctx); + // + // await ctx.Channel.EmbedAsync(eb.WithDescription($"Added **{days}** days of Patron benefits to {user.Mention}!") + // .AddField("Tier", Format.Bold(patron.Tier.ToString()), true) + // .AddField("Amount", $"**{patron.Amount / 100.0f:N1}$**", true) + // .AddField("Until", TimestampTag.FromDateTime(patron.ValidThru.AddDays(1)))); + // + // + // } + + private async Task InternalPatron(IUser user) + { + if (!_pConf.Data.IsEnabled) + { + await ReplyErrorLocalizedAsync(strs.patron_not_enabled); + return; + } + + var patron = await _service.GetPatronAsync(user.Id); + var quotaStats = await _service.GetUserQuotaStatistic(user.Id); + + var eb = _eb.Create(ctx) + .WithAuthor(user) + .WithTitle(GetText(strs.patron_info)) + .WithOkColor(); + + if (quotaStats.Commands.Count == 0 + && quotaStats.Groups.Count == 0 + && quotaStats.Modules.Count == 0) + { + eb.WithDescription(GetText(strs.no_quota_found)); + } + else + { + eb.AddField(GetText(strs.tier), Format.Bold(patron.Tier.ToFullName()), true) + .AddField(GetText(strs.pledge), $"**{patron.Amount / 100.0f:N1}$**", true); + + if (patron.Tier != PatronTier.None) + eb.AddField(GetText(strs.expires), patron.ValidThru.AddDays(1).ToShortAndRelativeTimestampTag(), true); + + eb.AddField(GetText(strs.quotas), "⁣", false); + + if (quotaStats.Commands.Count > 0) + { + var text = GetQuotaList(quotaStats.Commands); + if (!string.IsNullOrWhiteSpace(text)) + eb.AddField(GetText(strs.commands), text, true); + } + + if (quotaStats.Groups.Count > 0) + { + var text = GetQuotaList(quotaStats.Groups); + if (!string.IsNullOrWhiteSpace(text)) + eb.AddField(GetText(strs.groups), text, true); + } + + if (quotaStats.Modules.Count > 0) + { + var text = GetQuotaList(quotaStats.Modules); + if (!string.IsNullOrWhiteSpace(text)) + eb.AddField(GetText(strs.modules), text, true); + } + } + + + try + { + await ctx.User.EmbedAsync(eb); + _ = ctx.OkAsync(); + } + catch + { + await ReplyErrorLocalizedAsync(strs.cant_dm); + } + } + + private string GetQuotaList(IReadOnlyDictionary featureQuotaStats) + { + var text = string.Empty; + foreach (var (key, q) in featureQuotaStats) + { + text += $"\n⁣\t`{key}`\n"; + if (q.Hourly != default) + text += $"⁣ ⁣ {GetEmoji(q.Hourly)} {q.Hourly.Cur}/{q.Hourly.Max} per hour\n"; + if (q.Daily != default) + text += $"⁣ ⁣ {GetEmoji(q.Daily)} {q.Daily.Cur}/{q.Daily.Max} per day\n"; + if (q.Monthly != default) + text += $"⁣ ⁣ {GetEmoji(q.Monthly)} {q.Monthly.Cur}/{q.Monthly.Max} per month\n"; + } + + return text; + } + + private string GetEmoji((uint Cur, uint Max) limit) + => limit.Cur < limit.Max + ? "✅" + : "⚠️"; + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Utility/Patronage/PatronageService.cs b/src/NadekoBot/Modules/Utility/Patronage/PatronageService.cs new file mode 100644 index 000000000..bf5e79463 --- /dev/null +++ b/src/NadekoBot/Modules/Utility/Patronage/PatronageService.cs @@ -0,0 +1,834 @@ +using LinqToDB; +using LinqToDB.EntityFrameworkCore; +using NadekoBot.Common.ModuleBehaviors; +using NadekoBot.Db.Models; +using OneOf; +using StackExchange.Redis; +using CommandInfo = Discord.Commands.CommandInfo; + +namespace NadekoBot.Modules.Utility.Patronage; + +/// +public sealed class PatronageService + : IPatronageService, + IReadyExecutor, + IExecPreCommand, + INService +{ + public event Func OnNewPatronPayment = static delegate { return Task.CompletedTask; }; + public event Func OnPatronUpdated = static delegate { return Task.CompletedTask; }; + public event Func OnPatronRefunded = static delegate { return Task.CompletedTask; }; + + // this has to run right before the command + public int Priority + => int.MinValue; + + private static readonly PatronTier[] _tiers = Enum.GetValues(); + + private readonly PatronageConfig _pConf; + private readonly DbService _db; + private readonly DiscordSocketClient _client; + private readonly ISubscriptionHandler _subsHandler; + private readonly IEmbedBuilderService _eb; + private readonly ConnectionMultiplexer _redis; + private readonly IBotCredentials _creds; + private readonly TypedKey _quotaKey; + + public PatronageService( + PatronageConfig pConf, + DbService db, + DiscordSocketClient client, + ISubscriptionHandler subsHandler, + IEmbedBuilderService eb, + ConnectionMultiplexer redis, + IBotCredentials creds) + { + _pConf = pConf; + _db = db; + _client = client; + _subsHandler = subsHandler; + _eb = eb; + _redis = redis; + _creds = creds; + + _quotaKey = new TypedKey($"{_creds.RedisKey()}:quota:last_hourly_reset"); + } + + public Task OnReadyAsync() + { + if (_client.ShardId != 0) + return Task.CompletedTask; + + return Task.WhenAll(ResetLoopAsync(), LoadSubscribersLoopAsync()); + } + + private async Task LoadSubscribersLoopAsync() + { + var timer = new PeriodicTimer(TimeSpan.FromSeconds(60)); + while (await timer.WaitForNextTickAsync()) + { + try + { + if (!_pConf.Data.IsEnabled) + continue; + + await foreach (var batch in _subsHandler.GetPatronsAsync()) + { + await ProcesssPatronsAsync(batch); + } + } + catch (Exception ex) + { + Log.Error(ex, "Error processing patrons"); + } + } + } + + public async Task ResetLoopAsync() + { + await Task.Delay(1.Minutes()); + while (true) + { + try + { + if (!_pConf.Data.IsEnabled) + { + await Task.Delay(1.Minutes()); + continue; + } + + var now = DateTime.UtcNow; + var lastRun = DateTime.MinValue; + + var rdb = _redis.GetDatabase(); + var lastVal = await rdb.StringGetAsync(_quotaKey.Key); + if (lastVal != default) + { + lastRun = DateTime.FromBinary((long)lastVal); + } + + var nowDate = now.ToDateOnly(); + var lastDate = lastRun.ToDateOnly(); + + await using var ctx = _db.GetDbContext(); + await using var tran = await ctx.Database.BeginTransactionAsync(); + + if ((lastDate.Day == 1 || (lastDate.Month != nowDate.Month)) && nowDate.Day > 1) + { + // assumes bot won't be offline for a year + await ctx.GetTable() + .TruncateAsync(); + } + else if (nowDate.DayNumber != lastDate.DayNumber) + { + // day is different, means hour is different. + // reset both hourly and daily quota counts. + await ctx.GetTable() + .UpdateAsync((old) => new() + { + HourlyCount = 0, + DailyCount = 0, + }); + + await rdb.StringSetAsync(_quotaKey.Key, true); + } + else if (now.Hour != lastRun.Hour) // if it's not, just reset hourly quotas + { + await ctx.GetTable() + .UpdateAsync((old) => new() + { + HourlyCount = 0 + }); + } + + // assumes that the code above runs in less than an hour + await rdb.StringSetAsync(_quotaKey.Key, now.ToBinary()); + await tran.CommitAsync(); + } + catch (Exception ex) + { + Log.Error(ex, "Error in quota reset loop. Message: {ErrorMessage}", ex.Message); + } + + await Task.Delay(TimeSpan.FromHours(1).Add(TimeSpan.FromMinutes(1))); + } + } + + private async Task ProcesssPatronsAsync(IReadOnlyCollection subscribersEnum) + { + // process only users who have discord accounts connected + var subscribers = subscribersEnum.Where(x => x.UserId != 0).ToArray(); + + if (subscribers.Length == 0) + return; + + var todayDate = DateTime.UtcNow.Date; + await using var ctx = _db.GetDbContext(); + + // handle paid users + foreach (var subscriber in subscribers.Where(x => x.ChargeStatus == SubscriptionChargeStatus.Paid)) + { + if (subscriber.LastCharge is null) + continue; + + var lastChargeUtc = subscriber.LastCharge.Value.ToUniversalTime(); + var dateInOneMonth = lastChargeUtc.Date.AddMonths(1); + await using var tran = await ctx.Database.BeginTransactionAsync(); + try + { + var dbPatron = await ctx.GetTable() + .FirstOrDefaultAsync(x + => x.UniquePlatformUserId == subscriber.UniquePlatformUserId); + + if (dbPatron is null) + { + // if the user is not in the database alrady + dbPatron = await ctx.GetTable() + .InsertWithOutputAsync(() => new() + { + UniquePlatformUserId = subscriber.UniquePlatformUserId, + UserId = subscriber.UserId, + AmountCents = subscriber.Cents, + LastCharge = lastChargeUtc, + ValidThru = dateInOneMonth, + }); + + await tran.CommitAsync(); + + var newPatron = PatronUserToPatron(dbPatron); + _ = SendWelcomeMessage(newPatron); + await OnNewPatronPayment(newPatron); + } + else + { + if (dbPatron.LastCharge.Month < lastChargeUtc.Month) + { + // user is charged again for this month + // if his sub would end in teh future, extend it by one month. + // if it's not, just add 1 month to the last charge date + var count = await ctx.GetTable() + .Where(x => x.UniquePlatformUserId == subscriber.UniquePlatformUserId) + .UpdateAsync(old => new() + { + UserId = subscriber.UserId, + AmountCents = subscriber.Cents, + LastCharge = lastChargeUtc, + ValidThru = old.ValidThru >= todayDate + // ? Sql.DateAdd(Sql.DateParts.Month, 1, old.ValidThru).Value + ? old.ValidThru.AddMonths(1) + : dateInOneMonth, + }); + + // this should never happen + if (count == 0) + { + await tran.RollbackAsync(); + continue; + } + + await tran.CommitAsync(); + + await OnNewPatronPayment(PatronUserToPatron(dbPatron)); + } + else if (dbPatron.AmountCents != subscriber.Cents // if user changed the amount + || dbPatron.UserId != subscriber.UserId) // if user updated user id) + { + // the user updated the pledge or changed the connected discord account + var newData = await ctx.GetTable() + .Where(x => x.UniquePlatformUserId == subscriber.UniquePlatformUserId + && x.LastCharge < lastChargeUtc) + .UpdateWithOutputAsync(old => new() + { + UserId = subscriber.UserId, + AmountCents = subscriber.Cents, + LastCharge = lastChargeUtc, + ValidThru = old.ValidThru, + }); + await tran.CommitAsync(); + + // this should never happen + if (newData.Length == 0) + continue; + + await OnPatronUpdated(PatronUserToPatron(dbPatron), PatronUserToPatron(newData[0].Inserted)); + } + } + } + catch (Exception ex) + { + Log.Error(ex, + "Unexpected error occured while processing rewards for patron {UserId}", + subscriber.UserId); + } + } + + foreach (var patron in subscribers.Where(x => x.ChargeStatus == SubscriptionChargeStatus.Refunded)) + { + var expiredDate = DateTime.MinValue; + // if the subscription is refunded, Disable user's valid thru + var output = await ctx.GetTable() + .Where(x => x.UniquePlatformUserId == patron.UniquePlatformUserId + && x.ValidThru != expiredDate) + .UpdateWithOutputAsync(old => new() + { + ValidThru = expiredDate + }); + + if (output.Length == 0) + continue; + + await OnPatronRefunded(PatronUserToPatron(output[0].Inserted)); + } + } + + public async Task ExecPreCommandAsync( + ICommandContext ctx, + string moduleName, + CommandInfo command) + { + var ownerId = ctx.Guild?.OwnerId ?? 0; + + var result = await AttemptRunCommand( + ctx.User.Id, + ownerId: ownerId, + command.Aliases.First().ToLowerInvariant(), + command.Module.Parent == null ? string.Empty : command.Module.GetGroupName().ToLowerInvariant(), + moduleName.ToLowerInvariant() + ); + + return result.Match( + _ => false, + ins => + { + var eb = _eb.Create(ctx) + .WithPendingColor() + .WithTitle("Insufficient Patron Tier") + .AddField("For", $"{ins.FeatureType}: `{ins.Feature}`", true) + .AddField("Required Tier", + $"[{ins.RequiredTier.ToFullName()}](https://patreon.com/join/nadekobot)", + true); + + if (ctx.Guild is null || ctx.Guild?.OwnerId == ctx.User.Id) + eb.WithDescription("You don't have the sufficent Patron Tier to run this command.") + .WithFooter("You can use '.patron' and '.donate' commands for more info"); + else + eb.WithDescription( + "Neither you nor the server owner have the sufficent Patron Tier to run this command.") + .WithFooter("You can use '.patron' and '.donate' commands for more info"); + + _ = ctx.WarningAsync(); + + if (ctx.Guild?.OwnerId == ctx.User.Id) + _ = ctx.Channel.EmbedAsync(eb); + else + _ = ctx.User.EmbedAsync(eb); + + return true; + }, + quota => + { + var eb = _eb.Create(ctx) + .WithPendingColor() + .WithTitle("Quota Limit Reached"); + + if (quota.IsOwnQuota || ctx.User.Id == ownerId) + { + eb.WithDescription($"You've reached your quota of `{quota.Quota} {quota.QuotaPeriod.ToFullName()}`") + .WithFooter("You may want to check your quota by using the '.patron' command."); + } + else + { + eb.WithDescription( + $"This server reached the quota of {quota.Quota} `{quota.QuotaPeriod.ToFullName()}`") + .WithFooter("You may contact the server owner about this issue.\n" + + "Alternatively, you can become patron yourself by using the '.donate' command.\n" + + "If you're already a patron, it means you've reached your quota.\n" + + "You can use '.patron' command to check your quota status."); + } + + eb.AddField("For", $"{quota.FeatureType}: `{quota.Feature}`", true) + .AddField("Resets At", quota.ResetsAt.ToShortAndRelativeTimestampTag(), true); + + _ = ctx.WarningAsync(); + + // send the message in the server in case it's the owner + if (ctx.Guild?.OwnerId == ctx.User.Id) + _ = ctx.Channel.EmbedAsync(eb); + else + _ = ctx.User.EmbedAsync(eb); + + return true; + }); + } + + private async ValueTask> AttemptRunCommand( + ulong userId, + ulong ownerId, + string commandName, + string groupName, + string moduleName) + { + // try to run as a user + var res = await AttemptRunCommand(userId, commandName, groupName, moduleName, true); + + // if it fails, try to run as an owner + // but only if the command is ran in a server + // and if the owner is not the user + if (!res.IsT0 && ownerId != 0 && ownerId != userId) + res = await AttemptRunCommand(ownerId, commandName, groupName, moduleName, false); + + return res; + } + + /// + /// Returns either the current usage counter if limit wasn't reached, or QuotaLimit if it is. + /// + public async ValueTask> TryIncrementQuotaCounterAsync(ulong userId, + bool isSelf, + FeatureType featureType, + string featureName, + uint? maybeHourly, + uint? maybeDaily, + uint? maybeMonthly) + { + await using var ctx = _db.GetDbContext(); + + var now = DateTime.UtcNow; + await using var tran = await ctx.Database.BeginTransactionAsync(); + + var userQuotaData = await ctx.GetTable() + .FirstOrDefaultAsyncLinqToDB(x => x.UserId == userId + && x.Feature == featureName) + ?? new PatronQuota(); + + // if hourly exists, if daily exists, etc... + if (maybeHourly is uint hourly && userQuotaData.HourlyCount >= hourly) + { + return new QuotaLimit() + { + QuotaPeriod = QuotaPer.PerHour, + Quota = hourly, + // quite a neat trick. https://stackoverflow.com/a/5733560 + ResetsAt = now.Date.AddHours(now.Hour + 1), + Feature = featureName, + FeatureType = featureType, + IsOwnQuota = isSelf + }; + } + + if (maybeDaily is uint daily + && userQuotaData.DailyCount >= daily) + { + return new QuotaLimit() + { + QuotaPeriod = QuotaPer.PerDay, + Quota = daily, + ResetsAt = now.Date.AddDays(1), + Feature = featureName, + FeatureType = featureType, + IsOwnQuota = isSelf + }; + } + + if (maybeMonthly is uint monthly && userQuotaData.MonthlyCount >= monthly) + { + return new QuotaLimit() + { + QuotaPeriod = QuotaPer.PerMonth, + Quota = monthly, + ResetsAt = now.Date.SecondOfNextMonth(), + Feature = featureName, + FeatureType = featureType, + IsOwnQuota = isSelf + }; + } + + await ctx.GetTable() + .InsertOrUpdateAsync(() => new() + { + UserId = userId, + FeatureType = featureType, + Feature = featureName, + DailyCount = 1, + MonthlyCount = 1, + HourlyCount = 1, + }, + (old) => new() + { + HourlyCount = old.HourlyCount + 1, + DailyCount = old.DailyCount + 1, + MonthlyCount = old.MonthlyCount + 1, + }, + () => new() + { + UserId = userId, + FeatureType = featureType, + Feature = featureName, + }); + + await tran.CommitAsync(); + + return (userQuotaData.HourlyCount + 1, userQuotaData.DailyCount + 1, userQuotaData.MonthlyCount + 1); + } + + /// + /// Attempts to add 1 to user's quota for the command, group and module. + /// Input MUST BE lowercase + /// + /// Id of the user who is attempting to run the command + /// Name of the command the user is trying to run + /// Name of the command's group + /// Name of the command's top level module + /// Whether this is check is for the user himself. False if it's someone else's id (owner) + /// Either a succcess (user can run the command) or one of the error values. + private async ValueTask> AttemptRunCommand( + ulong userId, + string commandName, + string groupName, + string moduleName, + bool isSelf) + { + var confData = _pConf.Data; + + if (!confData.IsEnabled) + return default; + + if (_creds.IsOwner(userId)) + return default; + + // get user tier + var patron = await GetPatronAsync(userId); + FeatureType quotaForFeatureType; + + if (confData.Quotas.Commands.TryGetValue(commandName, out var quotaData)) + { + quotaForFeatureType = FeatureType.Command; + } + else if (confData.Quotas.Groups.TryGetValue(groupName, out quotaData)) + { + quotaForFeatureType = FeatureType.Group; + } + else if (confData.Quotas.Modules.TryGetValue(moduleName, out quotaData)) + { + quotaForFeatureType = FeatureType.Module; + } + else + { + return default; + } + + var featureName = quotaForFeatureType switch + { + FeatureType.Command => commandName, + FeatureType.Group => groupName, + FeatureType.Module => moduleName, + _ => throw new ArgumentOutOfRangeException(nameof(quotaForFeatureType)) + }; + + if (!TryGetTierDataOrLower(quotaData, patron.Tier, out var data)) + { + return new InsufficientTier() + { + Feature = featureName, + FeatureType = quotaForFeatureType, + RequiredTier = quotaData.Count == 0 + ? PatronTier.ComingSoon + : quotaData.Keys.First(), + UserTier = patron.Tier, + }; + } + + // no quota limits for this tier + if (data is null) + return default; + + var quotaCheckResult = await TryIncrementQuotaCounterAsync(userId, + isSelf, + quotaForFeatureType, + featureName, + data.TryGetValue(QuotaPer.PerHour, out var hourly) ? hourly : null, + data.TryGetValue(QuotaPer.PerDay, out var daily) ? daily : null, + data.TryGetValue(QuotaPer.PerMonth, out var monthly) ? monthly : null + ); + + return quotaCheckResult.Match(_ => default, x => x); + } + + private bool TryGetTierDataOrLower( + IReadOnlyDictionary data, + PatronTier tier, + out T? o) + { + // check for quotas on this tier + if (data.TryGetValue(tier, out o)) + return true; + + // if there are none, get the quota first tier below this one + // which has quotas specified + for (var i = _tiers.Length - 1; i >= 0; i--) + { + var lowerTier = _tiers[i]; + if (lowerTier < tier && data.TryGetValue(lowerTier, out o)) + return true; + } + + // if there are none, that means the feature is intended + // to be patron-only but the quotas haven't been specified yet + // so it will be marked as "Coming Soon" + o = default; + return false; + } + + public async Task GetPatronAsync(ulong userId) + { + await using var ctx = _db.GetDbContext(); + + // this can potentially return multiple users if the user + // is subscribed on multiple platforms + // or if there are multiple users on the same platform who connected the same discord account?! + var users = await ctx.GetTable() + .Where(x => x.UserId == userId) + .ToListAsync(); + + // first find all active subscriptions + // and return the one with the highest amount + var maxActive = users.Where(x => !x.ValidThru.IsBeforeToday()).MaxBy(x => x.AmountCents); + if (maxActive is not null) + return PatronUserToPatron(maxActive); + + // if there are no active subs, return the one with the highest amount + + var max = users.MaxBy(x => x.AmountCents); + if (max is null) + return default; // no patron with that name + + return PatronUserToPatron(max); + } + + public async Task GetUserQuotaStatistic(ulong userId) + { + var pConfData = _pConf.Data; + + if (!pConfData.IsEnabled) + return new(); + + var patron = await GetPatronAsync(userId); + + await using var ctx = _db.GetDbContext(); + var allPatronQuotas = await ctx.GetTable() + .Where(x => x.UserId == userId) + .ToListAsync(); + + var allQuotasDict = allPatronQuotas + .GroupBy(static x => x.FeatureType) + .ToDictionary(static x => x.Key, static x => x.ToDictionary(static y => y.Feature)); + + allQuotasDict.TryGetValue(FeatureType.Command, out var data); + var userCommandQuotaStats = GetFeatureQuotaStats(patron.Tier, data, pConfData.Quotas.Commands); + + allQuotasDict.TryGetValue(FeatureType.Group, out data); + var userGroupQuotaStats = GetFeatureQuotaStats(patron.Tier, data, pConfData.Quotas.Groups); + + allQuotasDict.TryGetValue(FeatureType.Module, out data); + var userModuleQuotaStats = GetFeatureQuotaStats(patron.Tier, data, pConfData.Quotas.Modules); + + return new UserQuotaStats() + { + Tier = patron.Tier, + Commands = userCommandQuotaStats, + Groups = userGroupQuotaStats, + Modules = userModuleQuotaStats, + }; + } + + // should i allow users to pay extra for more quota? + private IReadOnlyDictionary GetFeatureQuotaStats( + PatronTier patronTier, + IReadOnlyDictionary? allQuotasDict, + Dictionary?>> commands) + { + var userCommandQuotaStats = new Dictionary(); + foreach (var (key, quotaData) in commands) + { + if (TryGetTierDataOrLower(quotaData, patronTier, out var data)) + { + // if data is null that means the quota for the user's tier is unlimited + // no point in returning it? + + if (data is null) + continue; + + var (daily, hourly, monthly) = default((uint, uint, uint)); + // try to get users stats for this feature + // if it fails just leave them at 0 + if (allQuotasDict?.TryGetValue(key, out var quota) ?? false) + (daily, hourly, monthly) = (quota.DailyCount, quota.HourlyCount, quota.MonthlyCount); + + userCommandQuotaStats[key] = new FeatureQuotaStats() + { + Hourly = data.TryGetValue(QuotaPer.PerHour, out var hourD) + ? (hourly, hourD) + : default, + Daily = data.TryGetValue(QuotaPer.PerDay, out var maxD) + ? (daily, maxD) + : default, + Monthly = data.TryGetValue(QuotaPer.PerMonth, out var maxM) + ? (monthly, maxM) + : default, + }; + } + } + + return userCommandQuotaStats; + } + + public async Task TryGetFeatureLimitAsync(FeatureLimitKey key, ulong userId, int? defaultValue) + { + var conf = _pConf.Data; + + // if patron system is disabled, the quota is just default + if (!conf.IsEnabled) + return new() + { + Name = key.PrettyName, + Quota = default, + IsPatronLimit = false + }; + + + if (!conf.Quotas.Features.TryGetValue(key.Key, out var data)) + return new() + { + Name = key.PrettyName, + Quota = defaultValue, + IsPatronLimit = false, + }; + + var patron = await GetPatronAsync(userId); + if (!TryGetTierDataOrLower(data, patron.Tier, out var limit)) + return new() + { + Name = key.PrettyName, + Quota = defaultValue, + IsPatronLimit = false, + }; + + return new() + { + Name = key.PrettyName, + Quota = limit, + IsPatronLimit = true + }; + } + + // public async Task GiftPatronAsync(IUser user, int amount) + // { + // if (amount < 1) + // throw new ArgumentOutOfRangeException(nameof(amount)); + // + // + // } + + private Patron PatronUserToPatron(PatronUser user) + => new Patron() + { + UnqiuePlatformUserId = user.UniquePlatformUserId, + UserId = user.UserId, + Amount = user.AmountCents, + Tier = CalculateTier(user), + PaidAt = user.LastCharge, + ValidThru = user.ValidThru, + }; + + private PatronTier CalculateTier(PatronUser user) + { + if (user.ValidThru.IsBeforeToday()) + return PatronTier.None; + + return user.AmountCents switch + { + >= 1000 => PatronTier.X, + >= 500 => PatronTier.V, + >= 100 => PatronTier.I, + _ => PatronTier.None + }; + } + + private async Task SendWelcomeMessage(Patron patron) + { + try + { + var user = (IUser)_client.GetUser(patron.UserId) ?? await _client.Rest.GetUserAsync(patron.UserId); + if (user is null) + return; + + var eb = _eb.Create() + .WithOkColor() + .WithTitle("❤️ Thank you for supporting NadekoBot! ❤️") + .WithDescription( + "Your donation has been processed and you will receive the rewards shortly.\n" + + "You can visit to see rewards for your tier. 🎉") + .AddField("Tier", Format.Bold(patron.Tier.ToString()), true) + .AddField("Pledge", $"**{patron.Amount / 100.0f:N1}$**", true) + .AddField("Expires", + patron.ValidThru.AddDays(1).ToShortAndRelativeTimestampTag(), + true) + .AddField("Instructions", + @"*- Within the next **1-2 minutes** you will have all of the benefits of the Tier you've subscribed to.* +*- You can check your benefits on * +*- You can use the `.patron` command in this chat to check your current quota usage for the Patron-only commands* +*- **ALL** of the servers that you **own** will enjoy your Patron benefits.* +*- You can use any of the commands available in your tier on any server (assuming you have sufficient permissions to run those commands)* +*- Any user in any of your servers can use Patron-only commands, but they will spend **your quota**, which is why it's recommended to use Nadeko's command cooldown system (.h .cmdcd) or permission system to limit the command usage for your server members.* +*- Permission guide can be found here if you're not familiar with it: *", + isInline: false) + .WithFooter($"platform id: {patron.UnqiuePlatformUserId}"); + + await user.EmbedAsync(eb); + } + catch + { + Log.Warning("Unable to send a \"Welcome\" message to the patron {UserId}", patron.UserId); + } + } + + public async Task<(int Success, int Failed)> SendMessageToPatronsAsync(PatronTier tierAndHigher, string message) + { + await using var ctx = _db.GetDbContext(); + + var patrons = await ctx.GetTable() + .Where(x => x.ValidThru > DateTime.UtcNow) + .ToArrayAsync(); + + var text = SmartText.CreateFrom(message); + + var succ = 0; + var fail = 0; + foreach (var patron in patrons) + { + try + { + var user = await _client.GetUserAsync(patron.UserId); + await user.SendAsync(text); + ++succ; + } + catch + { + ++fail; + } + + await Task.Delay(1000); + } + + return (succ, fail); + } + + public PatronConfigData GetConfig() + => _pConf.Data; +} + +public readonly struct FeatureLimitKey +{ + public string PrettyName { get; init; } + public string Key { get; init; } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Utility/Patronage/QuotaLimit.cs b/src/NadekoBot/Modules/Utility/Patronage/QuotaLimit.cs new file mode 100644 index 000000000..79a6a1a58 --- /dev/null +++ b/src/NadekoBot/Modules/Utility/Patronage/QuotaLimit.cs @@ -0,0 +1,66 @@ +using NadekoBot.Db.Models; + +namespace NadekoBot.Modules.Utility.Patronage; + +/// +/// Represents information about why the user has triggered a quota limit +/// +public readonly struct QuotaLimit +{ + /// + /// Amount of usages reached, which is the limit + /// + public uint Quota { get; init; } + + /// + /// Which period is this quota limit for (hourly, daily, monthly, etc...) + /// + public QuotaPer QuotaPeriod { get; init; } + + /// + /// When does this quota limit reset + /// + public DateTime ResetsAt { get; init; } + + /// + /// Type of the feature this quota limit is for + /// + public FeatureType FeatureType { get; init; } + + /// + /// Name of the feature this quota limit is for + /// + public string Feature { get; init; } + + /// + /// Whether it is the user's own quota (true), or server owners (false) + /// + public bool IsOwnQuota { get; init; } +} + + +/// +/// Respresent information about the feature limit +/// +public readonly struct FeatureLimit +{ + + /// + /// Whether this limit comes from the patronage system + /// + public bool IsPatronLimit { get; init; } = false; + + /// + /// Maximum limit allowed + /// + public int? Quota { get; init; } = null; + + /// + /// Name of the limit + /// + public string Name { get; init; } = string.Empty; + + public FeatureLimit() + { + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Utility/Patronage/SubscriptionChargeStatus.cs b/src/NadekoBot/Modules/Utility/Patronage/SubscriptionChargeStatus.cs new file mode 100644 index 000000000..6b4644b82 --- /dev/null +++ b/src/NadekoBot/Modules/Utility/Patronage/SubscriptionChargeStatus.cs @@ -0,0 +1,10 @@ +#nullable disable +namespace NadekoBot.Modules.Utility; + +public enum SubscriptionChargeStatus +{ + Paid, + Refunded, + Unpaid, + Other, +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Utility/Patronage/UserQuotaStats.cs b/src/NadekoBot/Modules/Utility/Patronage/UserQuotaStats.cs new file mode 100644 index 000000000..3d8387f53 --- /dev/null +++ b/src/NadekoBot/Modules/Utility/Patronage/UserQuotaStats.cs @@ -0,0 +1,25 @@ +namespace NadekoBot.Modules.Utility.Patronage; + +public readonly struct UserQuotaStats +{ + private static readonly IReadOnlyDictionary _emptyDictionary + = new Dictionary(); + public PatronTier Tier { get; init; } + = PatronTier.None; + + public IReadOnlyDictionary Features { get; init; } + = _emptyDictionary; + + public IReadOnlyDictionary Commands { get; init; } + = _emptyDictionary; + + public IReadOnlyDictionary Groups { get; init; } + = _emptyDictionary; + + public IReadOnlyDictionary Modules { get; init; } + = _emptyDictionary; + + public UserQuotaStats() + { + } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Utility/Patronage/_common/IPatronData.cs b/src/NadekoBot/Modules/Utility/Patronage/_common/IPatronData.cs new file mode 100644 index 000000000..811beb7d8 --- /dev/null +++ b/src/NadekoBot/Modules/Utility/Patronage/_common/IPatronData.cs @@ -0,0 +1,11 @@ +namespace NadekoBot.Modules.Utility; + +public interface ISubscriberData +{ + public string UniquePlatformUserId { get; } + public ulong UserId { get; } + public int Cents { get; } + + public DateTime? LastCharge { get; } + public SubscriptionChargeStatus ChargeStatus { get; } +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Utility/Patronage/_common/IPatronageService.cs b/src/NadekoBot/Modules/Utility/Patronage/_common/IPatronageService.cs new file mode 100644 index 000000000..cbb6cac45 --- /dev/null +++ b/src/NadekoBot/Modules/Utility/Patronage/_common/IPatronageService.cs @@ -0,0 +1,56 @@ +using NadekoBot.Db.Models; +using OneOf; + +namespace NadekoBot.Modules.Utility.Patronage; + +/// +/// Manages patrons and provides access to their data +/// +public interface IPatronageService +{ + /// + /// Called when the payment is made. + /// Either as a single payment for that patron, + /// or as a recurring monthly donation. + /// + public event Func OnNewPatronPayment; + + /// + /// Called when the patron changes the pledge amount + /// (Patron old, Patron new) => Task + /// + public event Func OnPatronUpdated; + + /// + /// Called when the patron refunds the purchase or it's marked as fraud + /// + public event Func OnPatronRefunded; + + /// + /// Gets a Patron with the specified userId + /// + /// UserId for which to get the patron data for. + /// A patron with the specifeid userId + public Task GetPatronAsync(ulong userId); + + /// + /// Gets the quota statistic for the user/patron specified by the userId + /// + /// UserId of the user for which to get the quota statistic for + /// Quota stats for the specified user + Task GetUserQuotaStatistic(ulong userId); + + + Task TryGetFeatureLimitAsync(FeatureLimitKey key, ulong userId, int? defaultValue); + + ValueTask> TryIncrementQuotaCounterAsync( + ulong userId, + bool isSelf, + FeatureType featureType, + string featureName, + uint? maybeHourly, + uint? maybeDaily, + uint? maybeMonthly); + + PatronConfigData GetConfig(); +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Utility/Patronage/_common/ISubscriptionHandler.cs b/src/NadekoBot/Modules/Utility/Patronage/_common/ISubscriptionHandler.cs new file mode 100644 index 000000000..7e2a49773 --- /dev/null +++ b/src/NadekoBot/Modules/Utility/Patronage/_common/ISubscriptionHandler.cs @@ -0,0 +1,16 @@ +#nullable disable +namespace NadekoBot.Modules.Utility; + +/// +/// Services implementing this interface are handling pledges/subscriptions/payments coming +/// from a payment platform. +/// +public interface ISubscriptionHandler +{ + /// + /// Get Current patrons in batches. + /// This will only return patrons who have their discord account connected + /// + /// Batched patrons + public IAsyncEnumerable> GetPatronsAsync(); +} \ No newline at end of file diff --git a/src/NadekoBot/Modules/Utility/Remind/RemindService.cs b/src/NadekoBot/Modules/Utility/Remind/RemindService.cs index 62e152942..d7d32ca25 100644 --- a/src/NadekoBot/Modules/Utility/Remind/RemindService.cs +++ b/src/NadekoBot/Modules/Utility/Remind/RemindService.cs @@ -76,15 +76,14 @@ public class RemindService : INService, IReadyExecutor await uow.SaveChangesAsync(); } - // todo move isonshard to a method private async Task> GetRemindersBeforeAsync(DateTime now) { await using var uow = _db.GetDbContext(); return await uow.Reminders - .ToLinqToDBTable() - .Where(x => x.ServerId / 4194304 % (ulong)_creds.TotalShards == (ulong)_client.ShardId - && x.When < now) - .ToListAsyncLinqToDB(); + .ToLinqToDBTable() + .Where(x => Linq2DbExpressions.GuildOnShard(x.ServerId, _creds.TotalShards, _client.ShardId) + && x.When < now) + .ToListAsyncLinqToDB(); } public bool TryParseRemindMessage(string input, out RemindObject obj) diff --git a/src/NadekoBot/Modules/Utility/Utility.cs b/src/NadekoBot/Modules/Utility/Utility.cs index 367cb6cd1..8b243fcc3 100644 --- a/src/NadekoBot/Modules/Utility/Utility.cs +++ b/src/NadekoBot/Modules/Utility/Utility.cs @@ -465,9 +465,6 @@ public partial class Utility : NadekoModule } [Cmd] -#if GLOBAL_NADEKO - [Ratelimit(30)] -#endif public async partial Task Ping() { await sem.WaitAsync(5000); diff --git a/src/NadekoBot/Modules/Utility/VerboseErrors/VerboseErrorsService.cs b/src/NadekoBot/Modules/Utility/VerboseErrors/VerboseErrorsService.cs index 8924e70cf..8da8266e8 100644 --- a/src/NadekoBot/Modules/Utility/VerboseErrors/VerboseErrorsService.cs +++ b/src/NadekoBot/Modules/Utility/VerboseErrors/VerboseErrorsService.cs @@ -6,7 +6,7 @@ namespace NadekoBot.Modules.Utility.Services; public class VerboseErrorsService : INService { - private readonly ConcurrentHashSet _guildsEnabled; + private readonly ConcurrentHashSet _guildsDisabled; private readonly DbService _db; private readonly CommandHandler _ch; private readonly HelpService _hs; @@ -23,12 +23,12 @@ public class VerboseErrorsService : INService _ch.CommandErrored += LogVerboseError; - _guildsEnabled = new(bot.AllGuildConfigs.Where(x => x.VerboseErrors).Select(x => x.GuildId)); + _guildsDisabled = new(bot.AllGuildConfigs.Where(x => !x.VerboseErrors).Select(x => x.GuildId)); } private async Task LogVerboseError(CommandInfo cmd, ITextChannel channel, string reason) { - if (channel is null || !_guildsEnabled.Contains(channel.GuildId)) + if (channel is null || _guildsDisabled.Contains(channel.GuildId)) return; try @@ -36,35 +36,35 @@ public class VerboseErrorsService : INService var embed = _hs.GetCommandHelp(cmd, channel.Guild) .WithTitle("Command Error") .WithDescription(reason) + .WithFooter("Admin may disable verbose errors via `.ve` command") .WithErrorColor(); await channel.EmbedAsync(embed); } catch { - //ignore + Log.Information("Verbose error wasn't able to be sent to the server: {GuildId}", + channel.GuildId); } } - public bool ToggleVerboseErrors(ulong guildId, bool? enabled = null) + public bool ToggleVerboseErrors(ulong guildId, bool? maybeEnabled = null) { - using (var uow = _db.GetDbContext()) - { - var gc = uow.GuildConfigsForId(guildId, set => set); + using var uow = _db.GetDbContext(); + var gc = uow.GuildConfigsForId(guildId, set => set); - if (enabled == null) - enabled = gc.VerboseErrors = !gc.VerboseErrors; // Old behaviour, now behind a condition - else - gc.VerboseErrors = (bool)enabled; // New behaviour, just set it. + if (maybeEnabled is bool isEnabled) // set it + gc.VerboseErrors = isEnabled; + else // toggle it + isEnabled = gc.VerboseErrors = !gc.VerboseErrors; - uow.SaveChanges(); - } + uow.SaveChanges(); - if ((bool)enabled) // This doesn't need to be duplicated inside the using block - _guildsEnabled.Add(guildId); + if (isEnabled) // This doesn't need to be duplicated inside the using block + _guildsDisabled.TryRemove(guildId); else - _guildsEnabled.TryRemove(guildId); + _guildsDisabled.Add(guildId); - return (bool)enabled; + return isEnabled; } } \ No newline at end of file diff --git a/src/NadekoBot/Modules/Utility/_Common/Patreon/PatreonData.cs b/src/NadekoBot/Modules/Utility/_Common/Patreon/PatreonData.cs index 74153f9a6..9a855e004 100644 --- a/src/NadekoBot/Modules/Utility/_Common/Patreon/PatreonData.cs +++ b/src/NadekoBot/Modules/Utility/_Common/Patreon/PatreonData.cs @@ -12,7 +12,7 @@ public sealed class Attributes public bool IsFollower { get; set; } [JsonPropertyName("last_charge_date")] - public DateTime LastChargeDate { get; set; } + public DateTime? LastChargeDate { get; set; } [JsonPropertyName("last_charge_status")] public string LastChargeStatus { get; set; } @@ -57,7 +57,7 @@ public sealed class Address // // public CurrentlyEntitledTiers CurrentlyEntitledTiers { get; set; } // } -public sealed class PatreonResponse +public sealed class PatreonMembersResponse { [JsonPropertyName("data")] public List Data { get; set; } diff --git a/src/NadekoBot/Modules/Xp/Xp.cs b/src/NadekoBot/Modules/Xp/Xp.cs index be2301fb1..74a87a58d 100644 --- a/src/NadekoBot/Modules/Xp/Xp.cs +++ b/src/NadekoBot/Modules/Xp/Xp.cs @@ -2,6 +2,7 @@ using NadekoBot.Modules.Gambling.Services; using NadekoBot.Modules.Xp.Services; using NadekoBot.Services.Database.Models; +using System.Diagnostics; namespace NadekoBot.Modules.Xp; diff --git a/src/NadekoBot/Modules/Xp/XpService.cs b/src/NadekoBot/Modules/Xp/XpService.cs index d5b6ae770..ca3b611df 100644 --- a/src/NadekoBot/Modules/Xp/XpService.cs +++ b/src/NadekoBot/Modules/Xp/XpService.cs @@ -3,6 +3,7 @@ using Microsoft.EntityFrameworkCore; using NadekoBot.Common.ModuleBehaviors; using NadekoBot.Db; using NadekoBot.Db.Models; +using NadekoBot.Modules.Utility.Patronage; using NadekoBot.Services.Database.Models; using Newtonsoft.Json; using SixLabors.Fonts; @@ -43,6 +44,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand private readonly DiscordSocketClient _client; private readonly TypedKey _xpTemplateReloadKey; + private readonly IPatronageService _ps; public XpService( DiscordSocketClient client, @@ -57,7 +59,8 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand IHttpClientFactory http, XpConfigService xpConfig, IPubSub pubSub, - IEmbedBuilderService eb) + IEmbedBuilderService eb, + IPatronageService ps) { _db = db; _cmd = cmd; @@ -75,6 +78,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand _excludedChannels = new(); _client = client; _xpTemplateReloadKey = new("xp.template.reload"); + _ps = ps; InternalReloadXpTemplate(); @@ -167,7 +171,6 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand if (oldGlobalLevelData.Level < newGlobalLevelData.Level) { - du.LastLevelUp = DateTime.UtcNow; var first = item.First(); if (du.NotifyOnLevelUp != XpNotificationLocation.None) { @@ -178,7 +181,6 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand if (oldGuildLevelData.Level < newGuildLevelData.Level) { - usr.LastLevelUp = DateTime.UtcNow; //send level up notification var first = item.First(); if (usr.NotifyOnLevelUp != XpNotificationLocation.None) @@ -270,7 +272,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand } } - + private const string XP_TEMPLATE_PATH = "./data/xp_template.json"; private void InternalReloadXpTemplate() { try @@ -279,15 +281,33 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand { ContractResolver = new RequireObjectPropertiesContractResolver() }; - template = JsonConvert.DeserializeObject(File.ReadAllText("./data/xp_template.json"), + + if (!File.Exists(XP_TEMPLATE_PATH)) + { + var newTemp = new XpTemplate(); + newTemp.Version = 1; + File.WriteAllText(XP_TEMPLATE_PATH, JsonConvert.SerializeObject(newTemp, Formatting.Indented)); + } + + template = JsonConvert.DeserializeObject( + File.ReadAllText(XP_TEMPLATE_PATH), settings); + + if (template!.Version < 1) + { + Log.Warning("Loaded default xp_template.json values as the old one was version 0. " + + "Old one was renamed to xp_template.json.old"); + File.WriteAllText("./data/xp_template.json.old", JsonConvert.SerializeObject(template, Formatting.Indented)); + template = new(); + template.Version = 1; + File.WriteAllText(XP_TEMPLATE_PATH, JsonConvert.SerializeObject(template, Formatting.Indented)); + } } catch (Exception ex) { - Log.Error(ex, "Xp template is invalid. Loaded default values"); + Log.Error(ex, "xp_template.json is invalid. Loaded default values"); template = new(); - File.WriteAllText("./data/xp_template_backup.json", - JsonConvert.SerializeObject(template, Formatting.Indented)); + template.Version = 1; } } @@ -643,22 +663,20 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand public async Task GetUserStatsAsync(IGuildUser user) { - DiscordUser du; - UserXpStats stats; - long totalXp; - int globalRank; - int guildRank; - await using (var uow = _db.GetDbContext()) - { - du = uow.GetOrCreateUser(user, set => set.Include(x => x.Club)); - totalXp = du.TotalXp; - globalRank = uow.DiscordUser.GetUserGlobalRank(user.Id); - guildRank = uow.UserXpStats.GetUserGuildRanking(user.Id, user.GuildId); - stats = uow.GetOrCreateUserXpStats(user.GuildId, user.Id); - await uow.SaveChangesAsync(); - } + await using var uow = _db.GetDbContext(); + var du = uow.GetOrCreateUser(user, set => set.Include(x => x.Club)); + var totalXp = du.TotalXp; + var globalRank = uow.DiscordUser.GetUserGlobalRank(user.Id); + var guildRank = uow.UserXpStats.GetUserGuildRanking(user.Id, user.GuildId); + var stats = uow.GetOrCreateUserXpStats(user.GuildId, user.Id); + await uow.SaveChangesAsync(); - return new(du, stats, new(totalXp), new(stats.Xp + stats.AwardedXp), globalRank, guildRank); + return new(du, + stats, + new(totalXp), + new(stats.Xp + stats.AwardedXp), + globalRank, + guildRank); } public bool ToggleExcludeServer(ulong id) @@ -801,12 +819,38 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand new(template.Club.Name.Pos.X + 50, template.Club.Name.Pos.Y - 8))); } + Font GetTruncatedFont( + FontFamily fontFamily, + int fontSize, + FontStyle style, + string text, + int maxSize) + { + var font = fontFamily.CreateFont(fontSize, style); + var size = TextMeasurer.Measure(text, new(font)); + var scale = maxSize / size.Width; + if (scale < 1) + font = fontFamily.CreateFont(fontSize * scale, style); + + return font; + } + + if (template.User.GlobalLevel.Show) { + // up to 83 width + + var globalLevelFont = GetTruncatedFont( + _fonts.NotoSans, + template.User.GlobalLevel.FontSize, + FontStyle.Bold, + stats.Global.Level.ToString(), + 75); + img.Mutate(x => { x.DrawText(stats.Global.Level.ToString(), - _fonts.NotoSans.CreateFont(template.User.GlobalLevel.FontSize, FontStyle.Bold), + globalLevelFont, template.User.GlobalLevel.Color, new(template.User.GlobalLevel.Pos.X, template.User.GlobalLevel.Pos.Y)); //level }); @@ -814,17 +858,23 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand if (template.User.GuildLevel.Show) { + var guildLevelFont = GetTruncatedFont( + _fonts.NotoSans, + template.User.GuildLevel.FontSize, + FontStyle.Bold, + stats.Guild.Level.ToString(), + 75); + img.Mutate(x => { x.DrawText(stats.Guild.Level.ToString(), - _fonts.NotoSans.CreateFont(template.User.GuildLevel.FontSize, FontStyle.Bold), + guildLevelFont, template.User.GuildLevel.Color, new(template.User.GuildLevel.Pos.X, template.User.GuildLevel.Pos.Y)); }); } - - var pen = new Pen(Color.Black, 1); + var pen = new Pen(Color.Black, 1.25f); var global = stats.Global; var guild = stats.Guild; @@ -840,7 +890,16 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand if (template.User.Xp.Global.Show) { - img.Mutate(x => x.DrawText($"{global.LevelXp}/{global.RequiredXp}", + img.Mutate(x => x.DrawText( + new() + { + TextOptions = new() + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + } + }, + $"{global.LevelXp}/{global.RequiredXp}", _fonts.NotoSans.CreateFont(template.User.Xp.Global.FontSize, FontStyle.Bold), Brushes.Solid(template.User.Xp.Global.Color), pen, @@ -849,7 +908,16 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand if (template.User.Xp.Guild.Show) { - img.Mutate(x => x.DrawText($"{guild.LevelXp}/{guild.RequiredXp}", + img.Mutate(x => x.DrawText( + new() + { + TextOptions = new() + { + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + } + }, + $"{guild.LevelXp}/{guild.RequiredXp}", _fonts.NotoSans.CreateFont(template.User.Xp.Guild.FontSize, FontStyle.Bold), Brushes.Solid(template.User.Xp.Guild.Color), pen, @@ -872,46 +940,39 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand //ranking if (template.User.GlobalRank.Show) { - img.Mutate(x => x.DrawText(stats.GlobalRanking.ToString(), - _fonts.UniSans.CreateFont(template.User.GlobalRank.FontSize, FontStyle.Bold), + var globalRankStr = stats.GlobalRanking.ToString(); + + var globalRankFont = GetTruncatedFont( + _fonts.UniSans, + template.User.GlobalRank.FontSize, + FontStyle.Bold, + globalRankStr, + 68); + + img.Mutate(x => x.DrawText(globalRankStr, + globalRankFont, template.User.GlobalRank.Color, new(template.User.GlobalRank.Pos.X, template.User.GlobalRank.Pos.Y))); } if (template.User.GuildRank.Show) { - img.Mutate(x => x.DrawText(stats.GuildRanking.ToString(), - _fonts.UniSans.CreateFont(template.User.GuildRank.FontSize, FontStyle.Bold), + var guildRankStr = stats.GuildRanking.ToString(); + + var guildRankFont = GetTruncatedFont( + _fonts.UniSans, + template.User.GuildRank.FontSize, + FontStyle.Bold, + guildRankStr, + 43); + + img.Mutate(x => x.DrawText(guildRankStr, + guildRankFont, template.User.GuildRank.Color, new(template.User.GuildRank.Pos.X, template.User.GuildRank.Pos.Y))); } - - //time on this level - - string GetTimeSpent(DateTime time, string format) - { - var offset = DateTime.UtcNow - time; - return string.Format(format, offset.Days, offset.Hours, offset.Minutes); - } - - if (template.User.TimeOnLevel.Global.Show) - { - img.Mutate(x => x.DrawText(GetTimeSpent(stats.User.LastLevelUp, template.User.TimeOnLevel.Format), - _fonts.NotoSans.CreateFont(template.User.TimeOnLevel.Global.FontSize, FontStyle.Bold), - template.User.TimeOnLevel.Global.Color, - new(template.User.TimeOnLevel.Global.Pos.X, template.User.TimeOnLevel.Global.Pos.Y))); - } - - if (template.User.TimeOnLevel.Guild.Show) - { - img.Mutate(x - => x.DrawText(GetTimeSpent(stats.FullGuildStats.LastLevelUp, template.User.TimeOnLevel.Format), - _fonts.NotoSans.CreateFont(template.User.TimeOnLevel.Guild.FontSize, FontStyle.Bold), - template.User.TimeOnLevel.Guild.Color, - new(template.User.TimeOnLevel.Guild.Pos.X, template.User.TimeOnLevel.Guild.Pos.Y))); - } + //avatar - if (stats.User.AvatarId is not null && template.User.Icon.Show) { try @@ -959,10 +1020,34 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand if (template.Club.Icon.Show) await DrawClubImage(img, stats); - img.Mutate(x => x.Resize(template.OutputSize.X, template.OutputSize.Y)); - return ((Stream)img.ToStream(imageFormat), imageFormat); +// #if GLOBAL_NADEKO + await DrawFrame(img, stats.User.UserId); +// #endif + + var outputSize = template.OutputSize; + if (outputSize.X != img.Width || outputSize.Y != img.Height) + img.Mutate(x => x.Resize(template.OutputSize.X, template.OutputSize.Y)); + + var output = ((Stream)await img.ToStreamAsync(imageFormat), imageFormat); + + return output; }); +// #if GLOBAL_NADEKO + private async Task DrawFrame(Image img, ulong userId) + { + var patron = await _ps.GetPatronAsync(userId); + Image frame = null; + if (patron.Tier == PatronTier.V) + frame = Image.Load(File.OpenRead("data/images/frame_silver.png")); + else if (patron.Tier >= PatronTier.X || _creds.IsOwner(userId)) + frame = Image.Load(File.OpenRead("data/images/frame_gold.png")); + + if (frame is not null) + img.Mutate(x => x.DrawImage(frame, new Point(0, 0), new GraphicsOptions())); + } +// #endif + private void DrawXpBar(float percent, XpBar info, Image img) { var x1 = info.PointA.X; @@ -1026,7 +1111,7 @@ public class XpService : INService, IReadyExecutor, IExecNoCommand { if (!temp.IsImage() || temp.GetContentLength() > 11.Megabytes().Bytes) return; - + var imgData = await temp.Content.ReadAsByteArrayAsync(); using (var tempDraw = Image.Load(imgData)) { diff --git a/src/NadekoBot/Modules/Xp/_Common/XpTemplate.cs b/src/NadekoBot/Modules/Xp/_Common/XpTemplate.cs index cc750a00b..9ef13515a 100644 --- a/src/NadekoBot/Modules/Xp/_Common/XpTemplate.cs +++ b/src/NadekoBot/Modules/Xp/_Common/XpTemplate.cs @@ -7,11 +7,13 @@ namespace NadekoBot.Modules.Xp; public class XpTemplate { + public int Version { get; set; } = 0; + [JsonProperty("output_size")] public XpTemplatePos OutputSize { get; set; } = new() { - X = 450, - Y = 220 + X = 800, + Y = 392 }; public XpTemplateUser User { get; set; } = new() @@ -31,13 +33,13 @@ public class XpTemplate Show = true, Pos = new() { - X = 32, - Y = 10 + X = 14, + Y = 14 }, Size = new() { - X = 69, - Y = 70 + X = 72, + Y = 71 } }, GuildLevel = new() @@ -47,7 +49,7 @@ public class XpTemplate Pos = new() { X = 47, - Y = 297 + Y = 308 } }, GlobalLevel = new() @@ -57,7 +59,7 @@ public class XpTemplate Pos = new() { X = 47, - Y = 149 + Y = 160 } }, GuildRank = new() @@ -80,31 +82,6 @@ public class XpTemplate Y = 179 } }, - TimeOnLevel = - new() - { - Format = "{0}d{1}h{2}m", - Global = new() - { - FontSize = 20, - Show = true, - Pos = new() - { - X = 50, - Y = 204 - } - }, - Guild = new() - { - FontSize = 20, - Show = true, - Pos = new() - { - X = 50, - Y = 351 - } - } - }, Xp = new() { Bar = new() @@ -149,8 +126,8 @@ public class XpTemplate FontSize = 50, Pos = new() { - X = 430, - Y = 142 + X = 528, + Y = 170 } }, Guild = new() @@ -159,8 +136,8 @@ public class XpTemplate FontSize = 50, Pos = new() { - X = 400, - Y = 282 + X = 490, + Y = 313 } }, Awarded = new() @@ -169,8 +146,8 @@ public class XpTemplate FontSize = 25, Pos = new() { - X = 445, - Y = 347 + X = 490, + Y = 345 } } } @@ -226,17 +203,9 @@ public class XpTemplateUser public XpTemplateText GuildLevel { get; set; } public XpTemplateText GlobalRank { get; set; } public XpTemplateText GuildRank { get; set; } - public XpTemplateTimeOnLevel TimeOnLevel { get; set; } public XpTemplateXp Xp { get; set; } } -public class XpTemplateTimeOnLevel -{ - public string Format { get; set; } - public XpTemplateText Global { get; set; } - public XpTemplateText Guild { get; set; } -} - public class XpTemplateClub { public XpTemplateIcon Icon { get; set; } diff --git a/src/NadekoBot/NadekoBot.csproj b/src/NadekoBot/NadekoBot.csproj index 329cfb523..c9b20801a 100644 --- a/src/NadekoBot/NadekoBot.csproj +++ b/src/NadekoBot/NadekoBot.csproj @@ -43,6 +43,7 @@ + @@ -51,6 +52,7 @@ + @@ -114,7 +116,7 @@ PreserveNewest - + Always diff --git a/src/NadekoBot/Services/Currency/CurrencyServiceExtensions.cs b/src/NadekoBot/Services/Currency/CurrencyServiceExtensions.cs index 48954bd07..603eb479b 100644 --- a/src/NadekoBot/Services/Currency/CurrencyServiceExtensions.cs +++ b/src/NadekoBot/Services/Currency/CurrencyServiceExtensions.cs @@ -10,7 +10,7 @@ public static class CurrencyServiceExtensions return await wallet.GetBalance(); } - // todo transfer should be a transaction + // FUTURE should be a transaction public static async Task TransferAsync( this ICurrencyService cs, IEmbedBuilderService ebs, diff --git a/src/NadekoBot/Services/IGoogleApiService.cs b/src/NadekoBot/Services/IGoogleApiService.cs index 2fc154380..5d2592817 100644 --- a/src/NadekoBot/Services/IGoogleApiService.cs +++ b/src/NadekoBot/Services/IGoogleApiService.cs @@ -1,5 +1,4 @@ #nullable disable -using Google.Apis.Customsearch.v1.Data; namespace NadekoBot.Services; @@ -13,21 +12,8 @@ public interface IGoogleApiService Task> GetRelatedVideosAsync(string id, int count = 1, string user = null); Task> GetPlaylistTracksAsync(string playlistId, int count = 50); Task> GetVideoDurationsAsync(IEnumerable videoIds); - Task GetImageAsync(string query); Task Translate(string sourceText, string sourceLanguage, string targetLanguage); Task ShortenUrl(string url); Task ShortenUrl(Uri url); -} - -public struct ImageResult -{ - public Result.ImageData Image { get; } - public string Link { get; } - - public ImageResult(Result.ImageData image, string link) - { - Image = image; - Link = link; - } } \ No newline at end of file diff --git a/src/NadekoBot/Services/Impl/BotCredsProvider.cs b/src/NadekoBot/Services/Impl/BotCredsProvider.cs index 0ae8e204f..4550e60f9 100644 --- a/src/NadekoBot/Services/Impl/BotCredsProvider.cs +++ b/src/NadekoBot/Services/Impl/BotCredsProvider.cs @@ -112,8 +112,6 @@ public sealed class BotCredsProvider : IBotCredsProvider ymlData = Yaml.Serializer.Serialize(creds); File.WriteAllText(CREDS_FILE_NAME, ymlData); - - Reload(); } private string OldCredsJsonPath @@ -174,5 +172,10 @@ public sealed class BotCredsProvider : IBotCredsProvider } public IBotCredentials GetCreds() - => _creds; + { + lock (_reloadLock) + { + return _creds; + } + } } \ No newline at end of file diff --git a/src/NadekoBot/Services/Impl/GoogleApiService.cs b/src/NadekoBot/Services/Impl/GoogleApiService.cs index 757cd543a..5191276f7 100644 --- a/src/NadekoBot/Services/Impl/GoogleApiService.cs +++ b/src/NadekoBot/Services/Impl/GoogleApiService.cs @@ -1,6 +1,5 @@ #nullable disable using Google; -using Google.Apis.Customsearch.v1; using Google.Apis.Services; using Google.Apis.Urlshortener.v1; using Google.Apis.YouTube.v3; @@ -13,8 +12,6 @@ namespace NadekoBot.Services; public class GoogleApiService : IGoogleApiService, INService { - private const string SEARCH_ENGINE_ID = "018084019232060951019:hs5piey28-e"; - private static readonly Regex _plRegex = new("(?:youtu\\.be\\/|list=)(?[\\da-zA-Z\\-_]*)", RegexOptions.Compiled); @@ -153,13 +150,12 @@ public class GoogleApiService : IGoogleApiService, INService private readonly YouTubeService _yt; private readonly UrlshortenerService _sh; - private readonly CustomsearchService _cs; //private readonly Regex YtVideoIdRegex = new Regex(@"(?:youtube\.com\/\S*(?:(?:\/e(?:mbed))?\/|watch\?(?:\S*?&?v\=))|youtu\.be\/)(?[a-zA-Z0-9_-]{6,11})", RegexOptions.Compiled); - private readonly IBotCredentials _creds; + private readonly IBotCredsProvider _creds; private readonly IHttpClientFactory _httpFactory; - public GoogleApiService(IBotCredentials creds, IHttpClientFactory factory) + public GoogleApiService(IBotCredsProvider creds, IHttpClientFactory factory) { _creds = creds; _httpFactory = factory; @@ -167,12 +163,11 @@ public class GoogleApiService : IGoogleApiService, INService var bcs = new BaseClientService.Initializer { ApplicationName = "Nadeko Bot", - ApiKey = _creds.GoogleApiKey + ApiKey = _creds.GetCreds().GoogleApiKey }; _yt = new(bcs); _sh = new(bcs); - _cs = new(bcs); } public async Task> GetPlaylistIdsByKeywordsAsync(string keywords, int count = 1) @@ -207,7 +202,7 @@ public class GoogleApiService : IGoogleApiService, INService query.RelatedToVideoId = id; query.Type = "video"; query.QuotaUser = user; - return (await query.ExecuteAsync()).Items.Select(i => "http://www.youtube.com/watch?v=" + i.Id.VideoId); + return (await query.ExecuteAsync()).Items.Select(i => "https://www.youtube.com/watch?v=" + i.Id.VideoId); } public async Task> GetVideoLinksByKeywordAsync(string keywords, int count = 1) @@ -223,7 +218,7 @@ public class GoogleApiService : IGoogleApiService, INService query.Q = keywords; query.Type = "video"; query.SafeSearch = SearchResource.ListRequest.SafeSearchEnum.Strict; - return (await query.ExecuteAsync()).Items.Select(i => "http://www.youtube.com/watch?v=" + i.Id.VideoId); + return (await query.ExecuteAsync()).Items.Select(i => "https://www.youtube.com/watch?v=" + i.Id.VideoId); } public async Task> GetVideoInfosByKeywordAsync( @@ -241,7 +236,7 @@ public class GoogleApiService : IGoogleApiService, INService query.Q = keywords; query.Type = "video"; return (await query.ExecuteAsync()).Items.Select(i - => (i.Snippet.Title.TrimTo(50), i.Id.VideoId, "http://www.youtube.com/watch?v=" + i.Id.VideoId)); + => (i.Snippet.Title.TrimTo(50), i.Id.VideoId, "https://www.youtube.com/watch?v=" + i.Id.VideoId)); } public Task ShortenUrl(Uri url) @@ -252,7 +247,7 @@ public class GoogleApiService : IGoogleApiService, INService if (string.IsNullOrWhiteSpace(url)) throw new ArgumentNullException(nameof(url)); - if (string.IsNullOrWhiteSpace(_creds.GoogleApiKey)) + if (string.IsNullOrWhiteSpace(_creds.GetCreds().GoogleApiKey)) return url; try @@ -332,25 +327,6 @@ public class GoogleApiService : IGoogleApiService, INService return toReturn; } - public async Task GetImageAsync(string query) - { - if (string.IsNullOrWhiteSpace(query)) - throw new ArgumentNullException(nameof(query)); - - var req = _cs.Cse.List(); - req.Q = query; - req.Cx = SEARCH_ENGINE_ID; - req.Num = 1; - req.Fields = "items(image(contextLink,thumbnailLink),link)"; - req.SearchType = CseResource.ListRequest.SearchTypeEnum.Image; - req.Start = new NadekoRandom().Next(0, 20); - req.Safe = CseResource.ListRequest.SafeEnum.Active; - - var search = await req.ExecuteAsync(); - - return new(search.Items[0].Image, search.Items[0].Link); - } - public async Task Translate(string sourceText, string sourceLanguage, string targetLanguage) { string text; @@ -379,4 +355,5 @@ public class GoogleApiService : IGoogleApiService, INService Languages.TryGetValue(language, out var mode); return mode; } -} \ No newline at end of file +} + diff --git a/src/NadekoBot/Services/Impl/RedisImagesCache.cs b/src/NadekoBot/Services/Impl/RedisImagesCache.cs index fe1668a0c..86bc8253e 100644 --- a/src/NadekoBot/Services/Impl/RedisImagesCache.cs +++ b/src/NadekoBot/Services/Impl/RedisImagesCache.cs @@ -164,6 +164,15 @@ public sealed class RedisImagesCache : IImageCache, IReadyExecutor localImageUrls.Version = 3; File.WriteAllText(_imagesPath, Yaml.Serializer.Serialize(localImageUrls)); } + + if (localImageUrls.Version == 3) + { + localImageUrls.Version = 4; + if (localImageUrls.Xp?.Bg.ToString() == "https://cdn.nadeko.bot/other/xp/bg.png") + localImageUrls.Xp.Bg = new("https://cdn.nadeko.bot/other/xp/bg_k.png"); + + File.WriteAllText(_imagesPath, Yaml.Serializer.Serialize(localImageUrls)); + } } public async Task Reload() diff --git a/src/NadekoBot/Services/Impl/StatsService.cs b/src/NadekoBot/Services/Impl/StatsService.cs index 635446b17..e72a1ea45 100644 --- a/src/NadekoBot/Services/Impl/StatsService.cs +++ b/src/NadekoBot/Services/Impl/StatsService.cs @@ -7,7 +7,7 @@ namespace NadekoBot.Services; public sealed class StatsService : IStatsService, IReadyExecutor, INService { - public const string BOT_VERSION = "4.1.6"; + public const string BOT_VERSION = "4.2.0"; public string Author => "Kwoth#2452"; diff --git a/src/NadekoBot/Services/Impl/YtdlOperation.cs b/src/NadekoBot/Services/Impl/YtdlOperation.cs index 886bbf5f5..84a424c78 100644 --- a/src/NadekoBot/Services/Impl/YtdlOperation.cs +++ b/src/NadekoBot/Services/Impl/YtdlOperation.cs @@ -8,9 +8,13 @@ namespace NadekoBot.Services; public class YtdlOperation { private readonly string _baseArgString; + private readonly bool _isYtDlp; - public YtdlOperation(string baseArgString) - => _baseArgString = baseArgString; + public YtdlOperation(string baseArgString, bool isYtDlp = false) + { + _baseArgString = baseArgString; + _isYtDlp = isYtDlp; + } private Process CreateProcess(string[] args) { @@ -19,7 +23,7 @@ public class YtdlOperation { StartInfo = new() { - FileName = "youtube-dl", + FileName = _isYtDlp ? "yt-dlp" : "youtube-dl", Arguments = string.Format(_baseArgString, newArgs), UseShellExecute = false, RedirectStandardError = true, diff --git a/src/NadekoBot/Services/Settings/ConfigParsers.cs b/src/NadekoBot/Services/Settings/ConfigParsers.cs index 33419f7ec..0dbc29d4d 100644 --- a/src/NadekoBot/Services/Settings/ConfigParsers.cs +++ b/src/NadekoBot/Services/Settings/ConfigParsers.cs @@ -1,4 +1,5 @@ #nullable disable +using NadekoBot.Modules.Searches; using SixLabors.ImageSharp.PixelFormats; using System.Globalization; @@ -31,6 +32,10 @@ public static class ConfigParsers return false; } } + + public static bool InsensitiveEnum(string input, out T output) + where T: struct + => Enum.TryParse(input, true, out output); } public static class ConfigPrinters diff --git a/src/NadekoBot/Services/Settings/ConfigServiceBase.cs b/src/NadekoBot/Services/Settings/ConfigServiceBase.cs index f2fcec511..7031d17c3 100644 --- a/src/NadekoBot/Services/Settings/ConfigServiceBase.cs +++ b/src/NadekoBot/Services/Settings/ConfigServiceBase.cs @@ -12,7 +12,7 @@ namespace NadekoBot.Services; public abstract class ConfigServiceBase : IConfigService where TSettings : ICloneable, new() { - // todo future config arrays are not copied - they're not protected from mutations + // FUTURE config arrays are not copied - they're not protected from mutations public TSettings Data => data.Clone(); diff --git a/src/NadekoBot/_Extensions/BotCredentialsExtensions.cs b/src/NadekoBot/_Extensions/BotCredentialsExtensions.cs index 8a2b75c11..14b65c1d6 100644 --- a/src/NadekoBot/_Extensions/BotCredentialsExtensions.cs +++ b/src/NadekoBot/_Extensions/BotCredentialsExtensions.cs @@ -3,5 +3,8 @@ namespace NadekoBot.Extensions; public static class BotCredentialsExtensions { public static bool IsOwner(this IBotCredentials creds, IUser user) - => creds.OwnerIds.Contains(user.Id); + => creds.IsOwner(user.Id); + + public static bool IsOwner(this IBotCredentials creds, ulong userId) + => creds.OwnerIds.Contains(userId); } \ No newline at end of file diff --git a/src/NadekoBot/_Extensions/Extensions.cs b/src/NadekoBot/_Extensions/Extensions.cs index fc3031d1a..fb929b58b 100644 --- a/src/NadekoBot/_Extensions/Extensions.cs +++ b/src/NadekoBot/_Extensions/Extensions.cs @@ -1,5 +1,6 @@ using Humanizer.Localisation; using Nadeko.Medusa; +using System.Diagnostics; using System.Globalization; using System.Net.Http.Headers; using System.Text.Json; @@ -179,6 +180,9 @@ public static class Extensions return module; } + public static string GetGroupName(this ModuleInfo module) + => module.Name.Replace("Commands", "", StringComparison.InvariantCulture); + public static async Task> GetMembersAsync(this IRole role) { var users = await role.Guild.GetUsersAsync(CacheMode.CacheOnly); @@ -214,4 +218,10 @@ public static class Extensions => msg.Content.Headers.ContentLength is long length ? length : long.MaxValue; + + public static void Lap(this Stopwatch sw, string checkpoint) + { + Log.Information("Checkpoint {CheckPoint}: {Time}", checkpoint, sw.Elapsed.TotalMilliseconds); + sw.Restart(); + } } \ No newline at end of file diff --git a/src/NadekoBot/_Extensions/IMessageChannelExtensions.cs b/src/NadekoBot/_Extensions/IMessageChannelExtensions.cs index d515f5cba..6da88ba21 100644 --- a/src/NadekoBot/_Extensions/IMessageChannelExtensions.cs +++ b/src/NadekoBot/_Extensions/IMessageChannelExtensions.cs @@ -30,7 +30,7 @@ public static class MessageChannelExtensions public static async Task SendAsync( this IMessageChannel channel, string? plainText, - NadekoInteraction? inter, + NadekoButtonInteraction? inter, Embed? embed = null, IReadOnlyCollection? embeds = null, bool sanitizeAll = false) @@ -69,7 +69,7 @@ public static class MessageChannelExtensions IEmbedBuilder? embed, string plainText = "", IReadOnlyCollection? embeds = null, - NadekoInteraction? inter = null) + NadekoButtonInteraction? inter = null) => ch.SendAsync(plainText, inter, embed: embed?.Build(), @@ -80,7 +80,7 @@ public static class MessageChannelExtensions IEmbedBuilderService eb, string text, MessageType type, - NadekoInteraction? inter = null) + NadekoButtonInteraction? inter = null) { var builder = eb.Create().WithDescription(text); @@ -106,12 +106,14 @@ public static class MessageChannelExtensions this IMessageChannel ch, IEmbedBuilderService eb, MessageType type, - string title, + string? title, string text, string? url = null, string? footer = null) { - var embed = eb.Create().WithDescription(text).WithTitle(title); + var embed = eb.Create() + .WithDescription(text) + .WithTitle(title); if (url is not null && Uri.IsWellFormedUriString(url, UriKind.Absolute)) embed.WithUrl(url); @@ -135,7 +137,7 @@ public static class MessageChannelExtensions public static Task SendConfirmAsync( this IMessageChannel ch, IEmbedBuilderService eb, - string title, + string? title, string text, string? url = null, string? footer = null) diff --git a/src/NadekoBot/_Extensions/ImagesharpExtensions.cs b/src/NadekoBot/_Extensions/ImagesharpExtensions.cs index ecda753da..aa501000e 100644 --- a/src/NadekoBot/_Extensions/ImagesharpExtensions.cs +++ b/src/NadekoBot/_Extensions/ImagesharpExtensions.cs @@ -100,4 +100,25 @@ public static class ImagesharpExtensions imageStream.Position = 0; return imageStream; } + + public static async Task ToStreamAsync(this Image img, IImageFormat? format = null) + { + var imageStream = new MemoryStream(); + if (format?.Name == "GIF") + { + await img.SaveAsGifAsync(imageStream); + } + else + { + await img.SaveAsPngAsync(imageStream, + new PngEncoder() + { + ColorType = PngColorType.RgbWithAlpha, + CompressionLevel = PngCompressionLevel.DefaultCompression + }); + } + + imageStream.Position = 0; + return imageStream; + } } \ No newline at end of file diff --git a/src/NadekoBot/_Extensions/SocketMessageComponentExtensions.cs b/src/NadekoBot/_Extensions/SocketMessageComponentExtensions.cs index 938d4c699..481cde3a9 100644 --- a/src/NadekoBot/_Extensions/SocketMessageComponentExtensions.cs +++ b/src/NadekoBot/_Extensions/SocketMessageComponentExtensions.cs @@ -54,7 +54,7 @@ public static class SocketMessageComponentExtensions IEmbedBuilder? embed, string plainText = "", IReadOnlyCollection? embeds = null, - NadekoInteraction? inter = null, + NadekoButtonInteraction? inter = null, bool ephemeral = false) => smc.RespondAsync(plainText, embed: embed?.Build(), @@ -66,7 +66,7 @@ public static class SocketMessageComponentExtensions string text, MessageType type, bool ephemeral = false, - NadekoInteraction? inter = null) + NadekoButtonInteraction? inter = null) { var builder = eb.Create().WithDescription(text); diff --git a/src/NadekoBot/creds_example.yml b/src/NadekoBot/creds_example.yml index 1b42c0125..4b8b8ac01 100644 --- a/src/NadekoBot/creds_example.yml +++ b/src/NadekoBot/creds_example.yml @@ -7,13 +7,23 @@ token: '' ownerIds: [] # Keep this on 'true' unless you're sure your bot shouldn't use privileged intents or you're waiting to be accepted usePrivilegedIntents: true -# The number of shards that the bot will running on. +# The number of shards that the bot will be running on. # Leave at 1 if you don't know what you're doing. totalShards: 1 # Login to https://console.cloud.google.com, create a new project, go to APIs & Services -> Library -> YouTube Data API and enable it. # Then, go to APIs and Services -> Credentials and click Create credentials -> API key. # Used only for Youtube Data Api (at the moment). googleApiKey: '' +# 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 +google: + searchId: + imageSearchId: # Settings for voting system for discordbots. Meant for use on global Nadeko. votes: # top.gg votes service url diff --git a/src/NadekoBot/data/aliases.yml b/src/NadekoBot/data/aliases.yml index 9974540c1..221ffbedd 100644 --- a/src/NadekoBot/data/aliases.yml +++ b/src/NadekoBot/data/aliases.yml @@ -597,11 +597,10 @@ image: lmgtfy: - lmgtfy google: -- google -- g -duckduckgo: - - duckduckgo - - ddg + - google + - search + - g + - s hearthstone: - hearthstone - hs @@ -1304,3 +1303,8 @@ bankbalance: - balance - b - bal +patron: + - patron +patronmessage: + - patronmessage + - patronmsg \ No newline at end of file diff --git a/src/NadekoBot/data/bot.yml b/src/NadekoBot/data/bot.yml index 16b52251b..179684427 100644 --- a/src/NadekoBot/data/bot.yml +++ b/src/NadekoBot/data/bot.yml @@ -67,18 +67,19 @@ helpText: |- # List of modules and commands completely blocked on the bot blocked: commands: [] - modules: [] + modules: + - nsfw # Which string will be used to recognize the commands prefix: . # Toggles whether your bot will group greet/bye messages into a single message every 5 seconds. # 1st user who joins will get greeted immediately # If more users join within the next 5 seconds, they will be greeted in groups of 5. -# This will cause %user.mention% and other placeholders to be replaced with multiple users. +# This will cause %user.mention% and other placeholders to be replaced with multiple users. # Keep in mind this might break some of your embeds - for example if you have %user.avatar% in the thumbnail, # it will become invalid, as it will resolve to a list of avatars of grouped users. # note: This setting is primarily used if you're afraid of raids, or you're running medium/large bots where some -# servers might get hundreds of people join at once. This is used to prevent the bot from getting ratelimited, -# and (slightly) reduce the greet spam in those servers. +# servers might get hundreds of people join at once. This is used to prevent the bot from getting ratelimited, +# and (slightly) reduce the greet spam in those servers. groupGreets: false # Whether the bot will rotate through all specified statuses. # This setting can be changed via .ropl command. diff --git a/src/NadekoBot/data/gambling.yml b/src/NadekoBot/data/gambling.yml index 35995d741..47070691c 100644 --- a/src/NadekoBot/data/gambling.yml +++ b/src/NadekoBot/data/gambling.yml @@ -52,10 +52,10 @@ generation: timely: # How much currency will the users get every time they run .timely command # setting to 0 or less will disable this feature - amount: 0 + amount: 120 # How often (in hours) can users claim currency with .timely command # setting to 0 or less will disable this feature - cooldown: 24 + cooldown: 12 # How much will each user's owned currency decay over time. decay: # Percentage of user's current currency which will be deducted every 24h. diff --git a/src/NadekoBot/data/images.yml b/src/NadekoBot/data/images.yml index b277d8718..73ea5e676 100644 --- a/src/NadekoBot/data/images.yml +++ b/src/NadekoBot/data/images.yml @@ -1,5 +1,5 @@ # DO NOT CHANGE -version: 3 +version: 4 coins: heads: - https://cdn.nadeko.bot/coins/heads3.png @@ -24,7 +24,7 @@ rategirl: matrix: https://cdn.nadeko.bot/other/rategirl/matrix.png dot: https://cdn.nadeko.bot/other/rategirl/dot.png xp: - bg: https://cdn.nadeko.bot/other/xp/bg.png + bg: https://cdn.nadeko.bot/other/xp/bg_k.png rip: bg: https://cdn.nadeko.bot/other/rip/rip.png overlay: https://cdn.nadeko.bot/other/rip/overlay.png diff --git a/src/NadekoBot/data/images/frame_gold.png b/src/NadekoBot/data/images/frame_gold.png new file mode 100644 index 000000000..860129c5a Binary files /dev/null and b/src/NadekoBot/data/images/frame_gold.png differ diff --git a/src/NadekoBot/data/images/frame_silver.png b/src/NadekoBot/data/images/frame_silver.png new file mode 100644 index 000000000..25fc31f65 Binary files /dev/null and b/src/NadekoBot/data/images/frame_silver.png differ diff --git a/src/NadekoBot/data/patron.yml b/src/NadekoBot/data/patron.yml new file mode 100644 index 000000000..e569870fa --- /dev/null +++ b/src/NadekoBot/data/patron.yml @@ -0,0 +1,67 @@ +# DO NOT CHANGE +version: 1 +# Whether the patronage feature is enabled +isEnabled: false +# List of patron only features and relevant quota data +quotas: +# Dictionary of feature names with their respective limits. Set to null for unlimited + features: + "timely:extra_percent": + v: 10 + x: 22 + xx: 50 + l: 150 + c: 350 + "rero:max_count": + v: 25 + x: 50 + "cleverbot:response": + v: -20 + x: 5000 + xx: 12000 + l: 35000 + c: 100000 + # Dictionary of commands with their respective quota data + commands: + prune: + x: + perHour: 1 + xx: + perHour: 3 + google: + v: + perDay: 15 + x: + perDay: 30 + xx: + perDay: 60 + l: + perDay: 150 + c: + perDay: 300 + image: + v: + perDay: 15 + x: + perDay: 30 + xx: + perDay: 60 + l: + perDay: 150 + c: + perDay: 300 + youtube: + v: + perDay: 25 + x: + perDay: 50 + xx: + perDay: 100 + l: + perDay: 250 + c: + perDay: 500 + # Dictionary of groups with their respective quota data + groups: {} + # Dictionary of modules with their respective quota data + modules: {} diff --git a/src/NadekoBot/data/searches.yml b/src/NadekoBot/data/searches.yml new file mode 100644 index 000000000..343f26b53 --- /dev/null +++ b/src/NadekoBot/data/searches.yml @@ -0,0 +1,40 @@ +# Which engine should .search command +# 'google' requires googleApiKey and google.searchId set in creds.yml +# 'searx' requires at least one searx instance specified in the 'searxInstances' property below +webSearchEngine: Google +# Which engine should .image command use +# 'google' requires googleApiKey and google.imageSearchId set in creds.yml +# 'searx' requires at least one searx instance specified in the 'searxInstances' property below +imgSearchEngine: Google +# Which search provider will be used for the `.youtube` command. +# +# - `ytDataApiv3` - uses google's official youtube data api. Requires `GoogleApiKey` set in creds and youtube data api enabled in developers console +# +# - `ytdl` - default, uses youtube-dl. Requires `youtube-dl` to be installed and it's path added to env variables. Slow. +# +# - `ytdlp` - recommended easy, uses `yt-dlp`. Requires `yt-dlp` to be installed and it's path added to env variables +# +# - `invidious` - recommended advanced, uses invidious api. Requires at least one invidious instance specified in the `invidiousInstances` property +ytProvider: Ytdl +# Set the searx instance urls in case you want to use 'searx' for either img or web search. +# Nadeko will use a random one for each request. +# Use a fully qualified url. Example: `https://my-searx-instance.mydomain.com` +# Instances specified must support 'format=json' query parameter. +# - In case you're running your own searx instance, set +# +# search: +# formats: +# - json +# +# in 'searxng/settings.yml' on your server +# +# - If you're using a public instance, make sure that the instance you're using supports it (they usually don't) +searxInstances: [] +# Set the invidious instance urls in case you want to use 'invidious' for `.youtube` search +# Nadeko will use a random one for each request. +# These instances may be used for music queue functionality in the future. +# Use a fully qualified url. Example: https://my-invidious-instance.mydomain.com +# +# Instances specified must have api available. +# You check that by opening an api endpoint in your browser. For example: https://my-invidious-instance.mydomain.com/api/v1/trending +invidiousInstances: [] diff --git a/src/NadekoBot/data/strings/commands/commands.en-US.yml b/src/NadekoBot/data/strings/commands/commands.en-US.yml index 56faa5091..b74fe840b 100644 --- a/src/NadekoBot/data/strings/commands/commands.en-US.yml +++ b/src/NadekoBot/data/strings/commands/commands.en-US.yml @@ -193,7 +193,7 @@ iamnot: args: - "Gamer" expradd: - desc: "Add a expression with a trigger and a response. Running this command in server requires the Administration permission. Running this command in DM is Bot Owner only and adds a new global expression. Guide here: " + desc: "Add an expression with a trigger and a response. Running this command in server requires the Administration permission. Running this command in DM is Bot Owner only and adds a new global expression. Guide here: " args: - "\"hello\" Hi there %user.mention%" exprlist: @@ -278,7 +278,7 @@ rolehoist: desc: "Toggles whether this role is displayed in the sidebar or not. The role you specify has to be lower in the role hierarchy than your highest role." args: - "Guests" - - "\"Space Wizards\"" + - "Space Wizards" createrole: desc: "Creates a role with a given name." args: @@ -380,7 +380,7 @@ setnick: setavatar: desc: "Sets a new avatar image for the NadekoBot. Parameter is a direct link to an image." args: - - "http://i.imgur.com/xTG3a1I.jpg" + - "https://i.imgur.com/xTG3a1I.jpg" setgame: desc: "Sets the bots game status to either Playing, Listening, or Watching." args: @@ -578,7 +578,9 @@ serverblacklist: - "" - "2" cmdcooldown: - desc: "Sets a cooldown per user for a command or a expression. Set it to 0 to remove the cooldown." + desc: "Sets a cooldown, in seconds, for a command or an expression which will be applied per user. + Set it to 0 to remove the cooldown. + Supports a special command `cleverbot:response` which can be used limit how often users can talk to cleverbot" args: - ".h 5" - ".pat 30" @@ -1040,11 +1042,11 @@ urbandict: args: - "Pineapple" catfact: - desc: "Shows a random catfact from " + desc: "Shows a random catfact from " args: - "" yomama: - desc: "Shows a random joke from " + desc: "Shows a random joke from " args: - "" randjoke: @@ -2209,3 +2211,11 @@ bankbalance: desc: "Shows your current bank balance available for withdrawal." args: - "" +patron: + desc: "Check your patronage status and command usage quota. Bot owners can check targeted user's patronage status." + args: + - "" +patronmessage: + desc: "Sends a message to all patrons of the specified tier and higher. Supports embeds." + args: + - "x hello" diff --git a/src/NadekoBot/data/strings/responses/responses.en-US.json b/src/NadekoBot/data/strings/responses/responses.en-US.json index 0e8bd7d7d..0bb9bbecd 100644 --- a/src/NadekoBot/data/strings/responses/responses.en-US.json +++ b/src/NadekoBot/data/strings/responses/responses.en-US.json @@ -248,7 +248,6 @@ "command_not_found": "I can't find that command. Please verify that the command exists before trying again.", "desc": "Description", "cant_dm": "I can't dm you. Make sure your DMs are open. Go to **options (cog button) -> Privacy & Safety -> Allow direct messages from server members**", - "donate": "You can support the NadekoBot project on \nPatreon <{0}> or\nPaypal <{1}>\nDon't forget to leave your discord name or id in the message.\n\n**Thank you** ♥️", "guide": "**List of commands**: <{0}>\n**Hosting guides and docs can be found here**: <{1}>", "list_of_modules": "List of modules", "module_not_found": "That module does not exist.", @@ -999,5 +998,21 @@ "bank_deposited": "You deposited {0} to your bank account.", "bank_withdrew": "You withdrew {0} from your bank account.", "bank_withdraw_insuff": "You don't have sufficient {0} in your bank account.", - "cmd_group_commands": "'{0}' command group" + "cmd_group_commands": "'{0}' command group", + "limit_reached": "Feature limit of {0} reached.", + "feature_limit_reached_you": "You've reached the limit of {0} for the {1} feature. You may be able to increase this limit by upgrading your patron tier.", + "feature_limit_reached_owner": "Server owner has reached the limit of {0} for the {1} feature. Server owner may be able to upgrade this limit by upgrading patron tier.", + "feature_limit_reached_either": "The limit of {0} for the {1} feature has been reached. Either you or the server owner may able to upgrade this limit by upgrading the patron tier.", + "tier": "Tier", + "pledge": "Pledge", + "expires": "Expires", + "commands": "Commands", + "groups": "Groups", + "modules": "Modules", + "no_quota_found": "No quota found", + "patron_info": "Patron Info", + "quotas": "<<< Quotas >>>", + "patron_not_enabled": "Patron system is disabled.", + "results_in": "{0} results in {1}s", + "patron_msg_sent": "Done sending messages to patrons at and above tier {1}. {1} successfully sent and {2} failed." } diff --git a/src/NadekoBot/data/xp_template.json b/src/NadekoBot/data/xp_template.json index 73eb61ee7..a1ef4d6d2 100644 --- a/src/NadekoBot/data/xp_template.json +++ b/src/NadekoBot/data/xp_template.json @@ -1,7 +1,8 @@ { + "Version": 1, "output_size": { - "X": 450, - "Y": 220 + "X": 800, + "Y": 392 }, "User": { "Name": { @@ -16,12 +17,12 @@ "Icon": { "Show": true, "Pos": { - "X": 32, - "Y": 10 + "X": 14, + "Y": 14 }, "Size": { - "X": 69, - "Y": 70 + "X": 72, + "Y": 71 } }, "GlobalLevel": { @@ -30,7 +31,7 @@ "FontSize": 45, "Pos": { "X": 47, - "Y": 149 + "Y": 160 } }, "GuildLevel": { @@ -39,7 +40,7 @@ "FontSize": 45, "Pos": { "X": 47, - "Y": 297 + "Y": 308 } }, "GlobalRank": { @@ -60,27 +61,6 @@ "Y": 326 } }, - "TimeOnLevel": { - "Format": "{0}d{1}h{2}m", - "Global": { - "Color": "ffffffff", - "Show": true, - "FontSize": 20, - "Pos": { - "X": 50, - "Y": 204 - } - }, - "Guild": { - "Color": "ffffffff", - "Show": true, - "FontSize": 20, - "Pos": { - "X": 50, - "Y": 351 - } - } - }, "Xp": { "Bar": { "Show": true, @@ -116,8 +96,8 @@ "Show": true, "FontSize": 50, "Pos": { - "X": 430, - "Y": 142 + "X": 528, + "Y": 170 } }, "Guild": { @@ -125,8 +105,8 @@ "Show": true, "FontSize": 50, "Pos": { - "X": 400, - "Y": 282 + "X": 490, + "Y": 313 } }, "Awarded": { @@ -134,8 +114,8 @@ "Show": true, "FontSize": 25, "Pos": { - "X": 445, - "Y": 347 + "X": 490, + "Y": 345 } } } diff --git a/src/NadekoBot/data/xp_template_backup.json b/src/NadekoBot/data/xp_template_backup.json deleted file mode 100644 index 73eb61ee7..000000000 --- a/src/NadekoBot/data/xp_template_backup.json +++ /dev/null @@ -1,165 +0,0 @@ -{ - "output_size": { - "X": 450, - "Y": 220 - }, - "User": { - "Name": { - "Color": "ffffffff", - "Show": true, - "FontSize": 50, - "Pos": { - "X": 130, - "Y": 17 - } - }, - "Icon": { - "Show": true, - "Pos": { - "X": 32, - "Y": 10 - }, - "Size": { - "X": 69, - "Y": 70 - } - }, - "GlobalLevel": { - "Color": "ffffffff", - "Show": true, - "FontSize": 45, - "Pos": { - "X": 47, - "Y": 149 - } - }, - "GuildLevel": { - "Color": "ffffffff", - "Show": true, - "FontSize": 45, - "Pos": { - "X": 47, - "Y": 297 - } - }, - "GlobalRank": { - "Color": "ffffffff", - "Show": true, - "FontSize": 30, - "Pos": { - "X": 148, - "Y": 179 - } - }, - "GuildRank": { - "Color": "ffffffff", - "Show": true, - "FontSize": 30, - "Pos": { - "X": 148, - "Y": 326 - } - }, - "TimeOnLevel": { - "Format": "{0}d{1}h{2}m", - "Global": { - "Color": "ffffffff", - "Show": true, - "FontSize": 20, - "Pos": { - "X": 50, - "Y": 204 - } - }, - "Guild": { - "Color": "ffffffff", - "Show": true, - "FontSize": 20, - "Pos": { - "X": 50, - "Y": 351 - } - } - }, - "Xp": { - "Bar": { - "Show": true, - "Global": { - "Color": "00000066", - "PointA": { - "X": 321, - "Y": 104 - }, - "PointB": { - "X": 286, - "Y": 235 - }, - "Length": 450, - "Direction": 3 - }, - "Guild": { - "Color": "00000066", - "PointA": { - "X": 282, - "Y": 248 - }, - "PointB": { - "X": 247, - "Y": 379 - }, - "Length": 450, - "Direction": 3 - } - }, - "Global": { - "Color": "ffffffff", - "Show": true, - "FontSize": 50, - "Pos": { - "X": 430, - "Y": 142 - } - }, - "Guild": { - "Color": "ffffffff", - "Show": true, - "FontSize": 50, - "Pos": { - "X": 400, - "Y": 282 - } - }, - "Awarded": { - "Color": "ffffffff", - "Show": true, - "FontSize": 25, - "Pos": { - "X": 445, - "Y": 347 - } - } - } - }, - "Club": { - "Icon": { - "Show": true, - "Pos": { - "X": 722, - "Y": 25 - }, - "Size": { - "X": 45, - "Y": 45 - } - }, - "Name": { - "Color": "ffffffff", - "Show": true, - "FontSize": 35, - "Pos": { - "X": 650, - "Y": 49 - } - } - } -} \ No newline at end of file diff --git a/src/ayu/Ayu.Discord.Voice/VoiceClient.cs b/src/ayu/Ayu.Discord.Voice/VoiceClient.cs index 9a97a7105..31cfd5f97 100644 --- a/src/ayu/Ayu.Discord.Voice/VoiceClient.cs +++ b/src/ayu/Ayu.Discord.Voice/VoiceClient.cs @@ -147,7 +147,7 @@ namespace Ayu.Discord.Voice Buffer.BlockCopy(nonce, 0, rtpData, rtpDataLength - 4, 4); gw.SendRtpData(rtpData, rtpDataLength); - // todo future When there's a break in the sent data, + // FUTURE When there's a break in the sent data, // the packet transmission shouldn't simply stop. // Instead, send five frames of silence (0xF8, 0xFF, 0xFE) // before stopping to avoid unintended Opus interpolation