From 7b5145f1161e386c6e21cf0c35ab9eba05c87ea5 Mon Sep 17 00:00:00 2001 From: Kwoth Date: Tue, 14 Jun 2022 07:24:33 +0000 Subject: [PATCH] NadekoBot Patronage system, Search commands improvements + fixes --- .gitlab-ci.yml | 13 +- CHANGELOG.md | 129 +- src/Nadeko.Medusa/IEmbedBuilder.cs | 2 +- src/Nadeko.Medusa/Nadeko.Medusa.csproj | 1 - src/NadekoBot/Bot.cs | 9 + .../DontAddToIocContainerAttribute.cs | 11 + .../NoPublicBotPrecondition.cs | 20 +- src/NadekoBot/Common/Creds.cs | 19 +- src/NadekoBot/Common/IBotCredentials.cs | 1 + .../Interaction/NadekoActionInteraction.cs | 10 +- .../Common/Interaction/NadekoInteraction.cs | 28 +- .../Interaction/NadekoInteractionArray.cs | 43 + .../Interaction/NadekoInteractionBuilder.cs | 3 +- .../Interaction/NadekoInteractionData.cs | 2 +- .../Interaction/NadekoOwnInteraction.cs | 4 +- src/NadekoBot/Common/NInteraction.cs | 26 + src/NadekoBot/Common/NadekoModule.cs | 20 +- src/NadekoBot/Common/SmartText/SmartText.cs | 2 - src/NadekoBot/Db/Models/DiscordUser.cs | 2 + src/NadekoBot/Db/Models/GuildConfig.cs | 2 +- src/NadekoBot/Db/Models/PatronQuota.cs | 38 + src/NadekoBot/Db/Models/RewardedUser.cs | 4 +- src/NadekoBot/Db/NadekoContext.cs | 71 +- ...0220614071410_patronage-system.Designer.cs | 3481 ++++++++++++++++ .../MySql/20220614071410_patronage-system.cs | 176 + .../MySql/MysqlContextModelSnapshot.cs | 98 +- ...0220614071421_patronage-system.Designer.cs | 3621 +++++++++++++++++ .../20220614071421_patronage-system.cs | 170 + .../PostgreSqlContextModelSnapshot.cs | 98 +- .../Sqlite/20210707002343_cleanup.cs | 2 +- ...0220614071359_patronage-system.Designer.cs | 2796 +++++++++++++ .../Sqlite/20220614071359_patronage-system.cs | 123 + .../NadekoSqliteContextModelSnapshot.cs | 73 +- .../DangerousCommandsService.cs | 2 + .../Administration/Prune/PruneService.cs | 15 +- .../Role/IReactionRoleService.cs | 16 +- .../Role/ReactionRoleCommands.cs | 27 +- .../Role/ReactionRolesService.cs | 107 +- .../Administration/Role/RoleCommands.cs | 2 +- .../Modules/Gambling/Bank/BankCommands.cs | 2 +- .../Modules/Gambling/Bank/BankService.cs | 16 + .../Modules/Gambling/Bank/IBankService.cs | 1 + .../Modules/Gambling/CashInteraction.cs | 18 +- src/NadekoBot/Modules/Gambling/Gambling.cs | 48 +- .../Gambling/Wheel/WheelOfFortuneCommands.cs | 49 - .../Gambling/{Wheel => }/WheelOfFortune.cs | 0 .../Games/ChatterBot/ChatterbotService.cs | 127 +- .../Games/ChatterBot/CleverBotCommands.cs | 1 - .../ChatterBot/_Common/ChatterBotSession.cs | 34 - .../ChatterBot/_Common/CleverbotResponse.cs | 12 - .../_Common/OfficialCleverbotSession.cs | 53 - src/NadekoBot/Modules/Help/Help.cs | 100 +- src/NadekoBot/Modules/Help/HelpService.cs | 19 + .../DonateSelfhostingInteraction.cs | 12 + .../DonateTroubleshootInteraction.cs | 12 + .../Music/Services/extractor/YtLoader.cs | 2 +- .../Modules/Music/_Common/Impl/MusicPlayer.cs | 2 +- .../_Common/Resolvers/RadioResolveStrategy.cs | 14 - .../CommandCooldown/CleverBotResponseStr.cs | 10 + .../CleverbotResponseCmdCdTypeReader.cs | 15 + .../CommandCooldown/CmdCdsCommands.cs | 18 +- .../Modules/Searches/Feeds/FeedsService.cs | 48 +- .../Search/DefaultSearchServiceFactory.cs | 60 + .../DuckDuckGoSeachService.cs | 65 + .../Search/Google/GoogleCustomSearchResult.cs | 22 + .../Searches/Search/Google/GoogleImageData.cs | 12 + .../Search/Google/GoogleImageResult.cs | 19 + .../Search/Google/GoogleImageResultEntry.cs | 13 + .../Google/GoogleSearchResultInformation.cs | 13 + .../Search/Google/GoogleSearchService.cs | 66 + .../Google/OfficialGoogleSearchResultEntry.cs | 19 + .../GoogleScrape/GoogleScrapeService.cs | 62 + .../Searches/Search/IImageSearchResult.cs | 13 + .../Modules/Searches/Search/ISearchResult.cs | 8 + .../Searches/Search/ISearchResultEntry.cs | 9 + .../Search/ISearchResultInformation.cs | 7 + .../Modules/Searches/Search/ISearchService.cs | 9 + .../Searches/Search/ISearchServiceFactory.cs | 10 + .../Modules/Searches/Search/SearchCommands.cs | 206 + .../Searches/Search/SearchServiceBase.cs | 9 + .../Search/Searx/SearxImageSearchResult.cs | 28 + .../Searx/SearxImageSearchResultEntry.cs | 14 + .../Searches/Search/Searx/SearxInfobox.cs | 30 + .../Search/Searx/SearxSearchAttribute.cs | 15 + .../Search/Searx/SearxSearchResult.cs | 47 + .../Search/Searx/SearxSearchResultEntry.cs | 51 + .../Searx/SearxSearchResultInformation.cs | 7 + .../Search/Searx/SearxSearchService.cs | 76 + .../Searches/Search/Searx/SearxUrlData.cs | 15 + .../Search/Youtube/IYoutubeSearchService.cs | 6 + .../Search/Youtube/InvidiousSearchResponse.cs | 9 + .../Youtube/InvidiousYtSearchService.cs | 46 + .../Searches/Search/Youtube/VideoInfo.cs | 9 + .../Youtube/YoutubeDataApiSearchService.cs | 26 + .../Youtube/YtdlYoutubeSearchService.cs | 7 + .../Youtube/YtdlpYoutubeSearchService.cs | 7 + .../Search/Youtube/YtdlxServiceBase.cs | 34 + src/NadekoBot/Modules/Searches/Searches.cs | 147 +- .../Modules/Searches/SearchesService.cs | 172 +- .../StreamNotificationService.cs | 2 - .../{Services => _Common}/AtlExtensions.cs | 0 .../_Common/Config/ImgSearchEngine.cs | 7 + .../Searches/_Common/Config/SearchesConfig.cs | 63 + .../_Common/Config/SearchesConfigService.cs | 31 + .../_Common/Config/WebSearchEngine.cs | 7 + .../Modules/Utility/Info/InfoCommands.cs | 21 +- .../Utility/Patreon/PatreonRewardsService.cs | 305 -- .../Patronage/Config/PatronConfigData.cs | 37 + .../Utility/Patronage/Config/PatronTier.cs | 14 + .../Patronage/Config/PatronageConfig.cs | 18 + .../Utility/Patronage/Config/QuotaPer.cs | 8 + .../Patronage/CurrencyRewardService.cs | 190 + .../Utility/Patronage/FeatureQuotaStats.cs | 8 + .../Utility/Patronage/InsufficientTier.cs | 11 + .../Patronage/Patreon/PatreonClient.cs | 150 + .../Patronage/Patreon/PatreonCredentials.cs | 10 + .../Patronage/Patreon/PatreonMemberData.cs | 33 + .../Patreon/PatreonRefreshData.cs | 0 .../Patreon/PatreonSubscriptionHandler.cs | 81 + .../Modules/Utility/Patronage/Patron.cs | 38 + .../Utility/Patronage/PatronExtensions.cs | 39 + .../Utility/Patronage/PatronageCommands.cs | 153 + .../Utility/Patronage/PatronageService.cs | 834 ++++ .../Modules/Utility/Patronage/QuotaLimit.cs | 66 + .../Patronage/SubscriptionChargeStatus.cs | 10 + .../Utility/Patronage/UserQuotaStats.cs | 25 + .../Utility/Patronage/_common/IPatronData.cs | 11 + .../Patronage/_common/IPatronageService.cs | 56 + .../Patronage/_common/ISubscriptionHandler.cs | 16 + .../Modules/Utility/Remind/RemindService.cs | 9 +- src/NadekoBot/Modules/Utility/Utility.cs | 3 - .../VerboseErrors/VerboseErrorsService.cs | 36 +- .../Utility/_Common/Patreon/PatreonData.cs | 4 +- src/NadekoBot/Modules/Xp/Xp.cs | 1 + src/NadekoBot/Modules/Xp/XpService.cs | 209 +- .../Modules/Xp/_Common/XpTemplate.cs | 63 +- src/NadekoBot/NadekoBot.csproj | 4 +- .../Currency/CurrencyServiceExtensions.cs | 2 +- src/NadekoBot/Services/IGoogleApiService.cs | 14 - .../Services/Impl/BotCredsProvider.cs | 9 +- .../Services/Impl/GoogleApiService.cs | 41 +- .../Services/Impl/RedisImagesCache.cs | 9 + src/NadekoBot/Services/Impl/StatsService.cs | 2 +- src/NadekoBot/Services/Impl/YtdlOperation.cs | 10 +- .../Services/Settings/ConfigParsers.cs | 5 + .../Services/Settings/ConfigServiceBase.cs | 2 +- .../_Extensions/BotCredentialsExtensions.cs | 5 +- src/NadekoBot/_Extensions/Extensions.cs | 10 + .../_Extensions/IMessageChannelExtensions.cs | 14 +- .../_Extensions/ImagesharpExtensions.cs | 21 + .../SocketMessageComponentExtensions.cs | 4 +- src/NadekoBot/creds_example.yml | 12 +- src/NadekoBot/data/aliases.yml | 14 +- src/NadekoBot/data/bot.yml | 9 +- src/NadekoBot/data/gambling.yml | 4 +- src/NadekoBot/data/images.yml | 4 +- src/NadekoBot/data/images/frame_gold.png | Bin 0 -> 49859 bytes src/NadekoBot/data/images/frame_silver.png | Bin 0 -> 46029 bytes src/NadekoBot/data/patron.yml | 67 + src/NadekoBot/data/searches.yml | 40 + .../data/strings/commands/commands.en-US.yml | 22 +- .../strings/responses/responses.en-US.json | 19 +- src/NadekoBot/data/xp_template.json | 50 +- src/NadekoBot/data/xp_template_backup.json | 165 - src/ayu/Ayu.Discord.Voice/VoiceClient.cs | 2 +- 165 files changed, 14920 insertions(+), 1457 deletions(-) create mode 100644 src/NadekoBot/Common/Attributes/DontAddToIocContainerAttribute.cs rename src/NadekoBot/Common/{ => Attributes}/NoPublicBotPrecondition.cs (56%) create mode 100644 src/NadekoBot/Common/Interaction/NadekoInteractionArray.cs create mode 100644 src/NadekoBot/Common/NInteraction.cs create mode 100644 src/NadekoBot/Db/Models/PatronQuota.cs create mode 100644 src/NadekoBot/Migrations/MySql/20220614071410_patronage-system.Designer.cs create mode 100644 src/NadekoBot/Migrations/MySql/20220614071410_patronage-system.cs create mode 100644 src/NadekoBot/Migrations/Postgresql/20220614071421_patronage-system.Designer.cs create mode 100644 src/NadekoBot/Migrations/Postgresql/20220614071421_patronage-system.cs create mode 100644 src/NadekoBot/Migrations/Sqlite/20220614071359_patronage-system.Designer.cs create mode 100644 src/NadekoBot/Migrations/Sqlite/20220614071359_patronage-system.cs delete mode 100644 src/NadekoBot/Modules/Gambling/Wheel/WheelOfFortuneCommands.cs rename src/NadekoBot/Modules/Gambling/{Wheel => }/WheelOfFortune.cs (100%) delete mode 100644 src/NadekoBot/Modules/Games/ChatterBot/_Common/ChatterBotSession.cs create mode 100644 src/NadekoBot/Modules/Help/Interactions/DonateSelfhostingInteraction.cs create mode 100644 src/NadekoBot/Modules/Help/Interactions/DonateTroubleshootInteraction.cs create mode 100644 src/NadekoBot/Modules/Permissions/CommandCooldown/CleverBotResponseStr.cs create mode 100644 src/NadekoBot/Modules/Permissions/CommandCooldown/CleverbotResponseCmdCdTypeReader.cs create mode 100644 src/NadekoBot/Modules/Searches/Search/DefaultSearchServiceFactory.cs create mode 100644 src/NadekoBot/Modules/Searches/Search/DuckDuckGoScrape/DuckDuckGoSeachService.cs create mode 100644 src/NadekoBot/Modules/Searches/Search/Google/GoogleCustomSearchResult.cs create mode 100644 src/NadekoBot/Modules/Searches/Search/Google/GoogleImageData.cs create mode 100644 src/NadekoBot/Modules/Searches/Search/Google/GoogleImageResult.cs create mode 100644 src/NadekoBot/Modules/Searches/Search/Google/GoogleImageResultEntry.cs create mode 100644 src/NadekoBot/Modules/Searches/Search/Google/GoogleSearchResultInformation.cs create mode 100644 src/NadekoBot/Modules/Searches/Search/Google/GoogleSearchService.cs create mode 100644 src/NadekoBot/Modules/Searches/Search/Google/OfficialGoogleSearchResultEntry.cs create mode 100644 src/NadekoBot/Modules/Searches/Search/GoogleScrape/GoogleScrapeService.cs create mode 100644 src/NadekoBot/Modules/Searches/Search/IImageSearchResult.cs create mode 100644 src/NadekoBot/Modules/Searches/Search/ISearchResult.cs create mode 100644 src/NadekoBot/Modules/Searches/Search/ISearchResultEntry.cs create mode 100644 src/NadekoBot/Modules/Searches/Search/ISearchResultInformation.cs create mode 100644 src/NadekoBot/Modules/Searches/Search/ISearchService.cs create mode 100644 src/NadekoBot/Modules/Searches/Search/ISearchServiceFactory.cs create mode 100644 src/NadekoBot/Modules/Searches/Search/SearchCommands.cs create mode 100644 src/NadekoBot/Modules/Searches/Search/SearchServiceBase.cs create mode 100644 src/NadekoBot/Modules/Searches/Search/Searx/SearxImageSearchResult.cs create mode 100644 src/NadekoBot/Modules/Searches/Search/Searx/SearxImageSearchResultEntry.cs create mode 100644 src/NadekoBot/Modules/Searches/Search/Searx/SearxInfobox.cs create mode 100644 src/NadekoBot/Modules/Searches/Search/Searx/SearxSearchAttribute.cs create mode 100644 src/NadekoBot/Modules/Searches/Search/Searx/SearxSearchResult.cs create mode 100644 src/NadekoBot/Modules/Searches/Search/Searx/SearxSearchResultEntry.cs create mode 100644 src/NadekoBot/Modules/Searches/Search/Searx/SearxSearchResultInformation.cs create mode 100644 src/NadekoBot/Modules/Searches/Search/Searx/SearxSearchService.cs create mode 100644 src/NadekoBot/Modules/Searches/Search/Searx/SearxUrlData.cs create mode 100644 src/NadekoBot/Modules/Searches/Search/Youtube/IYoutubeSearchService.cs create mode 100644 src/NadekoBot/Modules/Searches/Search/Youtube/InvidiousSearchResponse.cs create mode 100644 src/NadekoBot/Modules/Searches/Search/Youtube/InvidiousYtSearchService.cs create mode 100644 src/NadekoBot/Modules/Searches/Search/Youtube/VideoInfo.cs create mode 100644 src/NadekoBot/Modules/Searches/Search/Youtube/YoutubeDataApiSearchService.cs create mode 100644 src/NadekoBot/Modules/Searches/Search/Youtube/YtdlYoutubeSearchService.cs create mode 100644 src/NadekoBot/Modules/Searches/Search/Youtube/YtdlpYoutubeSearchService.cs create mode 100644 src/NadekoBot/Modules/Searches/Search/Youtube/YtdlxServiceBase.cs rename src/NadekoBot/Modules/Searches/{Services => _Common}/AtlExtensions.cs (100%) create mode 100644 src/NadekoBot/Modules/Searches/_Common/Config/ImgSearchEngine.cs create mode 100644 src/NadekoBot/Modules/Searches/_Common/Config/SearchesConfig.cs create mode 100644 src/NadekoBot/Modules/Searches/_Common/Config/SearchesConfigService.cs create mode 100644 src/NadekoBot/Modules/Searches/_Common/Config/WebSearchEngine.cs delete mode 100644 src/NadekoBot/Modules/Utility/Patreon/PatreonRewardsService.cs create mode 100644 src/NadekoBot/Modules/Utility/Patronage/Config/PatronConfigData.cs create mode 100644 src/NadekoBot/Modules/Utility/Patronage/Config/PatronTier.cs create mode 100644 src/NadekoBot/Modules/Utility/Patronage/Config/PatronageConfig.cs create mode 100644 src/NadekoBot/Modules/Utility/Patronage/Config/QuotaPer.cs create mode 100644 src/NadekoBot/Modules/Utility/Patronage/CurrencyRewardService.cs create mode 100644 src/NadekoBot/Modules/Utility/Patronage/FeatureQuotaStats.cs create mode 100644 src/NadekoBot/Modules/Utility/Patronage/InsufficientTier.cs create mode 100644 src/NadekoBot/Modules/Utility/Patronage/Patreon/PatreonClient.cs create mode 100644 src/NadekoBot/Modules/Utility/Patronage/Patreon/PatreonCredentials.cs create mode 100644 src/NadekoBot/Modules/Utility/Patronage/Patreon/PatreonMemberData.cs rename src/NadekoBot/Modules/Utility/{ => Patronage}/Patreon/PatreonRefreshData.cs (100%) create mode 100644 src/NadekoBot/Modules/Utility/Patronage/Patreon/PatreonSubscriptionHandler.cs create mode 100644 src/NadekoBot/Modules/Utility/Patronage/Patron.cs create mode 100644 src/NadekoBot/Modules/Utility/Patronage/PatronExtensions.cs create mode 100644 src/NadekoBot/Modules/Utility/Patronage/PatronageCommands.cs create mode 100644 src/NadekoBot/Modules/Utility/Patronage/PatronageService.cs create mode 100644 src/NadekoBot/Modules/Utility/Patronage/QuotaLimit.cs create mode 100644 src/NadekoBot/Modules/Utility/Patronage/SubscriptionChargeStatus.cs create mode 100644 src/NadekoBot/Modules/Utility/Patronage/UserQuotaStats.cs create mode 100644 src/NadekoBot/Modules/Utility/Patronage/_common/IPatronData.cs create mode 100644 src/NadekoBot/Modules/Utility/Patronage/_common/IPatronageService.cs create mode 100644 src/NadekoBot/Modules/Utility/Patronage/_common/ISubscriptionHandler.cs create mode 100644 src/NadekoBot/data/images/frame_gold.png create mode 100644 src/NadekoBot/data/images/frame_silver.png create mode 100644 src/NadekoBot/data/patron.yml create mode 100644 src/NadekoBot/data/searches.yml delete mode 100644 src/NadekoBot/data/xp_template_backup.json 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 0000000000000000000000000000000000000000..860129c5a6af79f5762ad9b346de210ee8186e0f GIT binary patch literal 49859 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 literal 0 HcmV?d00001 diff --git a/src/NadekoBot/data/images/frame_silver.png b/src/NadekoBot/data/images/frame_silver.png new file mode 100644 index 0000000000000000000000000000000000000000..25fc31f65eb7ff345ecde8b8b14f2fed09cb5ac5 GIT binary patch literal 46029 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 literal 0 HcmV?d00001 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